diff options
author | Jesse Mazzella <ozyx@users.noreply.github.com> | 2022-11-10 23:06:13 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-10 23:06:13 +0300 |
commit | 7bb4a136d793044a0b21d61a5d52499a0b9e0ff6 (patch) | |
tree | 58b8a77ad5ae118e301caab2e748caa178a835a2 | |
parent | 8af3b4309f9a63c928b47dd0cdcc4a8f7dd4a37a (diff) |
Use Composition API to add/remove from composition (#5941)
* Use composition API in RemoveAction
* refactor: ScatterPlotView to use composition API
* fix: initialize transaction to null and reset
* fix: remove seriesKey and correct found condition
* refactor: Gauge to use composition API
* refactor: DisplayLayout to use composition API
* test: RemoveAction starts and ends transactions
* test: add ScatterPlot add/remove telemetry test
* test: fix e2e test and add annotation
* test: remove unnecessary awaits
* test: make some displayLayout tests stable
* test{displayLayout}: navigate to objects via url
* test(gauge): add test for add/remove telemetry
* fix(#3117): init layoutItems within transaction
* refactor: add clearSelection() method
* test: remove unstable tag
* fix(#3117): init frames and use transactions
- fixes #3117 for flexible layouts by syncing frames and composition
- also uses transactions now to avoid race condition
* test(flexibleLayout): removing items via context menu
- add test for removing items via context menu while focusing the layout
- add test for removing items via context menu while not focusing the layout
* fix(e2e): use pluginFixtures
* refactor(e2e): improve selectors
* refactor: use async/await for saving transactions
* docs(e2e): fix comments
* test: use soft expects
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
-rw-r--r-- | e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js | 28 | ||||
-rw-r--r-- | e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js | 77 | ||||
-rw-r--r-- | e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js | 93 | ||||
-rw-r--r-- | e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js | 93 | ||||
-rw-r--r-- | src/api/Editor.js | 15 | ||||
-rw-r--r-- | src/plugins/charts/scatter/ScatterPlotView.vue | 6 | ||||
-rw-r--r-- | src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue | 12 | ||||
-rw-r--r-- | src/plugins/displayLayout/components/DisplayLayout.vue | 36 | ||||
-rw-r--r-- | src/plugins/flexibleLayout/components/flexibleLayout.vue | 33 | ||||
-rw-r--r-- | src/plugins/formActions/EditPropertiesAction.js | 12 | ||||
-rw-r--r-- | src/plugins/gauge/components/Gauge.vue | 6 | ||||
-rw-r--r-- | src/plugins/notebook/components/Notebook.vue | 20 | ||||
-rw-r--r-- | src/plugins/remove/RemoveAction.js | 12 | ||||
-rw-r--r-- | src/plugins/remove/pluginSpec.js | 13 |
14 files changed, 375 insertions, 81 deletions
diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index a9c5e5670..a88d1a70a 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -23,7 +23,7 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions'); -test.describe('Testing Display Layout @unstable', () => { +test.describe('Display Layout', () => { let sineWaveObject; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); @@ -55,12 +55,12 @@ test.describe('Testing Display Layout @unstable', () => { // On getting data, check if the value found in the Display Layout is the most recent value // from the Sine Wave Generator const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); - const formattedTelemetryValue = await getTelemValuePromise; + const formattedTelemetryValue = getTelemValuePromise; const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); const displayLayoutValue = await displayLayoutValuePromise.textContent(); const trimmedDisplayValue = displayLayoutValue.trim(); - await expect(trimmedDisplayValue).toBe(formattedTelemetryValue); + expect(trimmedDisplayValue).toBe(formattedTelemetryValue); }); test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => { // Create a Display Layout @@ -86,12 +86,12 @@ test.describe('Testing Display Layout @unstable', () => { // On getting data, check if the value found in the Display Layout is the most recent value // from the Sine Wave Generator - const formattedTelemetryValue = await getTelemValuePromise; + const formattedTelemetryValue = getTelemValuePromise; const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); const displayLayoutValue = await displayLayoutValuePromise.textContent(); const trimmedDisplayValue = displayLayoutValue.trim(); - await expect(trimmedDisplayValue).toBe(formattedTelemetryValue); + expect(trimmedDisplayValue).toBe(formattedTelemetryValue); }); test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => { // Create a Display Layout @@ -121,11 +121,15 @@ test.describe('Testing Display Layout @unstable', () => { // delete - expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); + expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); }); test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3117' + }); // Create a Display Layout - await createDomainObjectWithDefaults(page, { + const displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout', name: "Test Display Layout" }); @@ -144,18 +148,18 @@ test.describe('Testing Display Layout @unstable', () => { // Expand the Display Layout so we can remove the sine wave generator await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); - // Click the original Sine Wave Generator to navigate away from the Display Layout - await page.locator('.c-tree__item .c-tree__item__name:text("Test Sine Wave Generator")').click(); + // Go to the original Sine Wave Generator to navigate away from the Display Layout + await page.goto(sineWaveObject.url); // Bring up context menu and remove await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' }); - await page.locator('text=Remove').click(); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); await page.locator('button:has-text("OK")').click(); // navigate back to the display layout to confirm it has been removed - await page.locator('.c-tree__item .c-tree__item__name:text("Test Display Layout")').click(); + await page.goto(displayLayout.url); - expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); + expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); }); }); diff --git a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js index 484091d7c..b6949e4fc 100644 --- a/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/flexibleLayout/flexibleLayout.e2e.spec.js @@ -23,12 +23,13 @@ const { test, expect } = require('../../../../pluginFixtures'); const { createDomainObjectWithDefaults } = require('../../../../appActions'); -test.describe('Testing Flexible Layout @unstable', () => { +test.describe('Flexible Layout', () => { + let sineWaveObject; test.beforeEach(async ({ page }) => { await page.goto('./', { waitUntil: 'networkidle' }); // Create Sine Wave Generator - await createDomainObjectWithDefaults(page, { + sineWaveObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator', name: "Test Sine Wave Generator" }); @@ -54,13 +55,81 @@ test.describe('Testing Flexible Layout @unstable', () => { await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty'); await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty'); // Check that panes can be dragged while Flexible Layout is in Edit mode - let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); + let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); await expect(dragWrapper).toHaveAttribute('draggable', 'true'); // Save Flexible Layout await page.locator('button[title="Save"]').click(); await page.locator('text=Save and Finish Editing').click(); // Check that panes are not draggable while Flexible Layout is in Browse mode - dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); + dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); await expect(dragWrapper).toHaveAttribute('draggable', 'false'); }); + test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => { + // Create a Display Layout + await createDomainObjectWithDefaults(page, { + type: 'Flexible Layout', + name: "Test Flexible Layout" + }); + // Edit Flexible Layout + await page.locator('[title="Edit"]').click(); + + // Expand the 'My Items' folder in the left tree + await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); + // Add the Sine Wave Generator to the Flexible Layout and save changes + await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); + + // Expand the Flexible Layout so we can remove the sine wave generator + await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); + + // Bring up context menu and remove + await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // Verify that the item has been removed from the layout + expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + }); + test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/3117' + }); + // Create a Flexible Layout + const flexibleLayout = await createDomainObjectWithDefaults(page, { + type: 'Flexible Layout', + name: "Test Flexible Layout" + }); + // Edit Flexible Layout + await page.locator('[title="Edit"]').click(); + + // Expand the 'My Items' folder in the left tree + await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); + // Add the Sine Wave Generator to the Flexible Layout and save changes + await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty'); + await page.locator('button[title="Save"]').click(); + await page.locator('text=Save and Finish Editing').click(); + + expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); + + // Expand the Flexible Layout so we can remove the sine wave generator + await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); + + // Go to the original Sine Wave Generator to navigate away from the Flexible Layout + await page.goto(sineWaveObject.url); + + // Bring up context menu and remove + await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' }); + await page.locator('li[role="menuitem"]:has-text("Remove")').click(); + await page.locator('button:has-text("OK")').click(); + + // navigate back to the display layout to confirm it has been removed + await page.goto(flexibleLayout.url); + + // Verify that the item has been removed from the layout + expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); + }); }); diff --git a/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js new file mode 100644 index 000000000..1e36449a5 --- /dev/null +++ b/e2e/tests/functional/plugins/gauge/gauge.e2e.spec.js @@ -0,0 +1,93 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +* This test suite is dedicated to testing the Gauge component. +*/ + +const { test, expect } = require('../../../../baseFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const uuid = require('uuid').v4; + +test.describe('Gauge', () => { + let gauge; + + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'networkidle' }); + + // Create the gauge + gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); + }); + + test('Can add and remove telemetry sources @unstable', async ({ page }) => { + const editButtonLocator = page.locator('button[title="Edit"]'); + const saveButtonLocator = page.locator('button[title="Save"]'); + + // Create a sine wave generator within the gauge + const swg1 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: gauge.uuid + }); + + // Navigate to the gauge and verify that + // the SWG appears in the elements pool + await page.goto(gauge.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); + await saveButtonLocator.click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + // Create another sine wave generator within the gauge + const swg2 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: gauge.uuid + }); + + // Verify that the 'Replace telemetry source' modal appears and accept it + await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); + await page.click('text=Ok'); + + // Navigate to the gauge and verify that the new SWG + // appears in the elements pool and the old one is gone + await page.goto(gauge.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await saveButtonLocator.click(); + + // Right click on the new SWG in the elements pool and delete it + await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + button: 'right' + }); + await page.locator('li[title="Remove this object from its containing object."]').click(); + + // Verify that the 'Remove object' confirmation modal appears and accept it + await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); + await page.click('text=Ok'); + + // Verify that the elements pool shows no elements + await expect(page.locator('text="No contained elements"')).toBeVisible(); + }); +}); diff --git a/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js new file mode 100644 index 000000000..ccc47fa8b --- /dev/null +++ b/e2e/tests/functional/plugins/plot/scatterPlot.e2e.spec.js @@ -0,0 +1,93 @@ +/***************************************************************************** + * 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. + *****************************************************************************/ + +/* +* This test suite is dedicated to testing the Scatter Plot component. +*/ + +const { test, expect } = require('../../../../pluginFixtures'); +const { createDomainObjectWithDefaults } = require('../../../../appActions'); +const uuid = require('uuid').v4; + +test.describe('Scatter Plot', () => { + let scatterPlot; + + test.beforeEach(async ({ page }) => { + // Open a browser, navigate to the main page, and wait until all networkevents to resolve + await page.goto('./', { waitUntil: 'networkidle' }); + + // Create the Scatter Plot + scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' }); + }); + + test('Can add and remove telemetry sources', async ({ page }) => { + const editButtonLocator = page.locator('button[title="Edit"]'); + const saveButtonLocator = page.locator('button[title="Save"]'); + + // Create a sine wave generator within the scatter plot + const swg1 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: scatterPlot.uuid + }); + + // Navigate to the scatter plot and verify that + // the SWG appears in the elements pool + await page.goto(scatterPlot.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); + await saveButtonLocator.click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + // Create another sine wave generator within the scatter plot + const swg2 = await createDomainObjectWithDefaults(page, { + type: 'Sine Wave Generator', + name: `swg-${uuid()}`, + parent: scatterPlot.uuid + }); + + // Verify that the 'Replace telemetry source' modal appears and accept it + await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); + await page.click('text=Ok'); + + // Navigate to the scatter plot and verify that the new SWG + // appears in the elements pool and the old one is gone + await page.goto(scatterPlot.url); + await editButtonLocator.click(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); + await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); + await saveButtonLocator.click(); + + // Right click on the new SWG in the elements pool and delete it + await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ + button: 'right' + }); + await page.locator('li[title="Remove this object from its containing object."]').click(); + + // Verify that the 'Remove object' confirmation modal appears and accept it + await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); + await page.click('text=Ok'); + + // Verify that the elements pool shows no elements + await expect(page.locator('text="No contained elements"')).toBeVisible(); + }); +}); diff --git a/src/api/Editor.js b/src/api/Editor.js index b9f00357b..53c731024 100644 --- a/src/api/Editor.js +++ b/src/api/Editor.js @@ -56,17 +56,12 @@ export default class Editor extends EventEmitter { * Save any unsaved changes from this editing session. This will * end the current transaction. */ - save() { + async save() { const transaction = this.openmct.objects.getActiveTransaction(); - - return transaction.commit() - .then(() => { - this.editing = false; - this.emit('isEditing', false); - this.openmct.objects.endTransaction(); - }).catch(error => { - throw error; - }); + await transaction.commit(); + this.editing = false; + this.emit('isEditing', false); + this.openmct.objects.endTransaction(); } /** diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue index 129a3bca9..7baf9721d 100644 --- a/src/plugins/charts/scatter/ScatterPlotView.vue +++ b/src/plugins/charts/scatter/ScatterPlotView.vue @@ -112,11 +112,7 @@ export default { } }, removeFromComposition(telemetryObject) { - let composition = this.domainObject.composition.filter(id => - !this.openmct.objects.areIdsEqual(id, telemetryObject.identifier) - ); - - this.openmct.objects.mutate(this.domainObject, 'composition', composition); + this.composition.remove(telemetryObject); }, addTelemetryObject(telemetryObject) { // grab information we need from the added telmetry object diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue index c7af21973..03dd02cd4 100644 --- a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue +++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue @@ -104,10 +104,14 @@ export default { this.$set(this.plotSeries, this.plotSeries.length, series); this.setAxesLabels(); }, - removeSeries(series) { - const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier)); - if (index !== undefined) { - this.$delete(this.plotSeries, index); + removeSeries(seriesKey) { + const seriesIndex = this.plotSeries.findIndex( + plotSeries => this.openmct.objects.areIdsEqual(seriesKey, plotSeries.identifier) + ); + + const foundSeries = seriesIndex > -1; + if (foundSeries) { + this.$delete(this.plotSeries, seriesIndex); this.setAxesLabels(); } }, diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue index 98afcba65..e20703c9e 100644 --- a/src/plugins/displayLayout/components/DisplayLayout.vue +++ b/src/plugins/displayLayout/components/DisplayLayout.vue @@ -245,6 +245,9 @@ export default { }); this.gridDimensions = [wMax * this.gridSize[0], hMax * this.gridSize[1]]; }, + clearSelection() { + this.$el.click(); + }, watchDisplayResize() { const resizeObserver = new ResizeObserver(() => this.updateGrid()); @@ -478,7 +481,7 @@ export default { }); _.pullAt(this.layoutItems, indices); this.mutate("configuration.items", this.layoutItems); - this.$el.click(); + this.clearSelection(); }, untrackItem(item) { if (!item.identifier) { @@ -504,15 +507,11 @@ export default { } if (!telemetryViewCount && !objectViewCount) { - this.removeFromComposition(keyString); + this.removeFromComposition(item); } }, - removeFromComposition(keyString) { - let composition = this.domainObject.composition ? this.domainObject.composition : []; - composition = composition.filter(identifier => { - return this.openmct.objects.makeKeyString(identifier) !== keyString; - }); - this.mutate("composition", composition); + removeFromComposition(item) { + this.composition.remove(item); }, initializeItems() { this.telemetryViewMap = {}; @@ -529,7 +528,10 @@ export default { } }); + this.startTransaction(); removedItems.forEach(this.removeFromConfiguration); + + return this.endTransaction(); }, isItemAlreadyTracked(child) { let found = false; @@ -590,7 +592,7 @@ export default { } }); this.mutate("configuration.items", layoutItems); - this.$el.click(); + this.clearSelection(); }, orderItem(position, selectedItems) { let delta = ORDERS[position]; @@ -773,7 +775,7 @@ export default { this.$nextTick(() => { this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems); this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles); - this.$el.click(); //clear selection; + this.clearSelection(); newDomainObjectsArray.forEach(domainObject => { this.composition.add(domainObject); @@ -867,6 +869,20 @@ export default { this.removeItem(selection); this.initSelectIndex = this.layoutItems.length - 1; //restore selection }, + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.transaction = this.openmct.objects.startTransaction(); + } + }, + async endTransaction() { + if (!this.transaction) { + return; + } + + await this.transaction.commit(); + this.openmct.objects.endTransaction(); + this.transaction = null; + }, toggleGrid() { this.showGrid = !this.showGrid; }, diff --git a/src/plugins/flexibleLayout/components/flexibleLayout.vue b/src/plugins/flexibleLayout/components/flexibleLayout.vue index c2f8958de..9f176ba80 100644 --- a/src/plugins/flexibleLayout/components/flexibleLayout.vue +++ b/src/plugins/flexibleLayout/components/flexibleLayout.vue @@ -185,10 +185,24 @@ export default { this.composition.off('add', this.addFrame); }, methods: { + containsObject(identifier) { + if ('composition' in this.domainObject) { + return this.domainObject.composition + .some(childId => this.openmct.objects.areIdsEqual(childId, identifier)); + } + + return false; + }, buildIdentifierMap() { this.containers.forEach(container => { container.frames.forEach(frame => { - let keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); + if (!this.containsObject(frame.domainObjectIdentifier)) { + this.removeChildObject(frame.domainObjectIdentifier); + + return; + } + + const keystring = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier); this.identifierMap[keystring] = true; }); }); @@ -296,11 +310,14 @@ export default { } }, persist(index) { + this.startTransaction(); if (index) { this.openmct.objects.mutate(this.domainObject, `configuration.containers[${index}]`, this.containers[index]); } else { this.openmct.objects.mutate(this.domainObject, 'configuration.containers', this.containers); } + + return this.endTransaction(); }, startContainerResizing(index) { let beforeContainer = this.containers[index]; @@ -366,6 +383,20 @@ export default { }); this.persist(); + }, + startTransaction() { + if (!this.openmct.objects.isTransactionActive()) { + this.transaction = this.openmct.objects.startTransaction(); + } + }, + async endTransaction() { + if (!this.transaction) { + return; + } + + await this.transaction.commit(); + this.openmct.objects.endTransaction(); + this.transaction = null; } } }; diff --git a/src/plugins/formActions/EditPropertiesAction.js b/src/plugins/formActions/EditPropertiesAction.js index f2cb232f5..eb4dec2e4 100644 --- a/src/plugins/formActions/EditPropertiesAction.js +++ b/src/plugins/formActions/EditPropertiesAction.js @@ -52,7 +52,7 @@ export default class EditPropertiesAction extends PropertiesAction { /** * @private */ - _onSave(changes) { + async _onSave(changes) { if (!this.openmct.objects.isTransactionActive()) { this.openmct.objects.startTransaction(); } @@ -70,14 +70,8 @@ export default class EditPropertiesAction extends PropertiesAction { this.openmct.objects.mutate(this.domainObject, key, value); }); const transaction = this.openmct.objects.getActiveTransaction(); - - return transaction.commit() - .catch(error => { - throw error; - }).finally(() => { - this.openmct.objects.endTransaction(); - }); - + await transaction.commit(); + this.openmct.objects.endTransaction(); } catch (error) { this.openmct.notifications.error('Error saving objects'); console.error(error); diff --git a/src/plugins/gauge/components/Gauge.vue b/src/plugins/gauge/components/Gauge.vue index 2d250158e..810ddfef0 100644 --- a/src/plugins/gauge/components/Gauge.vue +++ b/src/plugins/gauge/components/Gauge.vue @@ -598,11 +598,7 @@ export default { return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2); }, removeFromComposition(telemetryObject = this.telemetryObject) { - let composition = this.domainObject.composition.filter(id => - !this.openmct.objects.areIdsEqual(id, telemetryObject.identifier) - ); - - this.openmct.objects.mutate(this.domainObject, 'composition', composition); + this.composition.remove(telemetryObject); }, refreshData(bounds, isTick) { if (!isTick) { diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index c242d1356..91bdbebc7 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -894,24 +894,16 @@ export default { this.transaction = this.openmct.objects.startTransaction(); } }, - saveTransaction() { + async saveTransaction() { if (this.transaction !== undefined) { - this.transaction.commit() - .catch(error => { - throw error; - }).finally(() => { - this.openmct.objects.endTransaction(); - }); + await this.transaction.commit(); + this.openmct.objects.endTransaction(); } }, - cancelTransaction() { + async cancelTransaction() { if (this.transaction !== undefined) { - this.transaction.cancel() - .catch(error => { - throw error; - }).finally(() => { - this.openmct.objects.endTransaction(); - }); + await this.transaction.cancel(); + this.openmct.objects.endTransaction(); } } } diff --git a/src/plugins/remove/RemoveAction.js b/src/plugins/remove/RemoveAction.js index fd6700bde..0ab0a0fa8 100644 --- a/src/plugins/remove/RemoveAction.js +++ b/src/plugins/remove/RemoveAction.js @@ -35,6 +35,7 @@ export default class RemoveAction { this.openmct = openmct; this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable + this.#transaction = null; } async invoke(objectPath) { @@ -152,16 +153,13 @@ export default class RemoveAction { } } - saveTransaction() { + async saveTransaction() { if (!this.#transaction) { return; } - return this.#transaction.commit() - .catch(error => { - throw error; - }).finally(() => { - this.openmct.objects.endTransaction(); - }); + await this.#transaction.commit(); + this.openmct.objects.endTransaction(); + this.#transaction = null; } } diff --git a/src/plugins/remove/pluginSpec.js b/src/plugins/remove/pluginSpec.js index 8b1af6304..987709b34 100644 --- a/src/plugins/remove/pluginSpec.js +++ b/src/plugins/remove/pluginSpec.js @@ -78,6 +78,8 @@ describe("The Remove Action plugin", () => { spyOn(removeAction, 'removeFromComposition').and.callThrough(); spyOn(removeAction, 'inNavigationPath').and.returnValue(false); spyOn(openmct.objects, 'mutate').and.callThrough(); + spyOn(openmct.objects, 'startTransaction').and.callThrough(); + spyOn(openmct.objects, 'endTransaction').and.callThrough(); removeAction.removeFromComposition(parentObject, childObject); }); @@ -90,6 +92,17 @@ describe("The Remove Action plugin", () => { expect(openmct.objects.mutate).toHaveBeenCalled(); expect(openmct.objects.mutate.calls.argsFor(0)[0]).toEqual(parentObject); }); + + it("it should start a transaction", () => { + expect(openmct.objects.startTransaction).toHaveBeenCalled(); + }); + + it("it should end the transaction", (done) => { + setTimeout(() => { + expect(openmct.objects.endTransaction).toHaveBeenCalled(); + done(); + }, 100); + }); }); describe("when determining the object is applicable", () => { |