diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5fe33cf1ba..7ba460118a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -40,6 +40,7 @@ assignees: '' - [ ] Is there a workaround available? - [ ] Does this impact a critical component? - [ ] Is this just a visual bug with no functional impact? +- [ ] Does this block the execution of e2e tests? #### Additional Information diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 89fb12a9e3..8cbde6e73d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,7 +16,7 @@ Closes + + + + diff --git a/src/plugins/charts/scatter/ScatterPlotViewProvider.js b/src/plugins/charts/scatter/ScatterPlotViewProvider.js new file mode 100644 index 0000000000..338d2eb3e3 --- /dev/null +++ b/src/plugins/charts/scatter/ScatterPlotViewProvider.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2021, 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 ScatterPlotView from './ScatterPlotView.vue'; +import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js'; +import Vue from 'vue'; + +export default function ScatterPlotViewProvider(openmct) { + function isCompactView(objectPath) { + let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY); + + return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); + } + + return { + key: SCATTER_PLOT_VIEW, + name: 'Scatter Plot', + cssClass: 'icon-telemetry', + canView(domainObject, objectPath) { + return domainObject && domainObject.type === SCATTER_PLOT_KEY; + }, + + canEdit(domainObject, objectPath) { + return domainObject && domainObject.type === SCATTER_PLOT_KEY; + }, + + view: function (domainObject, objectPath) { + let component; + + return { + show: function (element) { + let isCompact = isCompactView(objectPath); + component = new Vue({ + el: element, + components: { + ScatterPlotView + }, + provide: { + openmct, + domainObject, + path: objectPath + }, + data() { + return { + options: { + compact: isCompact + } + }; + }, + template: '' + }); + }, + destroy: function () { + component.$destroy(); + component = undefined; + } + }; + } + }; +} diff --git a/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue new file mode 100644 index 0000000000..796a252ac7 --- /dev/null +++ b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue @@ -0,0 +1,393 @@ + + + diff --git a/src/plugins/charts/scatter/inspector/PlotOptions.vue b/src/plugins/charts/scatter/inspector/PlotOptions.vue new file mode 100644 index 0000000000..a72fcb8c9a --- /dev/null +++ b/src/plugins/charts/scatter/inspector/PlotOptions.vue @@ -0,0 +1,64 @@ + + + + diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue new file mode 100644 index 0000000000..c7af21973c --- /dev/null +++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue @@ -0,0 +1,153 @@ + + + + diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue new file mode 100644 index 0000000000..6781a27777 --- /dev/null +++ b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue @@ -0,0 +1,262 @@ + + + diff --git a/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js new file mode 100644 index 0000000000..54487dfe37 --- /dev/null +++ b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js @@ -0,0 +1,48 @@ +import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants'; +import Vue from 'vue'; +import PlotOptions from "./PlotOptions.vue"; + +export default function ScatterPlotInspectorViewProvider(openmct) { + return { + key: SCATTER_PLOT_INSPECTOR_KEY, + name: 'Bar Graph Inspector View', + canView: function (selection) { + if (selection.length === 0 || selection[0].length === 0) { + return false; + } + + let object = selection[0][0].context.item; + + return object + && object.type === SCATTER_PLOT_KEY; + }, + view: function (selection) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + PlotOptions + }, + provide: { + openmct, + domainObject: selection[0][0].context.item + }, + template: '' + }); + }, + destroy: function () { + if (component) { + component.$destroy(); + component = undefined; + } + } + }; + }, + priority: function () { + return 1; + } + }; +} diff --git a/src/plugins/charts/scatter/plugin.js b/src/plugins/charts/scatter/plugin.js new file mode 100644 index 0000000000..600c2970fd --- /dev/null +++ b/src/plugins/charts/scatter/plugin.js @@ -0,0 +1,127 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants.js'; +import ScatterPlotViewProvider from './ScatterPlotViewProvider'; +import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider'; +import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy'; +import Vue from "vue"; +import ScatterPlotForm from "./ScatterPlotForm.vue"; + +export default function () { + return function install(openmct) { + openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct)); + + openmct.types.addType(SCATTER_PLOT_KEY, { + key: SCATTER_PLOT_KEY, + name: "Scatter Plot", + cssClass: "icon-plot-scatter", + description: "View data as a scatter plot.", + creatable: true, + initialize: function (domainObject) { + domainObject.composition = []; + domainObject.configuration = { + styles: {}, + axes: {}, + ranges: {} + }; + }, + form: [ + { + name: 'Underlay data (JSON file)', + key: 'selectFile', + control: 'file-input', + text: 'Select File...', + type: 'application/json', + removable: true, + hideFromInspector: true, + property: [ + "selectFile" + ] + }, + { + name: "Underlay ranges", + control: "scatter-plot-form-control", + cssClass: "l-input", + key: "scatterPlotForm", + required: false, + hideFromInspector: false, + property: [ + "configuration", + "ranges" + ], + validate: ({ value }, callback) => { + const { rangeMin, rangeMax, domainMin, domainMax } = value; + const valid = { + rangeMin, + rangeMax, + domainMin, + domainMax + }; + + if (callback) { + callback(valid); + } + + const values = Object.values(valid); + const hasAllValues = values.every(rangeValue => rangeValue !== undefined); + const hasNoValues = values.every(rangeValue => rangeValue === undefined); + + return hasAllValues || hasNoValues; + } + } + ], + priority: 891 + }); + + openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct)); + + openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct)); + + openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow); + }; + + function getScatterPlotFormControl(openmct) { + return { + show(element, model, onChange) { + const rowComponent = new Vue({ + el: element, + components: { + ScatterPlotForm + }, + provide: { + openmct + }, + data() { + return { + model, + onChange + }; + }, + template: `` + }); + + return rowComponent; + } + }; + } +} + diff --git a/src/plugins/charts/scatter/pluginSpec.js b/src/plugins/charts/scatter/pluginSpec.js new file mode 100644 index 0000000000..2eb17c7a45 --- /dev/null +++ b/src/plugins/charts/scatter/pluginSpec.js @@ -0,0 +1,421 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 {createOpenMct, resetApplicationState} from "utils/testing"; +import Vue from "vue"; +import ScatterPlotPlugin from "./plugin"; +import ScatterPlot from './ScatterPlotView.vue'; +import EventEmitter from "EventEmitter"; +import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants'; + +describe("the plugin", function () { + let element; + let child; + let openmct; + let telemetryPromise; + let telemetryPromiseResolve; + let mockObjectPath; + + beforeEach((done) => { + mockObjectPath = [ + { + name: 'mock folder', + type: 'fake-folder', + identifier: { + key: 'mock-folder', + namespace: '' + } + } + ]; + const testTelemetry = [ + { + 'utc': 1, + 'some-key': 'some-value 1', + 'some-other-key': 'some-other-value 1' + }, + { + 'utc': 2, + 'some-key': 'some-value 2', + 'some-other-key': 'some-other-value 2' + }, + { + 'utc': 3, + 'some-key': 'some-value 3', + 'some-other-key': 'some-other-value 3' + } + ]; + + openmct = createOpenMct(); + + telemetryPromise = new Promise((resolve) => { + telemetryPromiseResolve = resolve; + }); + + spyOn(openmct.telemetry, 'request').and.callFake(() => { + telemetryPromiseResolve(testTelemetry); + + return telemetryPromise; + }); + + openmct.install(new ScatterPlotPlugin()); + + element = document.createElement("div"); + element.style.width = "640px"; + element.style.height = "480px"; + child = document.createElement("div"); + child.style.width = "640px"; + child.style.height = "480px"; + element.appendChild(child); + document.body.appendChild(element); + + spyOn(window, 'ResizeObserver').and.returnValue({ + observe() {}, + unobserve() {}, + disconnect() {} + }); + + openmct.time.timeSystem("utc", { + start: 0, + end: 4 + }); + + openmct.types.addType("test-object", { + creatable: true + }); + + openmct.on("start", done); + openmct.startHeadless(); + }); + + afterEach((done) => { + openmct.time.timeSystem('utc', { + start: 0, + end: 1 + }); + resetApplicationState(openmct).then(done).catch(done); + }); + + describe("The scatter plot view", () => { + let testDomainObject; + let scatterPlotObject; + // eslint-disable-next-line no-unused-vars + let component; + let mockComposition; + + beforeEach(async () => { + scatterPlotObject = { + identifier: { + namespace: "", + key: "test-plot" + }, + type: "telemetry.plot.scatter-plot", + name: "Test Scatter Plot", + configuration: { + axes: {}, + styles: {} + } + }; + + testDomainObject = { + identifier: { + namespace: "", + key: "test-object" + }, + type: "test-object", + name: "Test Object", + telemetry: { + values: [{ + key: "utc", + format: "utc", + name: "Time", + hints: { + domain: 1 + } + }, { + key: "some-key", + name: "Some attribute", + hints: { + range: 1 + } + }, { + key: "some-other-key", + name: "Another attribute", + hints: { + range: 2 + } + }] + } + }; + + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testDomainObject); + + return [testDomainObject]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + let viewContainer = document.createElement("div"); + child.append(viewContainer); + component = new Vue({ + el: viewContainer, + components: { + ScatterPlot + }, + provide: { + openmct: openmct, + domainObject: scatterPlotObject, + composition: openmct.composition.get(scatterPlotObject) + }, + template: "" + }); + + await Vue.nextTick(); + }); + + it("provides a scatter plot view", () => { + const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath); + const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW); + expect(plotViewProvider).toBeDefined(); + }); + + it("Renders plotly scatter plot", () => { + let scatterPlotElement = element.querySelectorAll(".plotly"); + expect(scatterPlotElement.length).toBe(1); + }); + }); + + describe("the scatter plot objects", () => { + const mockObject = { + name: 'A very nice scatter plot', + key: SCATTER_PLOT_KEY, + creatable: true + }; + + it('defines a scatter plot object type with the correct key', () => { + const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition; + expect(objectDef.key).toEqual(mockObject.key); + }); + + it('is creatable', () => { + const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition; + expect(objectDef.creatable).toEqual(mockObject.creatable); + }); + }); + + describe("The scatter plot composition policy", () => { + it("allows composition for telemetry that contain at least 2 ranges", () => { + const parent = { + "composition": [], + "configuration": { + axes: {}, + styles: {} + }, + "name": "Some Scatter Plot", + "type": "telemetry.plot.scatter-plot", + "location": "mine", + "modified": 1631005183584, + "persisted": 1631005183502, + "identifier": { + "namespace": "", + "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" + } + }; + const testTelemetryObject = { + identifier: { + namespace: "", + key: "test-object" + }, + type: "test-object", + name: "Test Object", + telemetry: { + values: [{ + key: "some-key", + name: "Some attribute", + hints: { + domain: 1 + } + }, { + key: "some-other-key", + name: "Another attribute", + hints: { + range: 1 + } + }, { + key: "some-other-key2", + name: "Another attribute2", + hints: { + range: 2 + } + }] + } + }; + const composition = openmct.composition.get(parent); + expect(() => { + composition.add(testTelemetryObject); + }).not.toThrow(); + expect(parent.composition.length).toBe(1); + }); + + it("disallows composition for telemetry that don't contain at least 2 range hints", () => { + const parent = { + "composition": [], + "configuration": { + axes: {}, + styles: {} + }, + "name": "Some Scatter Plot", + "type": "telemetry.plot.scatter-plot", + "location": "mine", + "modified": 1631005183584, + "persisted": 1631005183502, + "identifier": { + "namespace": "", + "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" + } + }; + const testTelemetryObject = { + identifier: { + namespace: "", + key: "test-object" + }, + type: "test-object", + name: "Test Object", + telemetry: { + values: [{ + key: "some-key", + name: "Some attribute", + hints: { + domain: 1 + } + }, { + key: "some-other-key", + name: "Another attribute", + hints: { + range: 1 + } + }] + } + }; + const composition = openmct.composition.get(parent); + expect(() => { + composition.add(testTelemetryObject); + }).toThrow(); + expect(parent.composition.length).toBe(0); + }); + }); + describe('the inspector view', () => { + let mockComposition; + let testDomainObject; + let selection; + let plotInspectorView; + let viewContainer; + let optionsElement; + beforeEach(async () => { + testDomainObject = { + identifier: { + namespace: "", + key: "test-object" + }, + type: "test-object", + name: "Test Object", + telemetry: { + values: [{ + key: "utc", + format: "utc", + name: "Time", + hints: { + domain: 1 + } + }, { + key: "some-key", + name: "Some attribute", + hints: { + range: 1 + } + }, { + key: "some-other-key", + name: "Another attribute", + hints: { + range: 2 + } + }] + } + }; + + selection = [ + [ + { + context: { + item: { + id: "test-object", + identifier: { + key: "test-object", + namespace: '' + }, + type: "telemetry.plot.scatter-plot", + configuration: { + axes: {}, + styles: { + } + }, + composition: [ + { + key: '~Some~foo.scatter' + } + ] + } + } + } + ] + ]; + + mockComposition = new EventEmitter(); + mockComposition.load = () => { + mockComposition.emit('add', testDomainObject); + + return [testDomainObject]; + }; + + spyOn(openmct.composition, 'get').and.returnValue(mockComposition); + + viewContainer = document.createElement('div'); + child.append(viewContainer); + + const applicableViews = openmct.inspectorViews.get(selection); + plotInspectorView = applicableViews[0]; + plotInspectorView.show(viewContainer); + + await Vue.nextTick(); + optionsElement = element.querySelector('.c-scatter-plot-options'); + }); + + afterEach(() => { + plotInspectorView.destroy(); + }); + + it('it renders the options', () => { + expect(optionsElement).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/charts/scatter/scatterPlotConstants.js b/src/plugins/charts/scatter/scatterPlotConstants.js new file mode 100644 index 0000000000..e458be37c6 --- /dev/null +++ b/src/plugins/charts/scatter/scatterPlotConstants.js @@ -0,0 +1,4 @@ +export const SCATTER_PLOT_VIEW = 'scatter-plot.view'; +export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot'; +export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector'; +export const TIME_STRIP_KEY = 'time-strip'; diff --git a/src/plugins/formActions/CreateActionSpec.js b/src/plugins/formActions/CreateActionSpec.js new file mode 100644 index 0000000000..2071da4710 --- /dev/null +++ b/src/plugins/formActions/CreateActionSpec.js @@ -0,0 +1,128 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 CreateAction from './CreateAction'; + +import { + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +import { debounce } from 'lodash'; + +let parentObject; +let parentObjectPath; +let unObserve; + +describe("The create action plugin", () => { + let openmct; + + const TYPES = [ + 'clock', + 'conditionWidget', + 'conditionWidget', + 'example.imagery', + 'example.state-generator', + 'flexible-layout', + 'folder', + 'generator', + 'hyperlink', + 'LadTable', + 'LadTableSet', + 'layout', + 'mmgis', + 'notebook', + 'plan', + 'table', + 'tabs', + 'telemetry-mean', + 'telemetry.plot.bar-graph', + 'telemetry.plot.overlay', + 'telemetry.plot.stacked', + 'time-strip', + 'timer', + 'webpage' + ]; + + beforeEach((done) => { + openmct = createOpenMct(); + + openmct.on('start', done); + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe('creates new objects for a', () => { + beforeEach(() => { + parentObject = { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + composition: [] + }; + parentObjectPath = [parentObject]; + + spyOn(openmct.objects, 'save'); + openmct.objects.save.and.callThrough(); + spyOn(openmct.forms, 'showForm'); + openmct.forms.showForm.and.callFake(formStructure => { + return Promise.resolve({ + name: 'test', + notes: 'test notes', + location: parentObjectPath + }); + }); + }); + + afterEach(() => { + parentObject = null; + unObserve(); + }); + + TYPES.forEach(type => { + it(`type ${type}`, (done) => { + function callback(newObject) { + const composition = newObject.composition; + + openmct.objects.get(composition[0]) + .then(object => { + expect(object.type).toEqual(type); + expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier)); + + done(); + }); + } + + const deBouncedCallback = debounce(callback, 300); + unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback); + + const createAction = new CreateAction(openmct, type, parentObject); + createAction.invoke(); + }); + }); + }); +}); diff --git a/src/plugins/formActions/EditPropertiesAction.js b/src/plugins/formActions/EditPropertiesAction.js index 4937b2ab3d..53afadfe90 100644 --- a/src/plugins/formActions/EditPropertiesAction.js +++ b/src/plugins/formActions/EditPropertiesAction.js @@ -45,7 +45,7 @@ export default class EditPropertiesAction extends PropertiesAction { } invoke(objectPath) { - this._showEditForm(objectPath); + return this._showEditForm(objectPath); } /** @@ -86,7 +86,7 @@ export default class EditPropertiesAction extends PropertiesAction { const formStructure = createWizard.getFormStructure(false); formStructure.title = 'Edit ' + this.domainObject.name; - this.openmct.forms.showForm(formStructure) + return this.openmct.forms.showForm(formStructure) .then(this._onSave.bind(this)); } } diff --git a/src/plugins/formActions/pluginSpec.js b/src/plugins/formActions/pluginSpec.js new file mode 100644 index 0000000000..9c4cbb2cc0 --- /dev/null +++ b/src/plugins/formActions/pluginSpec.js @@ -0,0 +1,222 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 { + createMouseEvent, + createOpenMct, + resetApplicationState +} from 'utils/testing'; + +import { debounce } from 'lodash'; + +describe('EditPropertiesAction plugin', () => { + let editPropertiesAction; + let openmct; + let element; + + beforeEach((done) => { + element = document.createElement('div'); + element.style.display = 'block'; + element.style.width = '1920px'; + element.style.height = '1080px'; + + openmct = createOpenMct(); + openmct.on('start', done); + openmct.startHeadless(element); + + editPropertiesAction = openmct.actions.getAction('properties'); + }); + + afterEach(() => { + editPropertiesAction = null; + + return resetApplicationState(openmct); + }); + + it('editPropertiesAction exists', () => { + expect(editPropertiesAction.key).toEqual('properties'); + }); + + it('edit properties action applies to only persistable objects', () => { + spyOn(openmct.objects, 'isPersistable').and.returnValue(true); + + const domainObject = { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + composition: [] + }; + const isApplicableTo = editPropertiesAction.appliesTo([domainObject]); + expect(isApplicableTo).toBe(true); + }); + + it('edit properties action does not apply to non persistable objects', () => { + spyOn(openmct.objects, 'isPersistable').and.returnValue(false); + + const domainObject = { + name: 'mock folder', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + composition: [] + }; + const isApplicableTo = editPropertiesAction.appliesTo([domainObject]); + expect(isApplicableTo).toBe(false); + }); + + it('edit properties action when invoked shows form', (done) => { + const domainObject = { + name: 'mock folder', + notes: 'mock notes', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + modified: 1643065068597, + persisted: 1643065068600, + composition: [] + }; + + const deBouncedFormChange = debounce(handleFormPropertyChange, 500); + openmct.forms.on('onFormPropertyChange', deBouncedFormChange); + + function handleFormPropertyChange(data) { + const form = document.querySelector('.js-form'); + const title = form.querySelector('input'); + expect(title.value).toEqual(domainObject.name); + + const notes = form.querySelector('textArea'); + expect(notes.value).toEqual(domainObject.notes); + + const buttons = form.querySelectorAll('button'); + expect(buttons[0].textContent.trim()).toEqual('OK'); + expect(buttons[1].textContent.trim()).toEqual('Cancel'); + + const clickEvent = createMouseEvent('click'); + buttons[1].dispatchEvent(clickEvent); + + openmct.forms.off('onFormPropertyChange', deBouncedFormChange); + } + + editPropertiesAction.invoke([domainObject]) + .catch(() => { + done(); + }); + }); + + it('edit properties action saves changes', (done) => { + const oldName = 'mock folder'; + const newName = 'renamed mock folder'; + const domainObject = { + name: oldName, + notes: 'mock notes', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + modified: 1643065068597, + persisted: 1643065068600, + composition: [] + }; + let unObserve; + + function callback(newObject) { + expect(newObject.name).not.toEqual(oldName); + expect(newObject.name).toEqual(newName); + + unObserve(); + done(); + } + + const deBouncedCallback = debounce(callback, 300); + unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback); + + let changed = false; + const deBouncedFormChange = debounce(handleFormPropertyChange, 500); + openmct.forms.on('onFormPropertyChange', deBouncedFormChange); + + function handleFormPropertyChange(data) { + const form = document.querySelector('.js-form'); + const title = form.querySelector('input'); + const notes = form.querySelector('textArea'); + + const buttons = form.querySelectorAll('button'); + expect(buttons[0].textContent.trim()).toEqual('OK'); + expect(buttons[1].textContent.trim()).toEqual('Cancel'); + + if (!changed) { + expect(title.value).toEqual(domainObject.name); + expect(notes.value).toEqual(domainObject.notes); + + // change input field value and dispatch event for it + title.focus(); + title.value = newName; + title.dispatchEvent(new Event('input')); + title.blur(); + + changed = true; + } else { + // click ok to save form changes + const clickEvent = createMouseEvent('click'); + buttons[0].dispatchEvent(clickEvent); + + openmct.forms.off('onFormPropertyChange', deBouncedFormChange); + } + } + + editPropertiesAction.invoke([domainObject]); + }); + + it('edit properties action discards changes', (done) => { + const name = 'mock folder'; + const domainObject = { + name, + notes: 'mock notes', + type: 'folder', + identifier: { + key: 'mock-folder', + namespace: '' + }, + modified: 1643065068597, + persisted: 1643065068600, + composition: [] + }; + + editPropertiesAction.invoke([domainObject]) + .catch(() => { + expect(domainObject.name).toEqual(name); + + done(); + }); + + const form = document.querySelector('.js-form'); + const buttons = form.querySelectorAll('button'); + const clickEvent = createMouseEvent('click'); + buttons[1].dispatchEvent(clickEvent); + }); +}); diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index a0ef2b8e4e..ad63b623df 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -55,7 +55,7 @@
{{formatImageAltText}}
+ >{{ formatImageAltText }}
{ }); describe("when moving an object to a new parent and removing from the old parent", () => { + let unObserve; beforeEach((done) => { openmct.router.path = []; @@ -104,7 +105,7 @@ describe("The Move Action plugin", () => { }); }); - openmct.objects.observe(parentObject, '*', (newObject) => { + unObserve = openmct.objects.observe(parentObject, '*', (newObject) => { done(); }); @@ -113,6 +114,10 @@ describe("The Move Action plugin", () => { moveAction.invoke([childObject, parentObject]); }); + afterEach(() => { + unObserve(); + }); + it("the child object's identifier should be in the new parent's composition", () => { let newParentChild = anotherParentObject.composition[0]; expect(newParentChild).toEqual(childObject.identifier); diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue index 9261063a02..6f45cd389a 100644 --- a/src/plugins/notebook/components/NotebookEntry.vue +++ b/src/plugins/notebook/components/NotebookEntry.vue @@ -28,12 +28,16 @@ @drop.prevent="dropOnEntry" >
-
- - {{ createdOnDate }} - {{ createdOnTime }} +
+ {{ createdOnDate }} + {{ createdOnTime }} + + + {{ entry.createdBy }} +