diff --git a/package.json b/package.json
index d044586b466..8aae2403061 100644
--- a/package.json
+++ b/package.json
@@ -100,14 +100,16 @@
"electron-squirrel-startup": "^0.1.4",
"font-awesome": "https://github.com/FortAwesome/Font-Awesome/archive/v4.4.0.tar.gz",
"get-object-path": "azer/get-object-path#74eb42de0cfd02c14ffdd18552f295aba723d394",
- "hadron-action": "^0.0.2",
+ "hadron-action": "^0.0.4",
"hadron-auto-update-manager": "^0.0.12",
"hadron-compile-cache": "^0.1.0",
- "hadron-component-registry": "^0.2.0",
+ "hadron-component-registry": "^0.3.0",
+ "hadron-document": "^0.12.0",
"hadron-ipc": "^0.0.7",
"hadron-module-cache": "^0.0.3",
"hadron-package-manager": "0.1.0",
"hadron-reflux-store": "^0.0.2",
+ "hadron-type-checker": "^0.3.0",
"highlight.js": "^8.9.1",
"jquery": "^2.1.4",
"kerberos": "mongodb-js/kerberos#bc619b1b9213eb4cdae786cf3fb916fc7be66758",
@@ -120,8 +122,8 @@
"marky-mark": "^1.2.1",
"moment": "^2.10.6",
"mongodb-collection-model": "^0.2.3",
- "mongodb-connection-model": "^4.2.0",
- "mongodb-data-service": "^0.2.1",
+ "mongodb-connection-model": "^4.3.0",
+ "mongodb-data-service": "^0.3.0",
"mongodb-database-model": "^0.1.2",
"mongodb-explain-plan-model": "^0.2.0",
"mongodb-extended-json": "^1.6.0",
@@ -137,6 +139,7 @@
"qs": "^5.2.0",
"raf": "^3.1.0",
"react": "^0.14.8",
+ "react-bootstrap": "0.29.5",
"react-dom": "^0.14.8",
"semver": "^5.1.0",
"storage-mixin": "^0.6.2",
diff --git a/src/app/documents/document-list.js b/src/app/documents/document-list.js
index 00e4ecc652c..a4cb8ba0351 100644
--- a/src/app/documents/document-list.js
+++ b/src/app/documents/document-list.js
@@ -25,7 +25,8 @@ var DocumentListView = View.extend({
Action.filterChanged(app.queryOptions.query.serialize());
},
render: function() {
- ReactDOM.render(React.createElement(this.documentList), this.el.parentNode);
+ var container = this.el.parentNode.parentNode.parentNode;
+ ReactDOM.render(React.createElement(this.documentList), container);
return this;
},
reset: function() {
diff --git a/src/app/documents/index.less b/src/app/documents/index.less
index b96bf540a33..c7aa5229df5 100644
--- a/src/app/documents/index.less
+++ b/src/app/documents/index.less
@@ -1,5 +1,5 @@
.truncate-text-mixin() {
- overflow-x: hidden;
+ // overflow-x: hidden; // commenting out to make the dropdowns work
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
@@ -31,6 +31,7 @@ ol.document-list {
}
li.document-list-item {
+ position: relative;
font-family: @font-family-monospace;
font-size: 11px;
padding-bottom: 10px;
@@ -97,6 +98,15 @@ ol.document-list {
}
}
+ .document-property.code > .document-property-value {
+ &::before {
+ content: "Code('";
+ }
+ &::after {
+ content: "')";
+ }
+ }
+
li.document-property.array, li.document-property.object {
display: block;
margin-left: -16px;
@@ -114,8 +124,8 @@ ol.document-list {
display: inline-block;
width: 12px;
height: 1em;
- margin-left: 2px;
- margin-right: 10px;
+ margin-left: -8px !important; // remove important hack later
+ margin-right: 4px;
.caret-right;
}
@@ -130,18 +140,21 @@ ol.document-list {
}
ol.document-property-body {
- // margin-left: 21px;
- padding-left: 36px;
- // border-left: 1px dotted @gray6;
+ padding-left: 0;
display: none;
+
+ .document-property-key,
+ .editable-key {
+ margin-left: 16px;
+ }
}
&.expanded {
> .document-property-header {
> .caret {
.caret-down;
vertical-align: bottom;
- margin-left: 4px;
- margin-right: 8px;
+ margin-left: -10px !important; // remove important hack later
+ margin-right: 2px;
}
}
> ol.document-property-body {
diff --git a/src/app/explain-plan/index.less b/src/app/explain-plan/index.less
index 28483502905..164985881af 100644
--- a/src/app/explain-plan/index.less
+++ b/src/app/explain-plan/index.less
@@ -1,5 +1,27 @@
.explain-container {
+ // hacks for json view on explain tab
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.array ol.document-property-body .document-property-key,
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.object ol.document-property-body .document-property-key {
+ margin-left: 0;
+ }
+
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.array.expanded > ol.document-property-body,
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.object.expanded > ol.document-property-body {
+ margin-left: 16px;
+ }
+
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.array.expanded,
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.object.expanded {
+ margin-left: 0;
+ }
+
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.array,
+ ol.document-list li.document-list-item ol.document-property-body li.document-property.object {
+ margin-left: 0;
+ }
+ // end hacks
+
.summary-container {
background: @pw;
padding: 12px;
@@ -59,7 +81,6 @@
float: left;
margin-right: 24px;
}
-
}
i.link {
diff --git a/src/app/index.less b/src/app/index.less
index eff4a442be6..467bb27d09d 100644
--- a/src/app/index.less
+++ b/src/app/index.less
@@ -22,3 +22,5 @@
@import "../auto-update/index.less";
@import "../help/index.less";
@import "metrics/index.less";
+
+@import "../internal-packages/crud/styles/crud.less";
diff --git a/src/app/models/preferences.js b/src/app/models/preferences.js
index 69234b853cb..63b6be38544 100644
--- a/src/app/models/preferences.js
+++ b/src/app/models/preferences.js
@@ -144,6 +144,14 @@ var Preferences = Model.extend(storageMixin, {
required: true,
default: false
},
+ /**
+ * Allow single document CRUD.
+ */
+ singleDocumentCrud: {
+ type: 'boolean',
+ required: true,
+ default: false
+ },
/**
* Switches to enable/disable various authentication / ssl types
*
diff --git a/src/app/styles/caret.less b/src/app/styles/caret.less
index f7a6d94939a..ba2a2982c3d 100644
--- a/src/app/styles/caret.less
+++ b/src/app/styles/caret.less
@@ -2,7 +2,6 @@
display: inline-block;
width: 0;
height: 0;
- margin-left: 2px;
vertical-align: middle;
}
.caret-down {
diff --git a/src/app/styles/palette.less b/src/app/styles/palette.less
index ec3ffb376c1..8f96636c041 100644
--- a/src/app/styles/palette.less
+++ b/src/app/styles/palette.less
@@ -44,6 +44,7 @@
@pw: #fff;
@cautionOrange: @alertOrange;
@errorBackground: #fdd0d1;
+@greenBg: #e9f2e3;
/* Text Colors */
/* default text color is @gray1 */
@@ -76,7 +77,10 @@
/* Alert Colors */
@alertOrange: #fbb129;
+@alertOrangeBg: #fff4dc;
+@alertOrangeBorder: #a06c29;
@alertRed: #ef4c4c;
+@alertRedBg: #feeded;
@alertBlue: #43b1e5;
/* Semantic Colors */
diff --git a/src/internal-packages/crud/lib/actions.js b/src/internal-packages/crud/lib/actions.js
new file mode 100644
index 00000000000..4ddd3f84356
--- /dev/null
+++ b/src/internal-packages/crud/lib/actions.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const Reflux = require('reflux');
+
+const Actions = Reflux.createActions([
+ 'documentRemoved',
+ 'openInsertDocumentDialog',
+ 'insertDocument'
+]);
+
+module.exports = Actions;
diff --git a/src/internal-packages/crud/lib/component/binary-value.jsx b/src/internal-packages/crud/lib/component/binary-value.jsx
new file mode 100644
index 00000000000..2513b27b835
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/binary-value.jsx
@@ -0,0 +1,71 @@
+'use strict';
+
+const React = require('react');
+const truncate = require('hadron-component-registry').truncate;
+
+/**
+ * Base 64 constant.
+ */
+const BASE_64 = 'base64';
+
+/**
+ * The new UUID type.
+ */
+const UUID = 4;
+
+/**
+ * The old UUID type.
+ */
+const UUID_OLD = 3;
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * Binary value component.
+ */
+class BinaryValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single binary value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ {this.renderValue()}
+
+ );
+ }
+
+ /**
+ * Render the value.
+ *
+ * @returns {Component} The component.
+ */
+ renderValue() {
+ var type = this.value.sub_type;
+ var buffer = this.value.buffer;
+ if (type === UUID || type === UUID_OLD) {
+ return `Binary('${truncate(buffer.toString())}')`;
+ }
+ return `Binary('${truncate(buffer.toString(BASE_64))}')`;
+ }
+}
+
+BinaryValue.displayName = 'BinaryValue';
+
+module.exports = BinaryValue;
diff --git a/src/internal-packages/crud/lib/component/cancel-edit-button.jsx b/src/internal-packages/crud/lib/component/cancel-edit-button.jsx
new file mode 100644
index 00000000000..4b4d4fd6181
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/cancel-edit-button.jsx
@@ -0,0 +1,38 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the cancel button.
+ */
+class CancelEditButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+ Cancel
+
+ );
+ }
+}
+
+CancelEditButton.displayName = 'CancelEditButton';
+
+module.exports = CancelEditButton;
diff --git a/src/internal-packages/crud/lib/component/cancel-insert-button.jsx b/src/internal-packages/crud/lib/component/cancel-insert-button.jsx
new file mode 100644
index 00000000000..f971e79eb0b
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/cancel-insert-button.jsx
@@ -0,0 +1,38 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the cancel button.
+ */
+class CancelInsertButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+ Cancel
+
+ );
+ }
+}
+
+CancelInsertButton.displayName = 'CancelInsertButton';
+
+module.exports = CancelInsertButton;
diff --git a/src/internal-packages/crud/lib/component/clone-document-button.jsx b/src/internal-packages/crud/lib/component/clone-document-button.jsx
new file mode 100644
index 00000000000..1d90ba60353
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/clone-document-button.jsx
@@ -0,0 +1,35 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the clone document button.
+ */
+class CloneDocumentButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+CloneDocumentButton.displayName = 'CloneDocumentButton';
+
+module.exports = CloneDocumentButton;
diff --git a/src/internal-packages/crud/lib/component/code-value.jsx b/src/internal-packages/crud/lib/component/code-value.jsx
new file mode 100644
index 00000000000..48c7c1f0538
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/code-value.jsx
@@ -0,0 +1,42 @@
+'use strict';
+
+const React = require('react');
+const truncate = require('hadron-component-registry').truncate;
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * Code value component.
+ */
+class CodeValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single max key value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ {truncate(this.value.code)}
+
+ );
+ }
+}
+
+CodeValue.displayName = 'CodeValue';
+
+module.exports = CodeValue;
diff --git a/src/internal-packages/crud/lib/component/delete-document-button.jsx b/src/internal-packages/crud/lib/component/delete-document-button.jsx
new file mode 100644
index 00000000000..f91f549473c
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/delete-document-button.jsx
@@ -0,0 +1,35 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the edit document button.
+ */
+class DeleteDocumentButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+DeleteDocumentButton.displayName = 'DeleteDocumentButton';
+
+module.exports = DeleteDocumentButton;
diff --git a/src/internal-packages/crud/lib/component/document-actions.jsx b/src/internal-packages/crud/lib/component/document-actions.jsx
new file mode 100644
index 00000000000..7b073d80666
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/document-actions.jsx
@@ -0,0 +1,51 @@
+'use strict';
+
+const React = require('react');
+const app = require('ampersand-app');
+const EditDocumentButton = require('./edit-document-button');
+const DeleteDocumentButton = require('./delete-document-button');
+const CloneDocumentButton = require('./clone-document-button');
+
+/**
+ * The feature flag.
+ */
+const FEATURE = 'singleDocumentCrud';
+
+/**
+ * Component for actions on the document.
+ */
+class DocumentActions extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the actions.
+ *
+ * @returns {Component} The actions component.
+ */
+ render() {
+ if (app.isFeatureEnabled(FEATURE)) {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+
+ );
+ }
+}
+
+DocumentActions.displayName = 'DocumentActions';
+
+module.exports = DocumentActions;
diff --git a/src/internal-packages/crud/lib/component/document-footer.jsx b/src/internal-packages/crud/lib/component/document-footer.jsx
new file mode 100644
index 00000000000..8a3cec0b642
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/document-footer.jsx
@@ -0,0 +1,191 @@
+'use strict';
+
+const _ = require('lodash');
+const React = require('react');
+const Element = require('hadron-document').Element;
+const CancelEditButton = require('./cancel-edit-button');
+const UpdateButton = require('./update-button');
+
+/**
+ * The progress mode.
+ */
+const PROGRESS = 'Progress';
+
+/**
+ * The success mode.
+ */
+const SUCCESS = 'Success';
+
+/**
+ * The error mode.
+ */
+const ERROR = 'Error';
+
+/**
+ * The editing mode.
+ */
+const EDITING = 'Editing';
+
+/**
+ * The viewing mode.
+ */
+const VIEWING = 'Viewing';
+
+/**
+ * Map of modes to styles.
+ */
+const MODES = {
+ 'Progress': 'in-progress',
+ 'Success': 'success',
+ 'Error': 'error',
+ 'Editing': 'modified',
+ 'Viewing': 'viewing'
+}
+
+/**
+ * The empty message.
+ */
+const EMPTY = '';
+
+/**
+ * The modified message.
+ */
+const MODIFIED = 'Document Modified.';
+
+/**
+ * The updating message.
+ */
+const UPDATING = 'Updating Document.';
+
+/**
+ * The updated message.
+ */
+const UPDATED = 'Document Updated.';
+
+/**
+ * Component for a the edit document footer.
+ */
+class DocumentFooter extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.doc = props.doc;
+ this.updateStore = props.updateStore;
+ this.actions = props.actions;
+
+ this.doc.on(Element.Events.Added, this.handleModification.bind(this));
+ this.doc.on(Element.Events.Edited, this.handleModification.bind(this));
+ this.doc.on(Element.Events.Removed, this.handleModification.bind(this));
+ this.doc.on(Element.Events.Reverted, this.handleModification.bind(this));
+
+ this.state = { mode: VIEWING, message: EMPTY };
+ }
+
+ /**
+ * Subscribe to the update store on mount.
+ */
+ componentDidMount() {
+ this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this));
+ }
+
+ /**
+ * Unsubscribe from the udpate store on unmount.
+ */
+ componentWillUnmount() {
+ this.unsubscribeUpdate();
+ }
+
+ /**
+ * Handle the user clicking the cancel button.
+ */
+ handleCancel() {
+ this.doc.cancel();
+ this.setState({ mode: VIEWING });
+ }
+
+ /**
+ * Handle an error with the document update.
+ *
+ * @param {Error} error - The error.
+ */
+ handleError(error) {
+ this.setState({ mode: ERROR, message: error.message });
+ }
+
+ /**
+ * Handle modification to the document.
+ */
+ handleModification() {
+ this.setState({
+ mode: this.doc.isModified() ? EDITING : VIEWING,
+ message: MODIFIED
+ });
+ }
+
+ /**
+ * Handle the user clicking the update button.
+ */
+ handleUpdate() {
+ var object = this.props.doc.generateObject();
+ this.setState({ mode: PROGRESS, message: UPDATING });
+ this.actions.update(object);
+ }
+
+ /**
+ * Handle a successful document update.
+ */
+ handleSuccess() {
+ this.setState({ mode: SUCCESS, message: UPDATED });
+ }
+
+ /**
+ * Handles a trigger from the store.
+ *
+ * @param {Boolean} success - If the update succeeded.
+ * @param {Error, Document} object - The error or document.
+ */
+ handleStoreUpdate(success, object) {
+ if (success) {
+ this.handleSuccess();
+ } else {
+ this.handleError(object);
+ }
+ }
+
+ /**
+ * Render the footer.
+ *
+ * @returns {Component} The footer component.
+ */
+ render() {
+ return (
+
+
+ {this.state.message}
+
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Get the style of the footer based on the current mode.
+ *
+ * @returns {String} The style.
+ */
+ style() {
+ return `document-footer ${MODES[this.state.mode]}`;
+ }
+}
+
+DocumentFooter.displayName = 'DocumentFooter';
+
+module.exports = DocumentFooter;
diff --git a/src/internal-packages/crud/lib/component/document-list.jsx b/src/internal-packages/crud/lib/component/document-list.jsx
index 573aa47061b..426520e13aa 100644
--- a/src/internal-packages/crud/lib/component/document-list.jsx
+++ b/src/internal-packages/crud/lib/component/document-list.jsx
@@ -4,9 +4,16 @@ const _ = require('lodash');
const React = require('react');
const ReactDOM = require('react-dom');
const app = require('ampersand-app');
-const ElementFactory = require('hadron-component-registry').ElementFactory;
const Action = require('hadron-action');
-const DocumentListStore = require('../store/document-list-store');
+const ObjectID = require('bson').ObjectID;
+const Document = require('./document');
+const ResetDocumentListStore = require('../store/reset-document-list-store');
+const LoadMoreDocumentsStore = require('../store/load-more-documents-store');
+const RemoveDocumentStore = require('../store/remove-document-store');
+const InsertDocumentStore = require('../store/insert-document-store');
+const InsertDocumentDialog = require('./insert-document-dialog');
+const SamplingMessage = require('./sampling-message');
+const Actions = require('../actions');
/**
* The full document list container class.
@@ -18,45 +25,45 @@ const LIST_CLASS = 'document-list';
*/
const SCROLL_EVENT = 'scroll';
+/**
+ * Base empty doc for insert dialog.
+ */
+const EMPTY_DOC = { '': '' };
+
/**
* Component for the entire document list.
*/
class DocumentList extends React.Component {
+ /**
+ * Attach the scroll event to the parent container.
+ */
+ attachScrollEvent() {
+ this._node.parentNode.addEventListener(
+ SCROLL_EVENT,
+ this.handleScroll.bind(this)
+ );
+ }
+
/**
* Fetch the state when the component mounts.
*/
componentDidMount() {
- this._attachScrollEvent();
- this.unsubscribe = DocumentListStore.listen((documents, reset, count) => {
- if (reset) {
- // If resetting, then we need to go back to page one with
- // the documents as the filter changed. The loaded count and
- // total count are reset here as well.
- this.setState({
- docs: this._documentListItems(documents),
- currentPage: 1,
- count: count,
- loadedCount: documents.length
- });
- } else {
- // If not resetting we append the documents to the existing
- // list and increment the page. The loaded count is incremented
- // by the number of new documents.
- this.setState({
- docs: this.state.docs.concat(this._documentListItems(documents)),
- currentPage: (this.state.currentPage + 1),
- loadedCount: (this.state.loadedCount + documents.length)
- });
- }
- });
+ this.attachScrollEvent();
+ this.unsubscribeReset = ResetDocumentListStore.listen(this.handleReset.bind(this));
+ this.unsubscribeLoadMore = LoadMoreDocumentsStore.listen(this.handleLoadMore.bind(this));
+ this.unsubscribeRemove = RemoveDocumentStore.listen(this.handleRemove.bind(this));
+ this.unsibscribeInsert = InsertDocumentStore.listen(this.handleInsert.bind(this));
}
/**
* Unsibscribe from the document list store when unmounting.
*/
componentWillUnmount() {
- this.unsubscribe();
+ this.unsubscribeReset();
+ this.unsubscribeLoadMore();
+ this.unsubscribeRemove();
+ this.unsubscribeInsert();
}
/**
@@ -66,43 +73,60 @@ class DocumentList extends React.Component {
*/
constructor(props) {
super(props);
- this.state = { docs: [], currentPage: 0 };
+ this.state = { docs: [], nextSkip: 0 };
}
/**
- * Render the document list.
+ * Handle the loading of more documents.
*
- * @returns {React.Component} The document list.
+ * @param {Array} documents - The next batch of documents.
*/
- render() {
- return (
-
- {this.state.docs}
-
- );
+ handleLoadMore(documents) {
+ // If not resetting we append the documents to the existing
+ // list and increment the page. The loaded count is incremented
+ // by the number of new documents.
+ this.setState({
+ docs: this.state.docs.concat(this.renderDocuments(documents)),
+ nextSkip: (this.state.nextSkip + documents.length),
+ loadedCount: (this.state.loadedCount + documents.length)
+ });
}
/**
- * Attach the scroll event to the parent container.
+ * Handle the reset of the document list.
+ *
+ * @param {Array} documents - The documents.
+ * @param {Integer} count - The count.
*/
- _attachScrollEvent() {
- this.documentListNode = ReactDOM.findDOMNode(this);
- this.documentListNode.parentNode.addEventListener(
- SCROLL_EVENT,
- this._handleScroll.bind(this)
- );
+ handleReset(documents, count) {
+ // If resetting, then we need to go back to page one with
+ // the documents as the filter changed. The loaded count and
+ // total count are reset here as well.
+ this.setState({
+ docs: this.renderDocuments(documents),
+ nextSkip: documents.length,
+ count: count,
+ loadedCount: documents.length
+ });
}
/**
- * Get the document list item components.
- *
- * @param {Array} docs - The raw documents.
+ * Handles removal of a document from the document list.
*
- * @return {Array} The document list item components.
+ * @param {Object} id - The id of the removed document.
*/
- _documentListItems(docs) {
- return _.map(docs, (doc) => {
- return React.createElement(DocumentListItem, { doc: doc, key: doc._id });
+ handleRemove(id) {
+ var index = _.findIndex(this.state.docs, (component) => {
+ if (id instanceof ObjectID) {
+ return id.equals(component.props.doc._id);
+ }
+ return component.props.doc._id === id;
+ });
+ this.state.docs.splice(index, 1);
+ this.setState({
+ docs: this.state.docs,
+ loadedCount: (this.state.loadedCount - 1),
+ nextSkip: (this.state.nextSkip - 1)
});
}
@@ -111,31 +135,91 @@ class DocumentList extends React.Component {
*
* @param {Event} evt - The scroll event.
*/
- _handleScroll(evt) {
+ handleScroll(evt) {
var container = evt.srcElement;
- if (container.scrollTop > (this.documentListNode.offsetHeight - this._scrollDelta())) {
+ if (container.scrollTop > (this._node.offsetHeight - this._scrollDelta())) {
// If we are scrolling downwards, and have hit the distance to initiate a scroll
// from the end of the list, we will fire the event to load more documents.
- this._nextBatch();
+ this.loadMore();
+ }
+ }
+
+ /**
+ * Handle opening of the insert dialog.
+ */
+ handleOpenInsert() {
+ Actions.openInsertDocumentDialog(EMPTY_DOC);
+ }
+
+ /**
+ * Handle insert of a new document.
+ *
+ * @param {Boolean} success - If the insert was successful.
+ * @param {Object} object - The new document or error.
+ */
+ handleInsert(success, object) {
+ if (success) {
+ this.setState({ count: this.state.count + 1 });
+ this.loadMore();
}
- // Bonus: if we have passed a certain number of docs that are out of view:
- // this._unloadPreviousBatch();
- // Bonus: if we are scrolling back up and are running out of previous docs:
- // this._previousBatch();
- // Bonus: if we are scrolling up and docs below are out of view:
- // this._unloadNextBatch();
}
/**
* Get the next batch of documents. Will only fire if there are more documents
* in the collection to load.
*/
- _nextBatch() {
+ loadMore() {
if (this.state.loadedCount < this.state.count) {
- Action.fetchNextDocuments(this.state.currentPage);
+ Action.fetchNextDocuments(this.state.nextSkip);
}
}
+ /**
+ * Render the document list.
+ *
+ * @returns {React.Component} The document list.
+ */
+ render() {
+ return (
+
+
+
+
+
this._node = c}>
+ {this.state.docs}
+
+
+
+
+
+ );
+ }
+
+ /**
+ * Get the document list item components.
+ *
+ * @param {Array} docs - The raw documents.
+ *
+ * @return {Array} The document list item components.
+ */
+ renderDocuments(docs) {
+ return _.map(docs, (doc) => {
+ return ( );
+ });
+ }
+
+ /**
+ * Determine if the component should update.
+ *
+ * @param {Object} nextProps - The next properties.
+ * @param {Object} nextState - The next state.
+ */
+ shouldComponentUpdate(nextProps, nextState) {
+ return (nextState.docs.length !== this.state.docs.length) ||
+ (nextState.nextSkip !== this.state.nextSkip) ||
+ (nextState.loadedCount !== this.state.loadedCount);
+ }
+
/**
* Get the distance in pixels from the end of the document list to the point when
* scrolling where we want to load more documents.
@@ -144,50 +228,13 @@ class DocumentList extends React.Component {
*/
_scrollDelta() {
if (!this.scrollDelta) {
- this.scrollDelta = this.documentListNode.offsetHeight;
+ this.scrollDelta = this._node.offsetHeight;
}
return this.scrollDelta;
}
}
-/**
- * The class for the document itself.
- */
-const DOCUMENT_CLASS = 'document-property-body';
-
-/**
- * The class for the list item wrapper.
- */
-const LIST_ITEM_CLASS = 'document-list-item';
-
-/**
- * Component for a single document in a list of documents.
- */
-class DocumentListItem extends React.Component {
-
- /**
- * Render a single document list item.
- */
- render() {
- return (
-
-
- {ElementFactory.elements(this.props.doc)}
-
-
- );
- }
-}
-
-/**
- * Set the display names for all components.
- */
DocumentList.displayName = 'DocumentList';
-DocumentListItem.displayName = 'DocumentListItem';
-
-/**
- * Set the child components.
- */
-DocumentList.DocumentListItem = DocumentListItem;
+DocumentList.Document = Document;
module.exports = DocumentList;
diff --git a/src/internal-packages/crud/lib/component/document.jsx b/src/internal-packages/crud/lib/component/document.jsx
new file mode 100644
index 00000000000..4b4a4f3a332
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/document.jsx
@@ -0,0 +1,327 @@
+'use strict';
+
+const _ = require('lodash');
+const app = require('ampersand-app');
+const React = require('react');
+const Reflux = require('reflux');
+const ElementFactory = require('hadron-component-registry').ElementFactory;
+const NamespaceStore = require('hadron-reflux-store').NamespaceStore;
+const HadronDocument = require('hadron-document');
+const Element = require('hadron-document').Element;
+const Actions = require('../actions');
+const EditableElement = require('./editable-element');
+const DocumentActions = require('./document-actions');
+const DocumentFooter = require('./document-footer');
+const Hotspot = require('./hotspot');
+
+/**
+ * The class for the document itself.
+ */
+const DOCUMENT_CLASS = 'document-property-body';
+
+/**
+ * The class for the list item wrapper.
+ */
+const LIST_ITEM_CLASS = 'document-list-item';
+
+/**
+ * Component for a single document in a list of documents.
+ */
+class Document extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.doc = props.doc;
+ this.state = { doc: this.doc, editing: false };
+
+ // Actions need to be scoped to the single document component and not
+ // global singletons.
+ this.actions = Reflux.createActions([ 'update', 'remove' ]);
+
+ // The update store needs to be scoped to a document and not a global
+ // singleton.
+ this.updateStore = this.createUpdateStore(this.actions);
+ this.removeStore = this.createRemoveStore(this.actions);
+ }
+
+ /**
+ * Create the scoped update store.
+ *
+ * @param {Action} actions - The component reflux actions.
+ *
+ * @returns {Store} The scoped store.
+ */
+ createUpdateStore(actions) {
+ return Reflux.createStore({
+
+ /**
+ * Initialize the store.
+ */
+ init: function() {
+ this.ns = NamespaceStore.ns;
+ this.listenTo(actions.update, this.update);
+ },
+
+ /**
+ * Update the document in the database.
+ *
+ * @param {Object} object - The replacement document.
+ */
+ update: function(object) {
+ app.dataService.findOneAndReplace(
+ this.ns,
+ { _id: object._id },
+ object,
+ { returnOriginal: false },
+ this.handleResult
+ );
+ },
+
+ /**
+ * Handle the result from the driver.
+ *
+ * @param {Error} error - The error.
+ * @param {Object} doc - The document.
+ *
+ * @returns {Object} The trigger event.
+ */
+ handleResult: function(error, doc) {
+ return (error) ? this.trigger(false, error) : this.trigger(true, doc);
+ }
+ });
+ }
+
+ /**
+ * Create the scoped remove store.
+ *
+ * @param {Action} actions - The component reflux actions.
+ *
+ * @returns {Store} The scoped store.
+ */
+ createRemoveStore(actions) {
+ return Reflux.createStore({
+
+ /**
+ * Initialize the store.
+ */
+ init: function() {
+ this.ns = NamespaceStore.ns;
+ this.listenTo(actions.remove, this.remove);
+ },
+
+ /**
+ * Remove the document from the collection.
+ *
+ * @param {Object} object - The object to delete.
+ */
+ remove: function(object) {
+ app.dataService.deleteOne(this.ns, { _id: object._id }, {}, this.handleResult);
+ },
+
+ /**
+ * Handle the result from the driver.
+ *
+ * @param {Error} error - The error.
+ * @param {Object} doc - The document.
+ *
+ * @returns {Object} The trigger event.
+ */
+ handleResult: function(error, result) {
+ return (error) ? this.trigger(false, error) : this.trigger(true, result);
+ }
+ });
+ }
+
+ /**
+ * Subscribe to the update store on mount.
+ */
+ componentDidMount() {
+ this.unsubscribeUpdate = this.updateStore.listen(this.handleStoreUpdate.bind(this));
+ this.unsubscribeRemove = this.removeStore.listen(this.handleStoreRemove.bind(this));
+ }
+
+ /**
+ * Unsubscribe from the udpate store on unmount.
+ */
+ componentWillUnmount() {
+ this.unsubscribeUpdate();
+ this.unsubscribeRemove();
+ }
+
+ /**
+ * Handles a trigger from the store.
+ *
+ * @param {Boolean} success - If the update succeeded.
+ * @param {Error, Document} object - The error or document.
+ */
+ handleStoreUpdate(success, object) {
+ if (this.state.editing) {
+ if (success) {
+ this.handleUpdateSuccess(object);
+ }
+ }
+ }
+
+ /**
+ * Handles a trigger from the store.
+ *
+ * @param {Boolean} success - If the update succeeded.
+ * @param {Error, Document} object - The error or document.
+ */
+ handleStoreRemove(success) {
+ if (success) {
+ this.handleRemoveSuccess();
+ }
+ }
+
+ /**
+ * Handle a sucessful update.
+ *
+ * @param {Object} doc - The updated document.
+ */
+ handleUpdateSuccess(doc) {
+ this.doc = doc;
+ this.setState({ doc: doc, editing: false });
+ }
+
+ /**
+ * Handle a sucessful update.
+ *
+ * @param {Object} doc - The updated document.
+ */
+ handleRemoveSuccess() {
+ Actions.documentRemoved(this.doc._id);
+ }
+
+ /**
+ * Handle the editing of the document.
+ */
+ handleEdit() {
+ var doc = new HadronDocument(this.doc);
+ doc.on(Element.Events.Added, this.handleModify.bind(this));
+ doc.on(Element.Events.Removed, this.handleModify.bind(this));
+ doc.on(HadronDocument.Events.Cancel, this.handleCancel.bind(this));
+
+ this.setState({ doc: doc, editing: true });
+ }
+
+ /**
+ * Handles canceling edits to the document.
+ */
+ handleCancel() {
+ this.setState({ doc: this.doc, editing: false });
+ }
+
+ /**
+ * Handle cloning of the document.
+ */
+ handleClone() {
+ Actions.openInsertDocumentDialog(this.doc);
+ }
+
+ /**
+ * Handles document deletion.
+ */
+ handleDelete() {
+ this.actions.remove(this.doc);
+ }
+
+ /**
+ * Handles modification to the document.
+ */
+ handleModify() {
+ this.setState({});
+ }
+
+ /**
+ * Get the elements for the document. If we are editing, we get editable elements,
+ * otherwise the readonly elements are returned.
+ *
+ * @returns {Array} The elements.
+ */
+ elements() {
+ if (this.state.editing) {
+ return this.editableElements(this.state.doc);
+ }
+ return ElementFactory.elements(this.state.doc);
+ }
+
+ /**
+ * Get the editable elements.
+ *
+ * @returns {Array} The editable elements.
+ */
+ editableElements() {
+ var elements = _.map(this.state.doc.elements, (element) => {
+ return (
+
+ );
+ });
+ var lastElement = elements[elements.length - 1].props.element;
+ elements.push( );
+ return elements;
+ }
+
+ /**
+ * Render a single document list item.
+ */
+ render() {
+ return (
+
+
+
+ {this.elements()}
+
+ {this.renderActions()}
+
+ {this.renderFooter()}
+
+ );
+ }
+
+ /**
+ * Render the actions component.
+ *
+ * @returns {Component} The actions component.
+ */
+ renderActions() {
+ if (!this.state.editing) {
+ return (
+
+ );
+ }
+ }
+
+ /**
+ * Render the footer component.
+ *
+ * @returns {Component} The footer component.
+ */
+ renderFooter() {
+ if (this.state.editing) {
+ return (
+
+ );
+ }
+ }
+
+ style() {
+ var style = LIST_ITEM_CLASS;
+ if (this.state.editing) {
+ style = style.concat(' editing');
+ }
+ return style;
+ }
+}
+
+Document.displayName = 'Document';
+
+module.exports = Document;
diff --git a/src/internal-packages/crud/lib/component/edit-document-button.jsx b/src/internal-packages/crud/lib/component/edit-document-button.jsx
new file mode 100644
index 00000000000..c356b7e59b4
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/edit-document-button.jsx
@@ -0,0 +1,35 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the edit document button.
+ */
+class EditDocumentButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+EditDocumentButton.displayName = 'EditDocumentButton';
+
+module.exports = EditDocumentButton;
diff --git a/src/internal-packages/crud/lib/component/editable-element.jsx b/src/internal-packages/crud/lib/component/editable-element.jsx
new file mode 100644
index 00000000000..2e133f2824b
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/editable-element.jsx
@@ -0,0 +1,259 @@
+'use strict';
+
+const React = require('react');
+const Element = require('hadron-document').Element;
+const EditableKey = require('./editable-key');
+const EditableValue = require('./editable-value');
+const RevertAction = require('./revert-action');
+const RemoveAction = require('./remove-action');
+const NoAction = require('./no-action');
+const Types = require('./types');
+
+/**
+ * The added constant.
+ */
+const ADDED = 'added';
+
+/**
+ * The edited constant.
+ */
+const EDITED = 'edited';
+
+/**
+ * The removed constant.
+ */
+const REMOVED = 'removed';
+
+/**
+ * The editing class constant.
+ */
+const EDITING = 'editing';
+
+/**
+ * The caret for expanding elements.
+ */
+const CARET = 'caret';
+
+/**
+ * The class for the document itself.
+ */
+const DOCUMENT_CLASS = 'document-property-body';
+
+/**
+ * The header class for expandable elements.
+ */
+const HEADER_CLASS = 'document-property-header expandable';
+
+/**
+ * The property class.
+ */
+const PROPERTY_CLASS = 'document-property';
+
+/**
+ * The expandable label class.
+ */
+const LABEL_CLASS = 'document-property-type-label';
+
+/**
+ * The expanded class name.
+ */
+const EXPANDED = 'expanded';
+
+/**
+ * The non-expandable class.
+ */
+const NON_EXPANDABLE = 'non-expandable';
+
+/**
+ * Mappings for non editable value components.
+ */
+const VALUE_MAPPINGS = {
+ 'Binary': './binary-value',
+ 'MinKey': './min-key-value',
+ 'MaxKey': './max-key-value',
+ 'Code': './code-value',
+ 'Timestamp': './timestamp-value',
+ 'ObjectID': './objectid-value'
+};
+
+/**
+ * General editable element component.
+ */
+class EditableElement extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ this.state = { expanded: false };
+ this.element.on(Element.Events.Added, this.handleAdd.bind(this));
+ this.element.on(Element.Events.Edited, this.handleEdit.bind(this));
+ this.element.on(Element.Events.Removed, this.handleRemove.bind(this));
+ this.element.on(Element.Events.Reverted, this.handleRevert.bind(this));
+ }
+
+ /**
+ * Render a single editable element.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return this.element.elements ? this.renderExpandable() : this.renderNonExpandable();
+ }
+
+ /**
+ * Render a non-expandable element.
+ *
+ * @returns {Component} The component.
+ */
+ renderNonExpandable() {
+ return (
+
+
+ {this.renderAction()}
+
+ :
+ {this.renderValue()}
+
+
+ );
+ }
+
+ /**
+ * Render the value for the component.
+ *
+ * @returns {Component} The value component.
+ */
+ renderValue() {
+ if (this.element.isValueEditable()) {
+ return ( );
+ }
+ var props = { element: this.element };
+ return React.createElement(this.valueComponent(this.element.currentType), props);
+ }
+
+ /**
+ * Render an expandable element.
+ *
+ * @returns {Component} The component.
+ */
+ renderExpandable() {
+ return (
+
+
+
+ {this.renderAction()}
+
+
+ :
+
+ {this.element.currentType}
+
+
+
+ {this.elementComponents()}
+
+
+ );
+ }
+
+ /**
+ * Get the components for the elements.
+ *
+ * @returns {Array} The components.
+ */
+ elementComponents() {
+ return _.map(this.element.elements, (element) => {
+ return ( );
+ });
+ }
+
+ /**
+ * Get the revert or remove action.
+ *
+ * @returns {Component} The component.
+ */
+ renderAction() {
+ if (this.element.isEdited() || this.element.isRemoved()) {
+ return ( );
+ } else if (this.element.key === '_id') {
+ return ( );
+ }
+ return ( );
+ }
+
+ /**
+ * Handle the addition of an element.
+ */
+ handleAdd() {
+ this.setState({ expanded: true });
+ }
+
+ /**
+ * Here to re-render the component when a key or value is edited.
+ */
+ handleEdit() {
+ this.setState({});
+ }
+
+ /**
+ * Handle removal of an element.
+ */
+ handleRemove() {
+ this.setState({});
+ }
+
+ /**
+ * Here to re-render the component when an edit is reverted.
+ */
+ handleRevert() {
+ this.setState({});
+ }
+
+ /**
+ * Toggles the expandable aspect of the element.
+ */
+ toggleExpandable() {
+ this.setState({ expanded: !this.state.expanded });
+ }
+
+ /**
+ * Get the style for the element component.
+ *
+ * @returns {String} The element style.
+ */
+ style() {
+ var style = `${PROPERTY_CLASS} ${this.element.currentType.toLowerCase()}`;
+ if (this.element.isAdded()) {
+ style = style.concat(` ${ADDED}`);
+ } else if (this.element.isEdited()) {
+ style = style.concat(` ${EDITED}`);
+ } else if (this.element.isRemoved()) {
+ style = style.concat(` ${REMOVED}`);
+ }
+ if (!this.element.elements) {
+ style = style.concat(` ${NON_EXPANDABLE}`);
+ }
+ if (this.state.expanded) {
+ style = style.concat(` ${EXPANDED}`);
+ }
+ return style;
+ }
+
+ /**
+ * Get the value component for the type.
+ *
+ * @returns {Component} The value component.
+ */
+ valueComponent(type) {
+ return require(VALUE_MAPPINGS[type]);
+ }
+}
+
+EditableElement.displayName = 'EditableElement';
+
+module.exports = EditableElement;
diff --git a/src/internal-packages/crud/lib/component/editable-key.jsx b/src/internal-packages/crud/lib/component/editable-key.jsx
new file mode 100644
index 00000000000..c08af915e99
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/editable-key.jsx
@@ -0,0 +1,154 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The editing class constant.
+ */
+const EDITING = 'editing';
+
+/**
+ * The duplicate key value.
+ */
+const DUPLICATE = 'duplicate';
+
+/**
+ * The document key class.
+ */
+const KEY_CLASS = 'editable-key';
+
+/**
+ * General editable key component.
+ */
+class EditableKey extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ this.state = { duplcate: false, editing: false };
+ }
+
+ /**
+ * Focus on this field on mount, so the tab can do it's job and move
+ * to the value field.
+ */
+ componentDidMount() {
+ if (this.element.isAdded()) {
+ if (!this.isEditable() && this._node) {
+ this._node.focus();
+ }
+ }
+ }
+
+ /**
+ * Render a single editable key.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+ this._node = c}
+ size={this.element.currentKey.length}
+ onBlur={this.handleBlur.bind(this)}
+ onFocus={this.handleFocus.bind(this)}
+ onChange={this.handleChange.bind(this)}
+ onKeyDown={this.handleKeyDown.bind(this)}
+ value={this.element.currentKey}
+ title={this.renderTitle()} />
+ );
+ }
+
+ /**
+ * Render the title.
+ *
+ * @returns {String} The title.
+ */
+ renderTitle() {
+ if (this.state.duplicate) {
+ return `Duplicate key: '${this.element.currentKey}'`
+ }
+ return this.element.currentKey;
+ }
+ /**
+ * When hitting a key on the last element some special things may happen.
+ *
+ * @param {Event} evt - The event.
+ */
+ handleKeyDown(evt) {
+ if (evt.keyCode === 27) {
+ this._node.blur();
+ }
+ }
+
+ /**
+ * Handles changes to the element key.
+ *
+ * @param {Event} evt - The event.
+ */
+ handleChange(evt) {
+ var value = evt.target.value;
+ if (this.isEditable()) {
+ if (this.element.isDuplicateKey(value)) {
+ this.setState({ duplicate: true });
+ } else if (this.state.duplicate) {
+ this.setState({ duplicate: false });
+ }
+ this.element.rename(value);
+ }
+ }
+
+ /**
+ * Handle focus on the key.
+ */
+ handleFocus() {
+ if (this.isEditable()) {
+ this.setState({ editing: true });
+ }
+ }
+
+ /**
+ * Handle blur from the key.
+ */
+ handleBlur() {
+ if (this.isEditable()) {
+ this.setState({ editing: false });
+ }
+ }
+
+ /**
+ * Is this component editable?
+ *
+ * @returns {Boolean} If the component is editable.
+ */
+ isEditable() {
+ return this.element.isKeyEditable() && this.element.parentElement.currentType !== 'Array';
+ }
+
+ /**
+ * Get the style for the key of the element.
+ *
+ * @returns {String} The key style.
+ */
+ style() {
+ var style = KEY_CLASS;
+ if (this.state.editing) {
+ style = style.concat(` ${EDITING}`);
+ }
+ if (this.state.duplicate) {
+ style = style.concat(` ${DUPLICATE}`);
+ }
+ return style;
+ }
+}
+
+EditableKey.displayName = 'EditableKey';
+
+module.exports = EditableKey;
diff --git a/src/internal-packages/crud/lib/component/editable-value.jsx b/src/internal-packages/crud/lib/component/editable-value.jsx
new file mode 100644
index 00000000000..5857e763d53
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/editable-value.jsx
@@ -0,0 +1,107 @@
+'use strict';
+
+const _ = require('lodash');
+const React = require('react');
+const ElementFactory = require('hadron-component-registry').ElementFactory;
+const TypeChecker = require('hadron-type-checker');
+
+/**
+ * The editing class constant.
+ */
+const EDITING = 'editing';
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'editable-value';
+
+/**
+ * General editable value component.
+ */
+class EditableValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ this.state = { editing: false };
+ }
+
+ /**
+ * Render a single editable value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+ this._node = c}
+ type='text'
+ className={this.style()}
+ onBlur={this.handleBlur.bind(this)}
+ onFocus={this.handleFocus.bind(this)}
+ onChange={this.handleChange.bind(this)}
+ onKeyDown={this.handleKeyDown.bind(this)}
+ value={this.element.currentValue} />
+ );
+ }
+
+ /**
+ * When hitting a key on the last element some special things may happen.
+ *
+ * @param {Event} evt - The event.
+ */
+ handleKeyDown(evt) {
+ if (evt.keyCode === 9 && !evt.shiftKey) {
+ this.element.next();
+ } else if (evt.keyCode === 27) {
+ this._node.blur();
+ }
+ }
+
+ /**
+ * Handles changes to the element value.
+ *
+ * @param {Event} evt - The event.
+ */
+ handleChange(evt) {
+ var value = evt.target.value;
+ var currentType = this.element.currentType;
+ if (_.includes(TypeChecker.castableTypes(value), currentType)) {
+ this.element.edit(TypeChecker.cast(value, currentType));
+ } else {
+ this.element.edit(value);
+ }
+ }
+
+ /**
+ * Handle focus on the value.
+ */
+ handleFocus() {
+ this.setState({ editing: true });
+ }
+
+ /**
+ * Handle blur from the value.
+ */
+ handleBlur() {
+ this.setState({ editing: false });
+ }
+
+ /**
+ * Get the style for the value of the element.
+ *
+ * @returns {String} The value style.
+ */
+ style() {
+ return this.state.editing ? `${VALUE_CLASS} ${EDITING}` : VALUE_CLASS;
+ }
+}
+
+EditableValue.displayName = 'EditableValue';
+
+module.exports = EditableValue;
diff --git a/src/internal-packages/crud/lib/component/hotspot.jsx b/src/internal-packages/crud/lib/component/hotspot.jsx
new file mode 100644
index 00000000000..ecdc70ec4d5
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/hotspot.jsx
@@ -0,0 +1,46 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for add element hotspot.
+ */
+class Hotspot extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.doc = props.doc;
+ this.element = props.element;
+ }
+
+ /**
+ * When clicking on a hotspot we append to the parent.
+ */
+ handleClick() {
+ if (this.element && this.element.parentElement) {
+ this.element.next();
+ } else {
+ this.doc.add('', '');
+ }
+ }
+
+ /**
+ * Render the hotspot.
+ *
+ * @returns {Component} The hotspot component.
+ */
+ render() {
+ return (
+
+ );
+ }
+}
+
+Hotspot.displayName = 'Hotspot';
+
+module.exports = Hotspot;
diff --git a/src/internal-packages/crud/lib/component/insert-button.jsx b/src/internal-packages/crud/lib/component/insert-button.jsx
new file mode 100644
index 00000000000..86efe53ddef
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/insert-button.jsx
@@ -0,0 +1,38 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the insert button.
+ */
+class InsertButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+ Insert
+
+ );
+ }
+}
+
+InsertButton.displayName = 'InsertButton';
+
+module.exports = InsertButton;
diff --git a/src/internal-packages/crud/lib/component/insert-document-dialog.jsx b/src/internal-packages/crud/lib/component/insert-document-dialog.jsx
new file mode 100644
index 00000000000..39bf9f65240
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/insert-document-dialog.jsx
@@ -0,0 +1,69 @@
+'use strict';
+
+const React = require('react');
+const Modal = require('react-bootstrap').Modal;
+const OpenInsertDocumentDialogStore = require('../store/open-insert-document-dialog-store');
+const InsertDocument = require('./insert-document');
+const CancelInsertButton = require('./cancel-insert-button');
+const InsertButton = require('./insert-button');
+const Actions = require('../actions');
+
+/**
+ * Component for the insert document dialog.
+ */
+class InsertDocumentDialog extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.state = { open: false };
+ }
+
+ componentWillMount() {
+ this.unsubscribeOpen = OpenInsertDocumentDialogStore.listen(this.handleStoreOpen.bind(this));
+ }
+
+ componentWillUnmount() {
+ this.unsubscribeOpen();
+ }
+
+ handleStoreOpen(doc) {
+ this.setState({ doc: doc, open: true });
+ }
+
+ handleCancel() {
+ this.setState({ open: false });
+ }
+
+ handleInsert() {
+ this.setState({ open: false });
+ Actions.insertDocument(this.state.doc.generateObject());
+ }
+
+ render() {
+ return (
+
+
+ Insert Document
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+InsertDocumentDialog.displayName = 'InsertDocumentDialog';
+
+module.exports = InsertDocumentDialog;
diff --git a/src/internal-packages/crud/lib/component/insert-document.jsx b/src/internal-packages/crud/lib/component/insert-document.jsx
new file mode 100644
index 00000000000..b2b1ad5265e
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/insert-document.jsx
@@ -0,0 +1,85 @@
+'use strict';
+
+const _ = require('lodash');
+const React = require('react');
+const Element = require('hadron-document').Element;
+const EditableElement = require('./editable-element');
+const Hotspot = require('./hotspot');
+
+/**
+ * The class for the document itself.
+ */
+const DOCUMENT_CLASS = 'document-property-body';
+
+/**
+ * The full document list container class.
+ */
+const LIST_CLASS = 'document-list';
+
+/**
+ * The class for the list item wrapper.
+ */
+const LIST_ITEM_CLASS = 'document-list-item';
+
+/**
+ * Component for a single document in a list of documents.
+ */
+class InsertDocument extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.doc = props.doc;
+ this.doc.on(Element.Events.Added, this.handleModify.bind(this));
+ this.doc.on(Element.Events.Removed, this.handleModify.bind(this));
+ }
+
+ /**
+ * Handle modifications to the document.
+ */
+ handleModify() {
+ this.setState({});
+ }
+
+ /**
+ * Render a single document list item.
+ */
+ render() {
+ return (
+
+
+
+
+ {this.renderElements(this.doc)}
+
+
+
+
+ );
+ }
+
+ /**
+ * Get the editable elements.
+ *
+ * @returns {Array} The editable elements.
+ */
+ renderElements() {
+ var elements = _.map(this.doc.elements, (element) => {
+ return (
+
+ );
+ });
+ var lastComponent = elements[elements.length - 1];
+ var lastElement = lastComponent ? lastComponent.props.element : null;
+ elements.push( );
+ return elements;
+ }
+}
+
+InsertDocument.displayName = 'InsertDocument';
+
+module.exports = InsertDocument;
diff --git a/src/internal-packages/crud/lib/component/max-key-value.jsx b/src/internal-packages/crud/lib/component/max-key-value.jsx
new file mode 100644
index 00000000000..b0468c82a42
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/max-key-value.jsx
@@ -0,0 +1,41 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * MaxKey value component.
+ */
+class MaxKeyValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single max key value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ MaxKey
+
+ );
+ }
+}
+
+MaxKeyValue.displayName = 'MaxKeyValue';
+
+module.exports = MaxKeyValue;
diff --git a/src/internal-packages/crud/lib/component/min-key-value.jsx b/src/internal-packages/crud/lib/component/min-key-value.jsx
new file mode 100644
index 00000000000..9751e98281f
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/min-key-value.jsx
@@ -0,0 +1,41 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * MinKey value component.
+ */
+class MinKeyValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single min key value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ MinKey
+
+ );
+ }
+}
+
+MinKeyValue.displayName = 'MinKeyValue';
+
+module.exports = MinKeyValue;
diff --git a/src/internal-packages/crud/lib/component/no-action.jsx b/src/internal-packages/crud/lib/component/no-action.jsx
new file mode 100644
index 00000000000..b18dccf0bc7
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/no-action.jsx
@@ -0,0 +1,39 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The actions class.
+ */
+const ACTIONS = 'actions';
+
+/**
+ * General element action component.
+ */
+class NoAction extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ }
+
+ /**
+ * Render a single editable key.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ );
+ }
+}
+
+NoAction.displayName = 'NoAction';
+
+module.exports = NoAction;
diff --git a/src/internal-packages/crud/lib/component/objectid-value.jsx b/src/internal-packages/crud/lib/component/objectid-value.jsx
new file mode 100644
index 00000000000..90d7a77cbc6
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/objectid-value.jsx
@@ -0,0 +1,41 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * ObjectID value component.
+ */
+class ObjectIDValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single object id value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ {String(this.value)}
+
+ );
+ }
+}
+
+ObjectIDValue.displayName = 'ObjectIDValue';
+
+module.exports = ObjectIDValue;
diff --git a/src/internal-packages/crud/lib/component/open-insert-dialog-button.jsx b/src/internal-packages/crud/lib/component/open-insert-dialog-button.jsx
new file mode 100644
index 00000000000..c13261c1543
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/open-insert-dialog-button.jsx
@@ -0,0 +1,38 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the open insert dialog button.
+ */
+class OpenInsertDialogButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+ + Insert
+
+ );
+ }
+}
+
+OpenInsertDialogButton.displayName = 'OpenInsertDialogButton';
+
+module.exports = OpenInsertDialogButton;
diff --git a/src/internal-packages/crud/lib/component/remove-action.jsx b/src/internal-packages/crud/lib/component/remove-action.jsx
new file mode 100644
index 00000000000..f3d62b09f6a
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/remove-action.jsx
@@ -0,0 +1,48 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The actions class.
+ */
+const ACTIONS = 'actions';
+
+/**
+ * General element action component.
+ */
+class RemoveAction extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ }
+
+ /**
+ * Render a single editable key.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Remove the change.
+ */
+ handleClick() {
+ this.element.remove();
+ }
+}
+
+RemoveAction.displayName = 'RemoveAction';
+
+module.exports = RemoveAction;
diff --git a/src/internal-packages/crud/lib/component/revert-action.jsx b/src/internal-packages/crud/lib/component/revert-action.jsx
new file mode 100644
index 00000000000..9313e9dc109
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/revert-action.jsx
@@ -0,0 +1,48 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The actions class.
+ */
+const ACTIONS = 'actions';
+
+/**
+ * General element action component.
+ */
+class RevertAction extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ }
+
+ /**
+ * Render a single editable key.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Revert the change.
+ */
+ handleClick() {
+ this.element.revert();
+ }
+}
+
+RevertAction.displayName = 'RevertAction';
+
+module.exports = RevertAction;
diff --git a/src/internal-packages/crud/lib/component/sampling-message.jsx b/src/internal-packages/crud/lib/component/sampling-message.jsx
new file mode 100644
index 00000000000..04cbd669622
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/sampling-message.jsx
@@ -0,0 +1,104 @@
+'use strict';
+
+const React = require('react');
+const app = require('ampersand-app');
+const ResetDocumentListStore = require('../store/reset-document-list-store');
+const RemoveDocumentStore = require('../store/remove-document-store');
+const InsertDocumentStore = require('../store/insert-document-store');
+const OpenInsertDialogButton = require('./open-insert-dialog-button');
+
+/**
+ * The feature flag.
+ */
+const FEATURE = 'singleDocumentCrud';
+
+/**
+ * Component for the sampling message.
+ */
+class SamplingMessage extends React.Component {
+
+ /**
+ * Fetch the state when the component mounts.
+ */
+ componentDidMount() {
+ this.unsubscribeReset = ResetDocumentListStore.listen(this.handleReset.bind(this));
+ this.unsubscribeRemove = RemoveDocumentStore.listen(this.handleRemove.bind(this));
+ this.unsibscribeInsert = InsertDocumentStore.listen(this.handleInsert.bind(this));
+ }
+
+ /**
+ * Unsibscribe from the document list store when unmounting.
+ */
+ componentWillUnmount() {
+ this.unsubscribeReset();
+ this.unsubscribeRemove();
+ this.unsubscribeInsert();
+ }
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.state = { count: 0 };
+ }
+
+ /**
+ * Handle the reset of the document list.
+ *
+ * @param {Array} documents - The documents.
+ * @param {Integer} count - The count.
+ */
+ handleReset(documents, count) {
+ this.setState({ count: count });
+ }
+
+ /**
+ * Handles removal of a document from the document list.
+ */
+ handleRemove() {
+ this.setState({ count: this.state.count - 1 });
+ }
+
+ /**
+ * Handle insert of a new document.
+ *
+ * @param {Boolean} success - If the insert was successful.
+ * @param {Object} object - The new document or error.
+ */
+ handleInsert(success, object) {
+ if (success) {
+ this.setState({ count: this.state.count + 1 });
+ }
+ }
+
+ /**
+ * Render the sampling message.
+ *
+ * @returns {React.Component} The document list.
+ */
+ render() {
+ return (
+
+ Query returned {this.state.count} documents.
+
+ {this.renderInsertButton()}
+
+ );
+ }
+
+ /**
+ * Render the insert button.
+ */
+ renderInsertButton() {
+ if (app.isFeatureEnabled(FEATURE)) {
+ return ( );
+ }
+ }
+}
+
+SamplingMessage.displayName = 'SamplingMessage';
+
+module.exports = SamplingMessage;
diff --git a/src/internal-packages/crud/lib/component/timestamp-value.jsx b/src/internal-packages/crud/lib/component/timestamp-value.jsx
new file mode 100644
index 00000000000..54a953246d0
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/timestamp-value.jsx
@@ -0,0 +1,41 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * The document value class.
+ */
+const VALUE_CLASS = 'document-property-value';
+
+/**
+ * Timestamp value component.
+ */
+class TimestampValue extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.value = props.element.currentValue;
+ }
+
+ /**
+ * Render a single timestamp value.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return (
+
+ {String(this.value)}
+
+ );
+ }
+}
+
+TimestampValue.displayName = 'TimestampValue';
+
+module.exports = TimestampValue;
diff --git a/src/internal-packages/crud/lib/component/types.jsx b/src/internal-packages/crud/lib/component/types.jsx
new file mode 100644
index 00000000000..88de6fbf48e
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/types.jsx
@@ -0,0 +1,114 @@
+'use strict';
+
+const React = require('react');
+const Element = require('hadron-document').Element;
+const TypeChecker = require('hadron-type-checker');
+
+/**
+ * General types component.
+ */
+class Types extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ this.element = props.element;
+ }
+
+ /**
+ * Handles a change in the type.
+ *
+ * @param {Event} evt - The event.
+ */
+ handleTypeChange(evt) {
+ this.element.edit(TypeChecker.cast(this.castableValue(), evt.target.innerText));
+ }
+
+ /**
+ * Render a type list.
+ *
+ * @returns {React.Component} The element component.
+ */
+ render() {
+ return this.element.isValueEditable() ? this.renderDropdown() : this.renderLabel();
+ }
+
+ /**
+ * Render the type list dropdown.
+ *
+ * @returns {Component} The react component.
+ */
+ renderDropdown() {
+ return (
+
+
+ {this.element.currentType}
+
+
+
+
+ );
+ }
+
+ /**
+ * Render the type list label.
+ *
+ * @returns {Component} The react component.
+ */
+ renderLabel() {
+ return (
+
+ {this.element.currentType}
+
+ );
+ }
+
+ /**
+ * Render the types
+ *
+ * @returns {Component} The react component.
+ */
+ renderTypes() {
+ return _.map(TypeChecker.castableTypes(this.castableValue()), (type) => {
+ return (
+
+ {type}
+
+ );
+ });
+ }
+
+ /**
+ * Get the castable value for this value.
+ *
+ * @returns {Object} The cast value.
+ */
+ castableValue() {
+ if (this.element.elements) {
+ if (this.element.currentType === 'Object') {
+ return {};
+ }
+ return _.map(this.element.elements, (element) => {
+ return element.currentValue;
+ });
+ }
+ return this.element.currentValue;
+ }
+}
+
+Types.displayName = 'Types';
+
+module.exports = Types;
diff --git a/src/internal-packages/crud/lib/component/update-button.jsx b/src/internal-packages/crud/lib/component/update-button.jsx
new file mode 100644
index 00000000000..874f5a56733
--- /dev/null
+++ b/src/internal-packages/crud/lib/component/update-button.jsx
@@ -0,0 +1,38 @@
+'use strict';
+
+const React = require('react');
+
+/**
+ * Component for the update button.
+ */
+class UpdateButton extends React.Component {
+
+ /**
+ * The component constructor.
+ *
+ * @param {Object} props - The properties.
+ */
+ constructor(props) {
+ super(props);
+ }
+
+ /**
+ * Render the button.
+ *
+ * @returns {Component} The button component.
+ */
+ render() {
+ return (
+
+ Update
+
+ );
+ }
+}
+
+UpdateButton.displayName = 'UpdateButton';
+
+module.exports = UpdateButton;
diff --git a/src/internal-packages/crud/lib/store/document-list-store.js b/src/internal-packages/crud/lib/store/document-list-store.js
deleted file mode 100644
index ef325eb930b..00000000000
--- a/src/internal-packages/crud/lib/store/document-list-store.js
+++ /dev/null
@@ -1,55 +0,0 @@
-'use strict';
-
-const Reflux = require('reflux');
-const app = require('ampersand-app');
-const NamespaceStore = require('hadron-reflux-store').NamespaceStore;
-const Action = require('hadron-action');
-
-/**
- * The reflux store for the list of documents.
- */
-const DocumentListStore = Reflux.createStore({
-
- /**
- * Initialize the document list store.
- */
- init: function() {
- this.listenTo(Action.filterChanged, this._resetDocuments);
- this.listenTo(Action.fetchNextDocuments, this._fetchNextDocuments);
- },
-
- /**
- * This function is called when the collection filter changes.
- *
- * @param {Object} filter - The query filter.
- */
- _resetDocuments: function(filter) {
- var ns = NamespaceStore.ns;
- if (ns) {
- app.dataService.count(ns, filter, {}, (err, count) => {
- var options = { limit: 20, sort: [[ '_id', 1 ]] };
- app.dataService.find(ns, filter, options, (error, documents) => {
- this.trigger(documents, true, count);
- });
- });
- }
- },
-
- /**
- * Fetch the next page of documents.
- *
- * @param {Integer} currentPage - The current page in the view.
- */
- _fetchNextDocuments: function(currentPage) {
- var ns = NamespaceStore.ns;
- if (ns) {
- var filter = app.queryOptions.query.serialize();
- var options = { skip: (currentPage * 20), limit: 20, sort: [[ '_id', 1 ]] };
- app.dataService.find(ns, filter, options, (error, documents) => {
- this.trigger(documents, false);
- });
- }
- }
-});
-
-module.exports = DocumentListStore;
diff --git a/src/internal-packages/crud/lib/store/insert-document-store.jsx b/src/internal-packages/crud/lib/store/insert-document-store.jsx
new file mode 100644
index 00000000000..770f52afa22
--- /dev/null
+++ b/src/internal-packages/crud/lib/store/insert-document-store.jsx
@@ -0,0 +1,42 @@
+'use strict';
+
+const Reflux = require('reflux');
+const app = require('ampersand-app');
+const NamespaceStore = require('hadron-reflux-store').NamespaceStore;
+const Actions = require('../actions');
+
+/**
+ * The reflux store for inserting documents.
+ */
+const InsertDocumentStore = Reflux.createStore({
+
+ /**
+ * Initialize the insert document list store.
+ */
+ init: function() {
+ this.listenTo(Actions.insertDocument, this.insertDocument);
+ },
+
+ /**
+ * Insert the document.
+ *
+ * @param {Document} doc - The document to insert.
+ */
+ insertDocument: function(doc) {
+ app.dataService.insertOne(NamespaceStore.ns, doc, {}, this.handleResult.bind(this));
+ },
+
+ /**
+ * Handle the result from the driver.
+ *
+ * @param {Error} error - The error.
+ * @param {Object} doc - The document.
+ *
+ * @returns {Object} The trigger event.
+ */
+ handleResult: function(error, doc) {
+ return (error) ? this.trigger(false, error) : this.trigger(true, doc);
+ }
+});
+
+module.exports = InsertDocumentStore;
diff --git a/src/internal-packages/crud/lib/store/load-more-documents-store.js b/src/internal-packages/crud/lib/store/load-more-documents-store.js
new file mode 100644
index 00000000000..22c955b50c1
--- /dev/null
+++ b/src/internal-packages/crud/lib/store/load-more-documents-store.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const Reflux = require('reflux');
+const app = require('ampersand-app');
+const NamespaceStore = require('hadron-reflux-store').NamespaceStore;
+const Action = require('hadron-action');
+
+/**
+ * The reflux store for loading more documents.
+ */
+const LoadMoreDocumentsStore = Reflux.createStore({
+
+ /**
+ * Initialize the reset document list store.
+ */
+ init: function() {
+ this.listenTo(Action.fetchNextDocuments, this.loadMoreDocuments);
+ },
+
+ /**
+ * Fetch the next page of documents.
+ *
+ * @param {Integer} skip - The number of documents to skip.
+ */
+ loadMoreDocuments: function(skip) {
+ var filter = app.queryOptions.query.serialize();
+ var options = { skip: skip, limit: 20, sort: [[ '_id', 1 ]] };
+ app.dataService.find(NamespaceStore.ns, filter, options, (error, documents) => {
+ this.trigger(documents);
+ });
+ }
+});
+
+module.exports = LoadMoreDocumentsStore;
diff --git a/src/internal-packages/crud/lib/store/open-insert-document-dialog-store.js b/src/internal-packages/crud/lib/store/open-insert-document-dialog-store.js
new file mode 100644
index 00000000000..67fc1392043
--- /dev/null
+++ b/src/internal-packages/crud/lib/store/open-insert-document-dialog-store.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const Reflux = require('reflux');
+const Actions = require('../actions');
+const HadronDocument = require('hadron-document');
+
+/**
+ * The reflux store for opening the insert document dialog.
+ */
+const OpenInsertDocumentDialogStore = Reflux.createStore({
+
+ /**
+ * Initialize the reset document list store.
+ */
+ init: function() {
+ this.listenTo(Actions.openInsertDocumentDialog, this.openInsertDocumentDialog);
+ },
+
+ /**
+ * Open the insert document dialog.
+ *
+ * @param {Object} doc - The document to open the dialog with.
+ */
+ openInsertDocumentDialog: function(doc) {
+ var hadronDoc = new HadronDocument(doc, true);
+ // We need to remove the _id or we will get an duplicate key error on
+ // insert, and we currently do not allow editing of the _id field.
+ if (hadronDoc.elements[0].currentKey === '_id') {
+ hadronDoc.elements.shift();
+ }
+ this.trigger(hadronDoc);
+ }
+});
+
+module.exports = OpenInsertDocumentDialogStore;
diff --git a/src/internal-packages/crud/lib/store/remove-document-store.js b/src/internal-packages/crud/lib/store/remove-document-store.js
new file mode 100644
index 00000000000..c930a6c7daf
--- /dev/null
+++ b/src/internal-packages/crud/lib/store/remove-document-store.js
@@ -0,0 +1,28 @@
+'use strict';
+
+const Reflux = require('reflux');
+const Actions = require('../actions');
+
+/**
+ * The reflux store for removing a document from the list.
+ */
+const RemoveDocumentStore = Reflux.createStore({
+
+ /**
+ * Initialize the reset document list store.
+ */
+ init: function() {
+ this.listenTo(Actions.documentRemoved, this.remove);
+ },
+
+ /**
+ * This function is called when when a document is deleted.
+ *
+ * @param {Object} id - The document id.
+ */
+ remove: function(id) {
+ this.trigger(id);
+ },
+});
+
+module.exports = RemoveDocumentStore;
diff --git a/src/internal-packages/crud/lib/store/reset-document-list-store.js b/src/internal-packages/crud/lib/store/reset-document-list-store.js
new file mode 100644
index 00000000000..c7c764f3483
--- /dev/null
+++ b/src/internal-packages/crud/lib/store/reset-document-list-store.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const Reflux = require('reflux');
+const app = require('ampersand-app');
+const NamespaceStore = require('hadron-reflux-store').NamespaceStore;
+const Action = require('hadron-action');
+
+/**
+ * The reflux store for resetting the document list.
+ */
+const ResetDocumentListStore = Reflux.createStore({
+
+ /**
+ * Initialize the reset document list store.
+ */
+ init: function() {
+ this.listenTo(Action.filterChanged, this.reset);
+ },
+
+ /**
+ * This function is called when the collection filter changes.
+ *
+ * @param {Object} filter - The query filter.
+ */
+ reset: function(filter) {
+ app.dataService.count(NamespaceStore.ns, filter, {}, (err, count) => {
+ var options = { limit: 20, sort: [[ '_id', 1 ]] };
+ app.dataService.find(NamespaceStore.ns, filter, options, (error, documents) => {
+ this.trigger(documents, count);
+ });
+ });
+ },
+});
+
+module.exports = ResetDocumentListStore;
diff --git a/src/internal-packages/crud/styles/crud.less b/src/internal-packages/crud/styles/crud.less
new file mode 100644
index 00000000000..fec01559378
--- /dev/null
+++ b/src/internal-packages/crud/styles/crud.less
@@ -0,0 +1,402 @@
+li.document-list-item.editing {
+ box-shadow: 2px 5px 8px gainsboro;
+}
+
+.column.main {
+ .sampling-message {
+ }
+}
+
+.document-footer {
+ font-family: "Akzidenz", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ height: 28px;
+ vertical-align: middle;
+ top: 13px;
+ position: relative;
+
+ .edit-message {
+ font-size: 14px;
+ font-style: italic;
+ padding-left: 10px;
+ padding-top: 4px;
+ color: @pw;
+ overflow: hidden;
+ height: 28px;
+ }
+
+ .document-footer-actions {
+ position: absolute;
+ right: 10px;
+ top: 2px;
+ padding-top: 1px;
+
+ .cancel {
+ font-weight: bold;
+ }
+
+ .update {
+ font-weight: bold;
+ border-radius: 3px;
+ }
+ }
+
+ &.modified {
+ background-color: @alertOrange;
+
+ .document-footer-actions {
+
+ .cancel {
+ color: @alertOrangeBorder;
+ }
+
+ .update {
+ color: @alertOrangeBorder;
+ border-color: @alertOrangeBorder;
+
+ &:hover {
+ background-color: rgba(255,255,255,0.7);
+ }
+ }
+ }
+ }
+ &.in-progress.in-progress {
+ background-color: @chart1;
+ }
+ &.success {
+ background-color: @green2;
+ }
+ &.error {
+ background-color: red;
+
+ .document-footer-actions {
+
+ .cancel {
+ color: @alertRedBg;
+ }
+ }
+ }
+ &.viewing {
+ background-color: #F8F8F8;
+
+ .document-footer-actions {
+
+ .cancel {
+ color: #B8B8B8;
+ }
+ }
+ }
+}
+
+ol.document-property-body:hover {
+ .document-actions {
+ button {
+ visibility: visible;
+ }
+ }
+}
+
+.document-elements {
+ width: 75%;
+ counter-reset: line;
+ position: relative;
+
+ .document-property.array {
+ margin-left: 0px !important;
+ }
+
+ .document-property.object {
+ margin-left: 0px !important;
+ }
+
+ .line-number {
+ display: inline-block;
+ color: @gray4;
+ width: 18px;
+ height: 17px;
+ text-align: center;
+ position: absolute;
+ left: -18px;
+
+ &::before {
+ counter-increment: line;
+ content: counter(line);
+ }
+ }
+
+ .types {
+ display: inline-block;
+ float: right;
+ width: 100px;
+ color: #999999;
+ height: 17px;
+
+ .type-label {
+ padding: 1px 12px;
+ }
+
+ .btn {
+ background-color: transparent;
+ color: @gray4;
+ border: none;
+ text-transform: none;
+ font-size: 11px;
+ border-radius: 8px;
+ padding: 1px 12px;
+ }
+
+ .caret {
+ visibility: hidden;
+ // margin-right: 0px;
+ // margin-left: 4px;
+ }
+
+ .dropdown-menu {
+ li {
+ span {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ font-size: 11px;
+ color: #333333;
+ white-space: nowrap;
+ }
+ span:hover {
+ color: #313030;
+ text-decoration: none;
+ background: #e6e6e6;
+ }
+ }
+ }
+ }
+
+ li.document-property.non-expandable:hover {
+ background-color: @gray8;
+
+ input {
+ background-color: @gray8;
+
+ &:focus {
+ background-color: @pw;
+ }
+ }
+
+ .actions {
+ visibility: visible;
+ }
+
+ .types {
+ .btn {
+ background-color: @gray6;
+ color: @gray0;
+ }
+ .caret {
+ visibility: visible;
+ }
+ }
+ }
+
+ li.document-property {
+ // position: relative;
+
+ .actions {
+ display: inline-block;
+ visibility: hidden;
+ width: 18px;
+ margin-right: 8px;
+ text-align: center;
+ cursor: pointer;
+ color: #999999;
+ }
+
+ &.edited {
+ background-color: @alertOrangeBg;
+
+ input {
+ background-color: @alertOrangeBg;
+
+ &:focus {
+ background-color: @pw;
+ }
+ }
+
+ .line-number {
+ background-color: @alertOrange;
+ color: @pw;
+ }
+ }
+
+ &.added {
+ background-color: @greenBg;
+
+ input {
+ background-color: @greenBg;
+
+ &:focus {
+ background-color: @pw;
+ }
+ }
+
+ .line-number {
+ background-color: @green2;
+ color: @pw;
+ }
+ }
+
+ &.removed {
+ background-color: @alertRedBg;
+
+ input {
+ background-color: @alertRedBg;
+
+ &:focus {
+ background-color: @pw;
+ }
+ }
+
+ .line-number {
+ background-color: @alertRed;
+ color: @pw;
+ }
+ }
+ }
+
+ div.document-property-header.expandable:hover {
+ background-color: #ebebed !important;
+
+ input {
+ background-color: #ebebed !important;
+ }
+
+ .line-number {
+ background-color: #ebebed !important;
+ }
+
+ .actions {
+ visibility: visible;
+ }
+
+ .types {
+ .btn {
+ border-color: #8c8c8c;
+ }
+ .caret {
+ visibility: visible;
+ }
+ }
+ }
+
+ .hotspot {
+ height: 10px;
+ cursor: text;
+ }
+}
+
+.document-actions {
+ position: absolute;
+ top: 0;
+ right: 10px;
+
+ button {
+ visibility: hidden;
+ }
+}
+
+input.editable-key {
+ position: relative;
+ font-weight: bold;
+ border: none;
+ padding-left: 1px;
+ z-index: 1;
+
+ &.editing {
+ border: 1px solid #999999;
+ box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.2);
+ z-index: 10;
+ margin-top: -1px;
+ margin-bottom: -1px;
+
+ &.duplicate {
+ border: 1px solid red;
+ }
+ }
+
+ &.duplicate {
+ color: red;
+ }
+}
+
+input.editable-value {
+ position: relative;
+ border: none;
+ padding-left: 1px;
+ z-index: 1;
+
+ &.editing {
+ border: 1px solid #999999;
+ box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.2);
+ z-index: 10;
+ margin-top: -1px;
+ margin-bottom: -1px;
+ }
+}
+
+.document-property.string > .editable-value {
+ color: steelblue;
+}
+
+.document-property.number > .editable-value {
+ color: green;
+}
+
+.document-property.boolean > .editable-value {
+ color: purple;
+}
+
+.document-property.date > .editable-value {
+ color: firebrick;
+}
+
+.modal-body {
+ .document-elements {
+ width: 100%;
+
+ li.document-property {
+
+ .actions {
+ display: inline-block;
+ visibility: hidden;
+ width: 18px;
+ margin-right: 8px;
+ text-align: center;
+ cursor: pointer;
+ color: #999999;
+ }
+
+ &.added {
+ background-color: #FFFFFF;
+
+ input {
+ background-color: #FFFFFF;
+
+ &:focus {
+ background-color: @pw;
+ }
+ }
+
+ .line-number {
+ background-color: #FFFFFF;
+ color: #999999;
+ }
+ }
+ }
+ }
+}
+
+.modal-footer {
+ .btn.insert {
+ border-color: green;
+ background-color: @green2;
+ color: #FFFFFF;
+ }
+}