diff --git a/.gitignore b/.gitignore index 5b09eeacb6b..294e3dd4443 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ report.json .compiled-sources/ src/app/compiled-less/ expansions.yml +.nvmrc diff --git a/package.json b/package.json index d11620ed93c..96be0a1e83e 100644 --- a/package.json +++ b/package.json @@ -102,9 +102,9 @@ "get-object-path": "azer/get-object-path#74eb42de0cfd02c14ffdd18552f295aba723d394", "hadron-action": "^0.0.4", "hadron-auto-update-manager": "^0.0.12", - "hadron-compile-cache": "^0.1.0", + "hadron-compile-cache": "^0.2.0", "hadron-component-registry": "^0.4.0", - "hadron-document": "^0.13.0", + "hadron-document": "^0.14.0", "hadron-ipc": "^0.0.7", "hadron-module-cache": "^0.0.3", "hadron-package-manager": "0.1.0", diff --git a/src/help/entries/cloning-a-single-document.md b/src/help/entries/cloning-a-single-document.md new file mode 100644 index 00000000000..3bb05a04248 --- /dev/null +++ b/src/help/entries/cloning-a-single-document.md @@ -0,0 +1,14 @@ +--- +title: Cloning a Single Document +tags: + - crud +related: + - inserting-a-single-document +section: CRUD +--- + +A single document can be cloned by clicking on the clone icon + on the right +side of the document in the document list. The Insert Document +modal will appear with all elements in the document cloned with +the exception of the `_id` element. diff --git a/src/help/entries/deleting-a-single-document.md b/src/help/entries/deleting-a-single-document.md new file mode 100644 index 00000000000..d29fab85f14 --- /dev/null +++ b/src/help/entries/deleting-a-single-document.md @@ -0,0 +1,12 @@ +--- +title: Deleting a Single Document +tags: + - crud +section: CRUD +--- + +A single document can be deleted by clicking on the delete icon + on the right +side of the document in the document list. Clicking on this button puts +the document into a pending delete mode, but the delete command will +not be sent to the server until the user confirms the edits by clicking 'Delete'. diff --git a/src/help/entries/editing-a-single-document.md b/src/help/entries/editing-a-single-document.md new file mode 100644 index 00000000000..a5052a36c44 --- /dev/null +++ b/src/help/entries/editing-a-single-document.md @@ -0,0 +1,53 @@ +--- +title: Editing a Single Document +tags: + - crud +section: CRUD +--- + +A single document can be edited by clicking on the edit icon + on the right +side of the document in the document list. Clicking on this button puts +the document into edit mode, but changes will not be sent to the server +until the user confirms the edits by clicking 'Update'. + +In edit mode, the document panel behaves similar to the CSS editor in +most modern web browers' development tools. + +### Editing an Element + +Clicking on an element key or value allows the user to change the key name +or the value of the element. The element's type can also be changed by +selecting a new type from the dropdown on the right side. Only types that +the value can currently be cast to will be visible in the list. Duplicate +key names will cause the key field to be highlighted in red. + +### Adding an Element + +A new element can be added to the document or any embedded document by +either clicking on the right side of the element or tabbing off the last +element's value field if the element is the last element in the document +or sub document. Clicking to the right of an element will also remove any +subsequent extra empty elements. + +### Deleting an Element + +An element can be deleted by clicking on the +icon to the left of the element's line number. + +### Reverting a Change + +A change to an element can be reverted by clicking the revert icon + +to the left of the element's line number. + +### Persisting Changes + +The changes to a document may be persisted by clicking on the 'Update' +button in the footer of the document panel. Clicking this button will +execute a `$findAndModify` on the server and update the document in the +list. + +### Canceling Changes + +To exit edit mode and cancel all pending changes, click the 'Cancel' button. diff --git a/src/help/entries/inserting-a-single-document.md b/src/help/entries/inserting-a-single-document.md new file mode 100644 index 00000000000..08a3d8901da --- /dev/null +++ b/src/help/entries/inserting-a-single-document.md @@ -0,0 +1,13 @@ +--- +title: Inserting a Single Document +tags: + - crud +related: + - editing-a-single-document +section: CRUD +--- + +A single document can be inserted by clicking on the 'Insert' button at +the top of the document list. An insert modal will open and the user +may edit the new document using the same behaviour provided by document +editing. diff --git a/src/internal-packages/crud/lib/component/document-actions.jsx b/src/internal-packages/crud/lib/component/document-actions.jsx index 59d692395db..c2887717850 100644 --- a/src/internal-packages/crud/lib/component/document-actions.jsx +++ b/src/internal-packages/crud/lib/component/document-actions.jsx @@ -1,14 +1,8 @@ 'use strict'; const React = require('react'); -const app = require('ampersand-app'); const IconButton = require('./icon-button'); -/** - * The feature flag. - */ -const FEATURE = 'singleDocumentCrud'; - /** * Component for actions on the document. */ @@ -29,26 +23,21 @@ class DocumentActions extends React.Component { * @returns {Component} The actions component. */ render() { - if (app.isFeatureEnabled(FEATURE)) { - return ( -
- - - -
- ); - } return ( -
+
+ + + +
); } } diff --git a/src/internal-packages/crud/lib/component/document.jsx b/src/internal-packages/crud/lib/component/document.jsx index 5675839f714..b572044f624 100644 --- a/src/internal-packages/crud/lib/component/document.jsx +++ b/src/internal-packages/crud/lib/component/document.jsx @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('lodash'); const app = require('ampersand-app'); const React = require('react'); const Reflux = require('reflux'); @@ -270,14 +269,15 @@ class Document extends React.Component { * @returns {Array} The editable elements. */ editableElements() { - var components = _.map(this.state.doc.elements, (element) => { - return ( - - ); - }); + var components = []; + for (let element of this.state.doc.elements) { + components.push() + } + // Add the hotspot to the end. In the case of insert, we need to guard against + // No elements being present. var lastComponent = components[components.length - 1]; - var lastElement = lastComponent ? lastComponent.props.element : null; - components.push(); + var lastElement = lastComponent ? lastComponent.props.element : this.state.doc; + components.push(); return components; } diff --git a/src/internal-packages/crud/lib/component/editable-element.jsx b/src/internal-packages/crud/lib/component/editable-element.jsx index fe5b0602319..82a11cc31a5 100644 --- a/src/internal-packages/crud/lib/component/editable-element.jsx +++ b/src/internal-packages/crud/lib/component/editable-element.jsx @@ -116,9 +116,10 @@ class EditableElement extends React.Component {
  • {this.renderAction()} - + : {this.renderValue()} +
  • ); @@ -149,7 +150,7 @@ class EditableElement extends React.Component {
    {this.renderAction()}
    - + :
    {this.element.currentType} @@ -168,12 +169,12 @@ class EditableElement extends React.Component { * @returns {Array} The components. */ elementComponents() { - var components = _.map(this.element.elements, (element) => { - return (); - }); - // var lastComponent = components[components.length - 1]; - // var lastElement = lastComponent ? lastComponent.props.element : null; - // components.push(); + var components = []; + var index = 0; + for (let element of this.element.elements) { + components.push(); + index++; + } return components; } diff --git a/src/internal-packages/crud/lib/component/editable-key.jsx b/src/internal-packages/crud/lib/component/editable-key.jsx index 8aa798098d7..f5a8290ac63 100644 --- a/src/internal-packages/crud/lib/component/editable-key.jsx +++ b/src/internal-packages/crud/lib/component/editable-key.jsx @@ -1,6 +1,7 @@ 'use strict'; const React = require('react'); +const inputSize = require('./utils').inputSize; /** * The editing class constant. @@ -17,6 +18,16 @@ const DUPLICATE = 'duplicate'; */ const KEY_CLASS = 'editable-key'; +/** + * Escape key code. + */ +const ESC = 27; + +/** + * Colon key code. + */ +const COLON = 186; + /** * General editable key component. */ @@ -38,15 +49,8 @@ class EditableKey extends React.Component { * to the value field. */ componentDidMount() { - if (this.element.isAdded()) { - if (this.props.insertIndex) { - // Focus for inserting new documents. - if (this.props.insertIndex === 1 && this.element.currentKey === '') { - this._node.focus(); - } - } else if (!this.isEditable() && this._node) { - this._node.focus(); - } + if (this.isAutoFocusable()) { + this._node.focus(); } } @@ -61,16 +65,25 @@ class EditableKey extends React.Component { type='text' className={this.style()} ref={(c) => this._node = c} - size={this.element.currentKey.length} + size={inputSize(this.renderValue())} + tabIndex={this.isEditable() ? 0 : -1} onBlur={this.handleBlur.bind(this)} onFocus={this.handleFocus.bind(this)} onChange={this.handleChange.bind(this)} onKeyDown={this.handleKeyDown.bind(this)} - value={this.element.currentKey} + onKeyUp={this.handleKeyUp.bind(this)} + value={this.renderValue()} title={this.renderTitle()} /> ); } + /** + * Render the value of the key. + */ + renderValue() { + return this.element.parent.currentType === 'Array' ? this.props.index : this.element.currentKey; + } + /** * Render the title. * @@ -88,8 +101,27 @@ class EditableKey extends React.Component { * @param {Event} evt - The event. */ handleKeyDown(evt) { - if (evt.keyCode === 27) { - this._node.blur(); + var value = evt.target.value; + if (evt.keyCode === ESC) { + if (value.length === 0) { + this.element.remove(); + } else { + this._node.blur(); + } + } + } + + /** + * If they key is a colon, tab to the next input. + */ + handleKeyUp(evt) { + if (evt.keyCode === COLON) { + var value = evt.target.value; + if (value !== ':') { + this.element.rename(value.replace(':', '')); + evt.target.value = ''; + this._node.nextSibling.nextSibling.focus(); + } } } @@ -100,7 +132,7 @@ class EditableKey extends React.Component { */ handleChange(evt) { var value = evt.target.value; - this._node.size = value.length; + this._node.size = inputSize(value); if (this.isEditable()) { if (this.element.isDuplicateKey(value)) { this.setState({ duplicate: true }); @@ -130,12 +162,30 @@ class EditableKey extends React.Component { } /** - * Is this component editable? + * Is this component auto focusable? + * + * This is true if: + * - When a new element has been added and is a normal element. + * - When not being tabbed into. + * + * Is false if: + * - When a new array value has been added. + * - When the key is _id * * @returns {Boolean} If the component is editable. */ + isAutoFocusable() { + return this.element.isAdded() && this.isEditable(); + } + + /** + * Is the key able to be edited? + * + * @returns {Boolean} If the key can be edited. + */ isEditable() { - return this.element.isKeyEditable() && this.element.parentElement.currentType !== 'Array'; + return this.element.isKeyEditable() && + this.element.parent.currentType !== 'Array'; } /** diff --git a/src/internal-packages/crud/lib/component/editable-value.jsx b/src/internal-packages/crud/lib/component/editable-value.jsx index ff29c650a1b..d177412d391 100644 --- a/src/internal-packages/crud/lib/component/editable-value.jsx +++ b/src/internal-packages/crud/lib/component/editable-value.jsx @@ -2,9 +2,15 @@ const _ = require('lodash'); const React = require('react'); +const inputSize = require('./utils').inputSize; const ElementFactory = require('hadron-component-registry').ElementFactory; const TypeChecker = require('hadron-type-checker'); +/** + * Escape key code. + */ +const ESC = 27; + /** * The editing class constant. */ @@ -31,6 +37,21 @@ class EditableValue extends React.Component { this.state = { 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.isAutoFocusable()) { + this._node.focus(); + } + } + + isAutoFocusable() { + return !this.element.isKeyEditable() || + this.element.parent.currentType === 'Array'; + } + /** * Render a single editable value. * @@ -41,7 +62,7 @@ class EditableValue extends React.Component { this._node = c} type='text' - size={this.element.currentValue ? (this.element.currentValue.length + 1) : 5} + size={inputSize(this.element.currentValue)} className={this.style()} onBlur={this.handleBlur.bind(this)} onFocus={this.handleFocus.bind(this)} @@ -58,10 +79,32 @@ class EditableValue extends React.Component { */ handleKeyDown(evt) { if (evt.keyCode === 9 && !evt.shiftKey) { - this.element.next(); - } else if (evt.keyCode === 27) { - this._node.blur(); + if (this.isTabable()) { + if (!this.element.nextElement) { + this.element.next(); + evt.preventDefault(); + evt.stopPropagation(); + } + } else { + // We don't want to create another element when the current one is blank. + evt.preventDefault(); + evt.stopPropagation(); + } + } else if (evt.keyCode === ESC) { + var value = evt.target.value; + if (value.length === 0 && this.element.currentKey.length === 0) { + this.element.remove(); + } else { + this._node.blur(); + } + } + } + + isTabable() { + if (this.element.parent.currentType === 'Array') { + return this.element.currentValue !== ''; } + return this.element.currentKey.length !== 0; } /** @@ -71,7 +114,7 @@ class EditableValue extends React.Component { */ handleChange(evt) { var value = evt.target.value; - this._node.size = value.length; + this._node.size = inputSize(value); var currentType = this.element.currentType; if (_.includes(TypeChecker.castableTypes(value), currentType)) { this.element.edit(TypeChecker.cast(value, currentType)); diff --git a/src/internal-packages/crud/lib/component/hotspot.jsx b/src/internal-packages/crud/lib/component/hotspot.jsx index 592b15a5e8e..6d434b2efbb 100644 --- a/src/internal-packages/crud/lib/component/hotspot.jsx +++ b/src/internal-packages/crud/lib/component/hotspot.jsx @@ -14,19 +14,50 @@ class Hotspot extends React.Component { */ constructor(props) { super(props); - this.doc = props.doc; this.element = props.element; } /** - * When clicking on a hotspot we append to the parent. + * When clicking on a hotspot we append or remove on the parent. */ handleClick() { - if (this.element && this.element.parentElement) { - this.element.next(); - } else { - this.doc.add('', ''); + this.element.next(); + } + + /** + * Get the last element for the base element. + * + * @param {Element} baseElement - The base element. + * + * @returns {Element} The last element. + */ + lastElement(baseElement) { + return baseElement.elements[baseElement.elements.length - 1]; + } + + /** + * Is the element removable? + * + * @param {Element} element - The element. + * + * @returns {Boolean} if the element must be removed. + */ + isRemovable(element) { + if (element.parentElement && element.parentElement.type === 'Array') { + return element.currentValue === ''; } + return element.currentKey === '' && element.currentValue === ''; + } + + /** + * Is the element actionable. + * + * @param {Element} element - If the element is actionable. + * + * @returns {Boolean} If the element is actionable. + */ + isActionable(element) { + return element.currentKey !== '' || element.currentValue !== ''; } /** diff --git a/src/internal-packages/crud/lib/component/insert-document.jsx b/src/internal-packages/crud/lib/component/insert-document.jsx index 9b1201edd0b..13375e77938 100644 --- a/src/internal-packages/crud/lib/component/insert-document.jsx +++ b/src/internal-packages/crud/lib/component/insert-document.jsx @@ -68,15 +68,16 @@ class InsertDocument extends React.Component { * @returns {Array} The editable elements. */ renderElements() { - var elements = _.map(this.doc.elements, (element, index) => { - return ( - - ); - }); - var lastComponent = elements[elements.length - 1]; - var lastElement = lastComponent ? lastComponent.props.element : null; - elements.push(); - return elements; + var components = []; + for (let element of this.doc.elements) { + components.push() + } + // Add the hotspot to the end. In the case of insert, we need to guard against + // No elements being present. + var lastComponent = components[components.length - 1]; + var lastElement = lastComponent ? lastComponent.props.element : this.state.doc; + components.push(); + return components; } } diff --git a/src/internal-packages/crud/lib/component/sampling-message.jsx b/src/internal-packages/crud/lib/component/sampling-message.jsx index 6a0e78138a7..c367a3a064d 100644 --- a/src/internal-packages/crud/lib/component/sampling-message.jsx +++ b/src/internal-packages/crud/lib/component/sampling-message.jsx @@ -1,15 +1,9 @@ 'use strict'; const React = require('react'); -const app = require('ampersand-app'); const ResetDocumentListStore = require('../store/reset-document-list-store'); const TextButton = require('./text-button'); -/** - * The feature flag. - */ -const FEATURE = 'singleDocumentCrud'; - /** * Component for the sampling message. */ @@ -59,23 +53,12 @@ class SamplingMessage extends React.Component {
    Query returned {this.state.count} documents. - {this.renderInsertButton()} -
    - ); - } - - /** - * Render the insert button. - */ - renderInsertButton() { - if (app.isFeatureEnabled(FEATURE)) { - return ( - ); - } +
    + ); } /** diff --git a/src/internal-packages/crud/lib/component/utils.js b/src/internal-packages/crud/lib/component/utils.js new file mode 100644 index 00000000000..f78ec2153cf --- /dev/null +++ b/src/internal-packages/crud/lib/component/utils.js @@ -0,0 +1,15 @@ +'use strict'; + +/** + * Get the size value for an input field when editing. + * + * @param {Object} value - The value. + * + * @returns {Integer} The size. + */ +function inputSize(value) { + var length = String(value).length; + return length === 0 ? 1 : length; +} + +module.exports.inputSize = inputSize; 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 index 83b352533cb..d44e82d98c1 100644 --- 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 @@ -25,10 +25,11 @@ const OpenInsertDocumentDialogStore = Reflux.createStore({ openInsertDocumentDialog: function(doc, clone) { var hadronDoc = new HadronDocument(doc, true); if (clone) { + var firstElement = hadronDoc.elements.firstElement; // 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(); + if (firstElement.currentKey === '_id') { + hadronDoc.elements.remove(firstElement); } } this.trigger(hadronDoc); diff --git a/src/internal-packages/crud/styles/crud.less b/src/internal-packages/crud/styles/crud.less index 24c0098354c..04ca994e748 100644 --- a/src/internal-packages/crud/styles/crud.less +++ b/src/internal-packages/crud/styles/crud.less @@ -144,10 +144,11 @@ ol.document-property-body:hover { .types { display: inline-block; - float: right; + position: absolute; width: 100px; color: #999999; height: 17px; + right: 0px; .type-label { padding: 1px 12px; @@ -218,6 +219,13 @@ ol.document-property-body:hover { li.document-property { // position: relative; + .hotspot { + height: 10px; + cursor: text; + display: inline-block; + width: 30px; + } + .actions { display: inline-block; visibility: hidden;