Initial implementation of Edit mode (#2181)

* Adding edit mode API and buttons

* Select navigated object by default. Enable table configuration in edit mode.

* Fixed issue with Table configuration

* Removed debugging code

* Added basic documentation to Editor API

* use selectable, table is selection[0], inspector cleans up

Update browse to set selection using openmct.selection.selectable
on navation so that selection contexts can be determined correctly.

Update table configuration to reference the first item in selection
instead of the last item in selection when displaying.

Update inspector code to remove display node from document on destroy.

* properly remove capturing handler

* inspector views respond to editing

InspectorViews respond to editing instead of openmct rerendering
the inspector on edit.
This commit is contained in:
Andrew Henry 2018-10-04 12:35:03 -07:00 committed by Pete Richards
parent 56b9708ab7
commit 3c324cbea0
11 changed files with 210 additions and 93 deletions

View File

@ -223,6 +223,8 @@ define([
this.Dialog = api.Dialog; this.Dialog = api.Dialog;
this.editor = new api.EditorAPI.default(this);
this.legacyRegistry = defaultRegistry; this.legacyRegistry = defaultRegistry;
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable()); this.install(this.plugins.TelemetryTable());
@ -310,13 +312,17 @@ define([
this.$injector.get('objectService'); this.$injector.get('objectService');
var appLayout = new Vue({ var appLayout = new Vue({
mixins: [Layout.default], components: {
'Layout': Layout.default
},
provide: { provide: {
openmct: this openmct: this
} },
template: '<Layout ref="layout"></Layout>'
}); });
domElement.appendChild(appLayout.$mount().$el); domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout;
this.layout = appLayout.$refs.layout;
Browse(this); Browse(this);
this.router.start(); this.router.start();
this.emit('start'); this.emit('start');

83
src/api/Editor.js Normal file
View File

@ -0,0 +1,83 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class Editor extends EventEmitter {
constructor(openmct) {
super();
this.editing = false;
this.openmct = openmct;
}
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
edit() {
this.editing = true;
this.getTransactionService().startTransaction();
this.emit('isEditing', true);
}
/**
* @returns true if the application is in edit mode, false otherwise.
*/
isEditing() {
return this.editing;
}
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
save() {
return this.getTransactionService().commit().then((result)=>{
this.editing = false;
this.emit('isEditing', false);
return result
}).catch((error)=>{
throw error;
});
}
/**
* End the currently active transaction and discard unsaved changes.
*/
cancel() {
this.getTransactionService().cancel();
this.editing = false;
this.emit('isEditing', false);
}
/**
* @private
*/
getTransactionService() {
if (!this.transactionService) {
this.transactionService = this.openmct.$injector.get('transactionService');
}
return this.transactionService;
}
}

View File

@ -28,7 +28,9 @@ define([
'./ui/Dialog', './ui/Dialog',
'./ui/GestureAPI', './ui/GestureAPI',
'./telemetry/TelemetryAPI', './telemetry/TelemetryAPI',
'./indicators/IndicatorAPI' './indicators/IndicatorAPI',
'./Editor'
], function ( ], function (
TimeAPI, TimeAPI,
ObjectAPI, ObjectAPI,
@ -37,7 +39,8 @@ define([
Dialog, Dialog,
GestureAPI, GestureAPI,
TelemetryAPI, TelemetryAPI,
IndicatorAPI IndicatorAPI,
EditorAPI
) { ) {
return { return {
TimeAPI: TimeAPI, TimeAPI: TimeAPI,
@ -47,6 +50,7 @@ define([
TypeRegistry: TypeRegistry, TypeRegistry: TypeRegistry,
GestureAPI: GestureAPI, GestureAPI: GestureAPI,
TelemetryAPI: TelemetryAPI, TelemetryAPI: TelemetryAPI,
IndicatorAPI: IndicatorAPI IndicatorAPI: IndicatorAPI,
EditorAPI: EditorAPI
}; };
}); });

View File

@ -33,30 +33,6 @@ define([
) { ) {
function TableConfigurationViewProvider(openmct) { function TableConfigurationViewProvider(openmct) {
let instantiateService;
function isBeingEdited(object) {
let oldStyleObject = getOldStyleObject(object);
return oldStyleObject.hasCapability('editor') &&
oldStyleObject.getCapability('editor').inEditContext();
}
function getOldStyleObject(object) {
let oldFormatModel = objectUtils.toOldFormat(object);
let oldFormatId = objectUtils.makeKeyString(object.identifier);
return instantiate(oldFormatModel, oldFormatId);
}
function instantiate(model, id) {
if (!instantiateService) {
instantiateService = openmct.$injector.get('instantiate');
}
return instantiateService(model, id);
}
return { return {
key: 'table-configuration', key: 'table-configuration',
name: 'Telemetry Table Configuration', name: 'Telemetry Table Configuration',
@ -65,8 +41,7 @@ define([
return false; return false;
} }
let object = selection[0].context.item; let object = selection[0].context.item;
return object.type === 'table' && return object.type === 'table';
isBeingEdited(object);
}, },
view: function (selection) { view: function (selection) {
let component; let component;
@ -86,7 +61,7 @@ define([
el: element el: element
}); });
}, },
destroy: function (element) { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
} }

View File

@ -23,10 +23,10 @@
define([ define([
'lodash', 'lodash',
'EventEmitter', 'EventEmitter',
'./TelemetryTableColumn', './TelemetryTableColumn'
], function (_, EventEmitter, TelemetryTableColumn) { ], function (_, EventEmitter, TelemetryTableColumn) {
class TelemetryTableConfiguration extends EventEmitter{ class TelemetryTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) { constructor(domainObject, openmct) {
super(); super();
@ -37,6 +37,8 @@ define([
this.addColumnsForObject = this.addColumnsForObject.bind(this); this.addColumnsForObject = this.addColumnsForObject.bind(this);
this.removeColumnsForObject = this.removeColumnsForObject.bind(this); this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
this.objectMutated = this.objectMutated.bind(this); this.objectMutated = this.objectMutated.bind(this);
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated); this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated);
} }
@ -56,11 +58,11 @@ define([
* @param {*} object * @param {*} object
*/ */
objectMutated(object) { objectMutated(object) {
let oldConfiguration = this.domainObject.configuration;
//Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible. //Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible.
this.domainObject = JSON.parse(JSON.stringify(object)); this.domainObject = object;
if (!_.eq(object.configuration, oldConfiguration)){ if (!_.eq(object.configuration, this.oldConfiguration)) {
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.emit('change', object.configuration); this.emit('change', object.configuration);
} }
} }

View File

@ -1,14 +1,12 @@
<template> <template>
<div class="grid-properties"> <div class="c-properties" v-if="isEditing">
<!--form class="form" --> <div class="c-properties__header">Table Columns</div>
<ul class="l-inspector-part"> <ul class="c-properties__section">
<h2>Table Columns</h2> <li class="c-properties__row" v-for="(title, key) in headers">
<li class="grid-row" v-for="(title, key) in headers"> <div class="c-properties__label" title="Show or Hide Column"><label :for="key + 'ColumnControl'">{{title}}</label></div>
<div class="grid-cell label" title="Show or Hide Column"><label :for="key + 'ColumnControl'">{{title}}</label></div> <div class="c-properties__value"><input type="checkbox" :id="key + 'ColumnControl'" :checked="configuration.hiddenColumns[key] !== true" @change="toggleColumn(key)"></div>
<div class="grid-cell value"><input type="checkbox" :id="key + 'ColumnControl'" :checked="configuration.hiddenColumns[key] !== true" @change="toggleColumn(key)"></div>
</li> </li>
</ul> </ul>
<!--/form -->
</div> </div>
</template> </template>
@ -21,6 +19,7 @@ export default {
data() { data() {
return { return {
headers: {}, headers: {},
isEditing: this.openmct.editor.isEditing(),
configuration: this.tableConfiguration.getConfiguration() configuration: this.tableConfiguration.getConfiguration()
} }
}, },
@ -41,11 +40,14 @@ export default {
removeObject(objectIdentifier) { removeObject(objectIdentifier) {
this.tableConfiguration.removeColumnsForObject(objectIdentifier, true); this.tableConfiguration.removeColumnsForObject(objectIdentifier, true);
this.updateHeaders(this.tableConfiguration.getAllHeaders()); this.updateHeaders(this.tableConfiguration.getAllHeaders());
},
toggleEdit(isEditing) {
this.isEditing = isEditing;
} }
}, },
mounted() { mounted() {
this.unlisteners = []; this.unlisteners = [];
this.openmct.editor.on('isEditing', this.toggleEdit);
let compositionCollection = this.openmct.composition.get(this.tableConfiguration.domainObject); let compositionCollection = this.openmct.composition.get(this.tableConfiguration.domainObject);
compositionCollection.load() compositionCollection.load()
@ -62,6 +64,7 @@ export default {
}, },
destroyed() { destroyed() {
this.tableConfiguration.destroy(); this.tableConfiguration.destroy();
this.openmct.editor.off('isEditing', this.toggleEdit);
this.unlisteners.forEach((unlisten) => unlisten()); this.unlisteners.forEach((unlisten) => unlisten());
} }
} }

View File

@ -58,7 +58,7 @@ define(['EventEmitter'], function (EventEmitter) {
this.selected[0].element.classList.remove('s-selected'); this.selected[0].element.classList.remove('s-selected');
} }
if (this.selected[1]) { if (this.selected[1] && this.selected[1].element) {
this.selected[1].element.classList.remove('s-selected-parent'); this.selected[1].element.classList.remove('s-selected-parent');
} }
@ -66,7 +66,7 @@ define(['EventEmitter'], function (EventEmitter) {
selectable[0].element.classList.add('s-selected'); selectable[0].element.classList.add('s-selected');
} }
if (selectable[1]) { if (selectable[1] && selectable[1].element) {
selectable[1].element.classList.add('s-selected-parent'); selectable[1].element.classList.add('s-selected-parent');
} }
@ -132,7 +132,7 @@ define(['EventEmitter'], function (EventEmitter) {
} }
return function () { return function () {
element.removeEventListener('click', capture); element.removeEventListener('click', capture, true);
element.removeEventListener('click', selectCapture); element.removeEventListener('click', selectCapture);
if (unlisten) { if (unlisten) {

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="c-properties"></div> <div>
</div>
</template> </template>
<style> <style>
@ -10,22 +11,26 @@
inject: ['openmct'], inject: ['openmct'],
mounted() { mounted() {
this.openmct.selection.on('change', this.updateSelection); this.openmct.selection.on('change', this.updateSelection);
this.updateSelection(this.openmct.selection.get()); this.updateSelection();
}, },
destroyed() { destroyed() {
this.openmct.selection.off('change', this.updateSelection); this.openmct.selection.off('change', this.updateSelection);
}, },
methods: { methods: {
updateSelection(selection) { updateSelection() {
let selection = this.openmct.selection.get();
if (this.selectedView && this.selectedView.destroy) { if (this.selectedView && this.selectedView.destroy) {
this.selectedView.destroy(); this.selectedView.destroy();
} delete this.viewContainer;
this.$el.innerHTML = ''; this.$el.innerHTML = '';
}
this.selectedView = this.openmct.inspectorViews.get(selection); this.selectedView = this.openmct.inspectorViews.get(selection);
if (!this.selectedView) { if (!this.selectedView) {
return; return;
} }
this.selectedView.show(this.$el); this.viewContainer = document.createElement('div');
this.$el.append(this.viewContainer)
this.selectedView.show(this.viewContainer);
} }
} }
} }

View File

@ -41,7 +41,9 @@
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<div class="l-browse-bar__action c-button icon-eye-open" title="Preview"></div> <div class="l-browse-bar__action c-button icon-eye-open" title="Preview"></div>
<div class="l-browse-bar__action c-button icon-notebook" title="New Notebook entry"></div> <div class="l-browse-bar__action c-button icon-notebook" title="New Notebook entry"></div>
<div class="l-browse-bar__action c-button c-button--major icon-pencil" title="Edit"></div> <div class="l-browse-bar__action c-button c-button--major icon-pencil" title="Edit" v-if="!isEditing" @click="edit()"></div>
<div class="l-browse-bar__action c-button c-button--major icon-save" title="Save and Finish Editing" v-if="isEditing" @click="saveAndFinishEditing()"></div>
<div class="l-browse-bar__action c-button icon-x" title="Cancel Editing" v-if="isEditing" @click="cancelEditing()"></div>
</div> </div>
</div> </div>
</div> </div>
@ -66,13 +68,23 @@
this.openmct.router.updateParams({ this.openmct.router.updateParams({
view: this.viewKey view: this.viewKey
}); });
},
edit() {
this.openmct.editor.edit();
},
cancelEditing() {
this.openmct.editor.cancel();
},
saveAndFinishEditing() {
this.openmct.editor.save();
} }
}, },
data: function () { data: function () {
return { return {
showViewMenu: false, showViewMenu: false,
domainObject: {}, domainObject: {},
viewKey: undefined viewKey: undefined,
isEditing: this.openmct.editor.isEditing()
} }
}, },
computed: { computed: {
@ -106,6 +118,10 @@
this.showViewMenu = false; this.showViewMenu = false;
} }
}); });
this.openmct.editor.on('isEditing', (isEditing) => {
this.isEditing = isEditing;
});
} }
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="l-shell"> <div class="l-shell" :class="{'is-editing': isEditing}">
<div class="l-shell__head"> <div class="l-shell__head">
<CreateButton class="l-shell__create-button"></CreateButton> <CreateButton class="l-shell__create-button"></CreateButton>
<div class="l-shell__controls"> <div class="l-shell__controls">
@ -242,6 +242,10 @@
} }
export default { export default {
inject: ['openmct'],
data() {
return {isEditing: false};
},
components: { components: {
Inspector, Inspector,
MctStatus, MctStatus,
@ -255,6 +259,11 @@
pane, pane,
BrowseBar BrowseBar
}, },
mounted() {
this.openmct.editor.on('isEditing', (isEditing)=>{
this.isEditing = isEditing;
});
},
data: function () { data: function () {
return { return {
fullScreen: false, fullScreen: false,

View File

@ -7,12 +7,24 @@ define([
return function install(openmct) { return function install(openmct) {
let navigateCall = 0; let navigateCall = 0;
let browseObject; let browseObject;
let removeSelectable = undefined;
function viewObject(object, viewProvider) { function viewObject(object, viewProvider) {
if (removeSelectable) {
removeSelectable();
removeSelectable = undefined;
}
openmct.layout.$refs.browseObject.show(object, viewProvider.key); openmct.layout.$refs.browseObject.show(object, viewProvider.key);
openmct.layout.$refs.browseBar.domainObject = object; openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key; openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
removeSelectable = openmct.selection.selectable(
openmct.layout.$refs.browseObject.$el,
{
item: object
},
true
);
}; };
function navigateToPath(path, currentViewKey) { function navigateToPath(path, currentViewKey) {
@ -22,16 +34,18 @@ define([
if (!Array.isArray(path)) { if (!Array.isArray(path)) {
path = path.split('/'); path = path.split('/');
} }
let keyString = path[path.length - 1]; return Promise.all(path.map((keyString)=>{
// TODO: retain complete path in navigation. return openmct.objects.get(keyString);
return openmct.objects.get(keyString) })).then((objects)=>{
.then((object) => {
if (currentNavigation !== navigateCall) { if (currentNavigation !== navigateCall) {
return; // Prevent race. return; // Prevent race.
} }
openmct.layout.$refs.browseBar.domainObject = object;
browseObject = object; let navigatedObject = objects[objects.length - 1];
if (!object) {
openmct.layout.$refs.browseBar.domainObject = navigatedObject;
browseObject = navigatedObject;
if (!navigatedObject) {
openmct.layout.$refs.browseObject.clear(); openmct.layout.$refs.browseObject.clear();
return; return;
} }
@ -39,12 +53,12 @@ define([
.objectViews .objectViews
.getByProviderKey(currentViewKey) .getByProviderKey(currentViewKey)
if (currentProvider && currentProvider.canView(object)) { if (currentProvider && currentProvider.canView(navigatedObject)) {
viewObject(object, currentProvider); viewObject(navigatedObject, currentProvider);
return; return;
} }
let defaultProvider = openmct.objectViews.get(object)[0]; let defaultProvider = openmct.objectViews.get(navigatedObject)[0];
if (defaultProvider) { if (defaultProvider) {
openmct.router.updateParams({ openmct.router.updateParams({
view: defaultProvider.key view: defaultProvider.key