From 02f101377037f644fae0f6f7bafdd31ced79e9a9 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 2 Nov 2023 13:42:37 -0700 Subject: [PATCH] fix: DisplayLayout and FlexibleLayout toolbar actions only apply to selected layout (#7184) * refactor: convert to ES6 function * fix: include `keyString` in event name - This negates the need for complicated logic in determining which objectView the action was intended for * fix: handle the case of currentView being null * fix: add keyString to flexibleLayout toolbar events * fix: properly unregister listeners * fix: remove unused imports * fix: revert parameter reorder * refactor: replace usage of `arguments` with `...args` * fix: add a11y to display layout + toolbar * test: add first cut of layout toolbar suite * test: cleanup a bit and add Image test * test: add stubs * fix: remove unused variable * refactor(DisplayLayoutToolbar): convert to ES6 class * test: generate localStorage data for display layout tests * fix: clarify "Add" button label * test: cleanup and don't parameterize tests * test: fix path for recycled_local_storage.json * fix: path to local storage file * docs: add documentation for utilizing localStorage in e2e * fix: path to recycled_local_storage.json * docs: add note hyperlink --- e2e/README.md | 24 +- .../display_layout_with_child_layouts.json | 22 + .../generateLocalStorageData.e2e.spec.js | 36 + .../conditionSet/conditionSet.e2e.spec.js | 11 +- .../displayLayout/displayLayout.e2e.spec.js | 139 +- .../displayLayout/DisplayLayoutToolbar.js | 1690 +++++++++-------- .../displayLayout/components/BoxView.vue | 3 + .../components/DisplayLayout.vue | 2 + .../components/DisplayLayoutGrid.vue | 9 +- .../displayLayout/components/EllipseView.vue | 3 + .../displayLayout/components/LayoutFrame.vue | 14 +- .../displayLayout/components/LineView.vue | 9 +- .../displayLayout/components/TextView.vue | 3 + src/plugins/displayLayout/plugin.js | 7 +- .../flexibleLayoutViewProvider.js | 7 +- src/plugins/flexibleLayout/toolbarProvider.js | 28 +- src/ui/components/ObjectView.vue | 18 +- src/ui/toolbar/components/ToolbarMenu.vue | 8 +- 18 files changed, 1175 insertions(+), 858 deletions(-) create mode 100644 e2e/test-data/display_layout_with_child_layouts.json diff --git a/e2e/README.md b/e2e/README.md index 8c270676b0..4a4ebbd2ce 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -193,7 +193,7 @@ Current list of test tags: |`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).| |`@gds` | Denotes a GDS Test Case used in the VIPER Mission.| |`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.| -|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).| +|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)| |`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.| |`@unstable` | A new test or test which is known to be flaky.| |`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.| @@ -352,6 +352,28 @@ By adhering to this principle, we can create tests that are both robust and refl 1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions. This ensures that your changes will be picked up with large refactors. +##### Utilizing LocalStorage + 1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states. + 1. To generate a localStorage state to be used in a test: + - Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder: + ```js + // Save localStorage for future test execution + await context.storageState({ + path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json') + }); + ``` + - Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite): + ```js + const LOCALSTORAGE_PATH = path.resolve( + __dirname, + '../../../../test-data/display_layout_with_child_layouts.json' + ); + test.use({ + storageState: path.resolve(__dirname, LOCALSTORAGE_PATH) + }); + ``` + + ### How to write a great test - Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole` diff --git a/e2e/test-data/display_layout_with_child_layouts.json b/e2e/test-data/display_layout_with_child_layouts.json new file mode 100644 index 0000000000..437e6d8a7d --- /dev/null +++ b/e2e/test-data/display_layout_with_child_layouts.json @@ -0,0 +1,22 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "http://localhost:8080", + "localStorage": [ + { + "name": "mct", + "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},\"764a490f-4a83-4874-a062-e38c112f69c7\":{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":30,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"},{\"width\":32,\"height\":18,\"x\":30,\"y\":1,\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9f89d6f6-fb7f-4af3-9cc3-4c5d0864e908\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605120,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413605120},\"801d3a35-91ac-43ae-b175-bc1be65f3587\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140},\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240}}" + }, + { + "name": "mct-recent-objects", + "value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7\",\"domainObject\":{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240}},{\"objectPath\":[{\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7/d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"domainObject\":{\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240}},{\"objectPath\":[{\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140},{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7/801d3a35-91ac-43ae-b175-bc1be65f3587\",\"domainObject\":{\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020}}]" + }, + { + "name": "mct-tree-expanded", + "value": "[]" + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js index 15dec748ed..9d180db083 100644 --- a/e2e/tests/framework/generateLocalStorageData.e2e.spec.js +++ b/e2e/tests/framework/generateLocalStorageData.e2e.spec.js @@ -55,6 +55,42 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => { await page.goto('./', { waitUntil: 'domcontentloaded' }); }); + test('Generate display layout with 2 child display layouts', async ({ page, context }) => { + // Create Display Layout + const parent = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Parent Display Layout' + }); + const child1 = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Child Layout 1', + parent: parent.uuid + }); + const child2 = await createDomainObjectWithDefaults(page, { + type: 'Display Layout', + name: 'Child Layout 2', + parent: parent.uuid + }); + + await page.goto(parent.url); + await page.getByLabel('Edit').click(); + await page.getByLabel(`${child2.name} Layout Grid`).hover(); + await page.getByLabel('Move Sub-object Frame').nth(1).click(); + await page.getByLabel('X:').fill('30'); + + await page.getByLabel(`${child1.name} Layout Grid`).hover(); + await page.getByLabel('Move Sub-object Frame').first().click(); + await page.getByLabel('Y:').fill('30'); + + await page.getByLabel('Save').click(); + await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); + + //Save localStorage for future test execution + await context.storageState({ + path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json') + }); + }); + // TODO: Visual test for the generated object here // - Move to using appActions to create the overlay plot // and embedded standard telemetry object diff --git a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js index 1452d3164c..8111599a47 100644 --- a/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js +++ b/e2e/tests/functional/plugins/conditionSet/conditionSet.e2e.spec.js @@ -19,7 +19,7 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +/* global __dirname */ /* This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to @@ -31,6 +31,7 @@ const { createDomainObjectWithDefaults, createExampleTelemetryObject } = require('../../../../appActions'); +const path = require('path'); let conditionSetUrl; let getConditionSetIdentifierFromUrl; @@ -48,7 +49,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); //Save localStorage for future test execution - await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); + await context.storageState({ + path: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json') + }); //Set object identifier from url conditionSetUrl = page.url(); @@ -59,7 +62,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { }); //Load localStorage for subsequent tests - test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); + test.use({ + storageState: path.resolve(__dirname, '../../../../test-data/recycled_local_storage.json') + }); //Begin suite of tests again localStorage test('Condition set object properties persist in main view and inspector @localStorage', async ({ diff --git a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js index 4eed324cba..aaa590764e 100644 --- a/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js +++ b/e2e/tests/functional/plugins/displayLayout/displayLayout.e2e.spec.js @@ -19,8 +19,9 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ - +/* global __dirname */ const { test, expect } = require('../../../../pluginFixtures'); +const path = require('path'); const { createDomainObjectWithDefaults, setStartOffset, @@ -29,6 +30,88 @@ const { setIndependentTimeConductorBounds } = require('../../../../appActions'); +const LOCALSTORAGE_PATH = path.resolve( + __dirname, + '../../../../test-data/display_layout_with_child_layouts.json' +); +const TINY_IMAGE_BASE64 = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII'; + +test.describe('Display Layout Toolbar Actions', () => { + const PARENT_DISPLAY_LAYOUT_NAME = 'Parent Display Layout'; + const CHILD_DISPLAY_LAYOUT_NAME1 = 'Child Layout 1'; + test.beforeEach(async ({ page }) => { + await page.goto('./', { waitUntil: 'domcontentloaded' }); + await setRealTimeMode(page); + await page + .locator('a') + .filter({ hasText: 'Parent Display Layout Display Layout' }) + .first() + .click(); + await page.getByLabel('Edit').click(); + }); + test.use({ + storageState: path.resolve(__dirname, LOCALSTORAGE_PATH) + }); + + test('can add/remove Text element to a single layout', async ({ page }) => { + const layoutObject = 'Text'; + await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME); + }); + await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1); + }); + }); + test('can add/remove Image to a single layout', async ({ page }) => { + const layoutObject = 'Image'; + await test.step("Add and remove image element from the parent's layout", async () => { + expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject); + expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1); + await removeLayoutObject(page, layoutObject); + expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + }); + await test.step("Add and remove image from the child's layout", async () => { + await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject); + expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1); + await removeLayoutObject(page, layoutObject); + expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0); + }); + }); + test(`can add/remove Box to a single layout`, async ({ page }) => { + const layoutObject = 'Box'; + await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME); + }); + await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1); + }); + }); + test(`can add/remove Line to a single layout`, async ({ page }) => { + const layoutObject = 'Line'; + await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME); + }); + await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1); + }); + }); + test(`can add/remove Ellipse to a single layout`, async ({ page }) => { + const layoutObject = 'Ellipse'; + await test.step(`Add and remove ${layoutObject} from the parent's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, PARENT_DISPLAY_LAYOUT_NAME); + }); + await test.step(`Add and remove ${layoutObject} from the child's layout`, async () => { + await addAndRemoveDrawingObjectAndAssert(page, layoutObject, CHILD_DISPLAY_LAYOUT_NAME1); + }); + }); + test.fixme('Can switch view types of a single SWG in a layout', async ({ page }) => {}); + test.fixme('Can merge multiple plots in a layout', async ({ page }) => {}); + test.fixme('Can adjust stack order of a single object in a layout', async ({ page }) => {}); + test.fixme('Can duplicate a single object in a layout', async ({ page }) => {}); +}); + test.describe('Display Layout', () => { /** @type {import('../../../../appActions').CreatedObjectInfo} */ let sineWaveObject; @@ -41,6 +124,7 @@ test.describe('Display Layout', () => { type: 'Sine Wave Generator' }); }); + test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => { @@ -339,6 +423,59 @@ test.describe('Display Layout', () => { }); }); +async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) { + expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0); + await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject); + expect( + await page + .getByLabel(layoutObject, { + exact: true + }) + .count() + ).toBe(1); + await removeLayoutObject(page, layoutObject); + expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0); +} + +/** + * Remove the first matching layout object from the layout + * @param {import('@playwright/test').Page} page + * @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject + */ +async function removeLayoutObject(page, layoutObject) { + await page + .getByLabel(`Move ${layoutObject} Frame`, { exact: true }) + .or(page.getByLabel(layoutObject, { exact: true })) + .first() + // eslint-disable-next-line playwright/no-force-option + .click({ force: true }); + await page.getByTitle('Delete the selected object').click(); + await page.getByRole('button', { name: 'OK' }).click(); +} + +/** + * Add a layout object to the specified layout + * @param {import('@playwright/test').Page} page + * @param {string} layoutName + * @param {'Box' | 'Ellipse' | 'Line' | 'Text' | 'Image'} layoutObject + */ +async function addLayoutObject(page, layoutName, layoutObject) { + await page.getByLabel(`${layoutName} Layout Grid`).click(); + await page.getByText('Add Drawing Object').click(); + await page + .getByRole('menuitem', { + name: layoutObject + }) + .click(); + if (layoutObject === 'Text') { + await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!'); + await page.getByText('OK').click(); + } else if (layoutObject === 'Image') { + await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64); + await page.getByText('OK').click(); + } +} + /** * Util for subscribing to a telemetry object by object identifier * Limitations: Currently only works to return telemetry once to the node scope diff --git a/src/plugins/displayLayout/DisplayLayoutToolbar.js b/src/plugins/displayLayout/DisplayLayoutToolbar.js index 1e73e3b465..66206ae7b8 100644 --- a/src/plugins/displayLayout/DisplayLayoutToolbar.js +++ b/src/plugins/displayLayout/DisplayLayoutToolbar.js @@ -20,855 +20,901 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['lodash'], function (_) { - function DisplayLayoutToolbar(openmct) { - return { - name: 'Display Layout Toolbar', - key: 'layout', - description: 'A toolbar for objects inside a display layout.', - forSelection: function (selection) { - if (!selection || selection.length === 0) { - return false; - } +import _ from 'lodash'; - let selectionPath = selection[0]; - let selectedObject = selectionPath[0]; - let selectedParent = selectionPath[1]; +const CONTEXT_ACTION = 'contextAction'; +const CONTEXT_ACTIONS = Object.freeze({ + ADD_ELEMENT: 'addElement', + REMOVE_ITEM: 'removeItem', + DUPLICATE_ITEM: 'duplicateItem', + ORDER_ITEM: 'orderItem', + SWITCH_VIEW_TYPE: 'switchViewType', + MERGE_MULTIPLE_TELEMETRY_VIEWS: 'mergeMultipleTelemetryViews', + MERGE_MULTIPLE_OVERLAY_PLOTS: 'mergeMultipleOverlayPlots', + TOGGLE_GRID: 'toggleGrid' +}); - // Apply the layout toolbar if the selected object is inside a layout, or the main layout is selected. - return ( - (selectedParent && - selectedParent.context.item && - selectedParent.context.item.type === 'layout') || - (selectedObject.context.item && selectedObject.context.item.type === 'layout') - ); - }, - toolbar: function (selectedObjects) { - const DIALOG_FORM = { - text: { - title: 'Text Element Properties', - sections: [ - { - rows: [ - { - key: 'text', - control: 'textfield', - name: 'Text', - required: true, - cssClass: 'l-input-lg' - } - ] - } - ] - }, - image: { - title: 'Image Properties', - sections: [ - { - rows: [ - { - key: 'url', - control: 'textfield', - name: 'Image URL', - cssClass: 'l-input-lg', - required: true - } - ] - } - ] +const DIALOG_FORM = { + text: { + title: 'Text Element Properties', + sections: [ + { + rows: [ + { + key: 'text', + control: 'textfield', + name: 'Text', + required: true, + cssClass: 'l-input-lg' } - }; - const VIEW_TYPES = { - 'telemetry-view': { - value: 'telemetry-view', - name: 'Alphanumeric', - class: 'icon-alphanumeric' - }, - 'telemetry.plot.overlay': { - value: 'telemetry.plot.overlay', - name: 'Overlay Plot', - class: 'icon-plot-overlay' - }, - 'telemetry.plot.stacked': { - value: 'telemetry.plot.stacked', - name: 'Stacked Plot', - class: 'icon-plot-stacked' - }, - table: { - value: 'table', - name: 'Table', - class: 'icon-tabular-scrolling' + ] + } + ] + }, + image: { + title: 'Image Properties', + sections: [ + { + rows: [ + { + key: 'url', + control: 'textfield', + name: 'Image URL', + cssClass: 'l-input-lg', + required: true } - }; - const APPLICABLE_VIEWS = { - 'telemetry-view': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table - ], - 'telemetry.plot.overlay': [ - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table, - VIEW_TYPES['telemetry-view'] - ], - 'telemetry.plot.stacked': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES.table, - VIEW_TYPES['telemetry-view'] - ], - table: [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES['telemetry-view'] - ], - 'telemetry-view-multi': [ - VIEW_TYPES['telemetry.plot.overlay'], - VIEW_TYPES['telemetry.plot.stacked'], - VIEW_TYPES.table - ], - 'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']] - }; + ] + } + ] + } +}; +const VIEW_TYPES = { + 'telemetry-view': { + value: 'telemetry-view', + name: 'Alphanumeric', + class: 'icon-alphanumeric' + }, + 'telemetry.plot.overlay': { + value: 'telemetry.plot.overlay', + name: 'Overlay Plot', + class: 'icon-plot-overlay' + }, + 'telemetry.plot.stacked': { + value: 'telemetry.plot.stacked', + name: 'Stacked Plot', + class: 'icon-plot-stacked' + }, + table: { + value: 'table', + name: 'Table', + class: 'icon-tabular-scrolling' + } +}; +const APPLICABLE_VIEWS = { + 'telemetry-view': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table + ], + 'telemetry.plot.overlay': [ + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table, + VIEW_TYPES['telemetry-view'] + ], + 'telemetry.plot.stacked': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES.table, + VIEW_TYPES['telemetry-view'] + ], + table: [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES['telemetry-view'] + ], + 'telemetry-view-multi': [ + VIEW_TYPES['telemetry.plot.overlay'], + VIEW_TYPES['telemetry.plot.stacked'], + VIEW_TYPES.table + ], + 'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']] +}; - function getPath(selectionPath) { - return `configuration.items[${selectionPath[0].context.index}]`; - } +export default class DisplayLayoutToolbar { + #openmct; - function getAllOfType(selection, specificType) { - return selection.filter((selectionPath) => { - let type = selectionPath[0].context.layoutItem.type; + constructor(openmct) { + this.#openmct = openmct; + this.name = 'Display Layout Toolbar'; + this.key = 'layout'; + this.description = 'A toolbar for objects inside a display layout.'; + } - return type === specificType; - }); - } + forSelection(selection) { + if (!selection || selection.length === 0) { + return false; + } - function getAllTypes(selection) { - return selection.filter((selectionPath) => { - let type = selectionPath[0].context.layoutItem.type; + let selectionPath = selection[0]; + let selectedObject = selectionPath[0]; + let selectedParent = selectionPath[1]; - return ( - type === 'text-view' || - type === 'telemetry-view' || - type === 'box-view' || - type === 'ellipse-view' || - type === 'image-view' || - type === 'line-view' || - type === 'subobject-view' - ); - }); - } + // Apply the layout toolbar if the selected object is inside a layout, or the main layout is selected. + return ( + (selectedParent && + selectedParent.context.item && + selectedParent.context.item.type === 'layout') || + (selectedObject.context.item && selectedObject.context.item.type === 'layout') + ); + } - function getAddButton(selection, selectionPath) { - if (selection.length === 1) { - selectionPath = selectionPath || selection[0]; + #getPath(selectionPath) { + return `configuration.items[${selectionPath[0].context.index}]`; + } - return { - control: 'menu', - domainObject: selectionPath[0].context.item, - method: function (option) { - let name = option.name.toLowerCase(); - let form = DIALOG_FORM[name]; - if (form) { - showForm(form, name, selectionPath); - } else { - openmct.objectViews.emit('contextAction', 'addElement', name); - } - }, - key: 'add', - icon: 'icon-plus', - label: 'Add', - options: [ - { - name: 'Box', - class: 'icon-box-round-corners' - }, - { - name: 'Ellipse', - class: 'icon-circle' - }, - { - name: 'Line', - class: 'icon-line-horz' - }, - { - name: 'Text', - class: 'icon-font' - }, - { - name: 'Image', - class: 'icon-image' - } - ] - }; - } - } + #getAllOfType(selection, specificType) { + return selection.filter((selectionPath) => { + let type = selectionPath[0].context.layoutItem.type; - function getToggleFrameButton(selectedParent, selection) { - return { - control: 'toggle-button', - domainObject: selectedParent, - applicableSelectedItems: selection.filter( - (selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view' - ), - property: function (selectionPath) { - return getPath(selectionPath) + '.hasFrame'; - }, - options: [ - { - value: false, - icon: 'icon-frame-hide', - title: 'Frame visible', - label: 'Hide frame' - }, - { - value: true, - icon: 'icon-frame-show', - title: 'Frame hidden', - label: 'Show frame' - } - ] - }; - } + return type === specificType; + }); + } - function getRemoveButton(selectedParent, selectionPath, selection) { - return { - control: 'button', - domainObject: selectedParent, - icon: 'icon-trash', - title: 'Delete the selected object', - method: function () { - let prompt = openmct.overlays.dialog({ - iconClass: 'alert', - message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, - buttons: [ - { - label: 'OK', - emphasis: 'true', - callback: function () { - openmct.objectViews.emit( - 'contextAction', - 'removeItem', - getAllTypes(selection) - ); - prompt.dismiss(); - } - }, - { - label: 'Cancel', - callback: function () { - prompt.dismiss(); - } - } - ] - }); - } - }; - } + #getAllTypes(selection) { + return selection.filter((selectionPath) => { + let type = selectionPath[0].context.layoutItem.type; - function getStackOrder(selectedParent, selectionPath) { - return { - control: 'menu', - domainObject: selectedParent, - icon: 'icon-layers', - title: 'Move the selected object above or below other objects', - options: [ - { - name: 'Move to Top', - value: 'top', - class: 'icon-arrow-double-up' - }, - { - name: 'Move Up', - value: 'up', - class: 'icon-arrow-up' - }, - { - name: 'Move Down', - value: 'down', - class: 'icon-arrow-down' - }, - { - name: 'Move to Bottom', - value: 'bottom', - class: 'icon-arrow-double-down' - } - ], - method: function (option) { - openmct.objectViews.emit( - 'contextAction', - 'orderItem', - option.value, - getAllTypes(selectedObjects) - ); - } - }; - } + return ( + type === 'text-view' || + type === 'telemetry-view' || + type === 'box-view' || + type === 'ellipse-view' || + type === 'image-view' || + type === 'line-view' || + type === 'subobject-view' + ); + }); + } - function getXInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + '.x'; - }, - label: 'X:', - title: 'X position' - }; - } - } + #getAddButton(selection, selectionPath) { + if (selection.length === 1) { + selectionPath = selectionPath || selection[0]; + const domainObject = selectionPath[0].context.item; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); - function getYInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + '.y'; - }, - label: 'Y:', - title: 'Y position' - }; - } - } - - function getWidthInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + '.width'; - }, - label: 'W:', - title: 'Resize object width' - }; - } - } - - function getHeightInput(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: getAllTypes(selection), - property: function (selectionPath) { - return getPath(selectionPath) + '.height'; - }, - label: 'H:', - title: 'Resize object height' - }; - } - } - - function getX2Input(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: selection.filter((selectionPath) => { - return selectionPath[0].context.layoutItem.type === 'line-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + '.x2'; - }, - label: 'X2:', - title: 'X2 position' - }; - } - } - - function getY2Input(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'input', - type: 'number', - domainObject: selectedParent, - applicableSelectedItems: selection.filter((selectionPath) => { - return selectionPath[0].context.layoutItem.type === 'line-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + '.y2'; - }, - label: 'Y2:', - title: 'Y2 position' - }; - } - } - - function getTextButton(selectedParent, selection) { - return { - control: 'button', - domainObject: selectedParent, - applicableSelectedItems: selection.filter((selectionPath) => { - return selectionPath[0].context.layoutItem.type === 'text-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath); - }, - icon: 'icon-pencil', - title: 'Edit text properties', - label: 'Edit text', - dialog: DIALOG_FORM.text - }; - } - - function getTelemetryValueMenu(selectionPath, selection) { - if (selection.length === 1) { - return { - control: 'select-menu', - domainObject: selectionPath[1].context.item, - applicableSelectedItems: selection.filter((path) => { - return path[0].context.layoutItem.type === 'telemetry-view'; - }), - property: function (path) { - return getPath(path) + '.value'; - }, - title: 'Set value', - options: openmct.telemetry - .getMetadata(selectionPath[0].context.item) - .values() - .map((value) => { - return { - name: value.name, - value: value.key - }; - }) - }; - } - } - - function getDisplayModeMenu(selectedParent, selection) { - if (selection.length === 1) { - return { - control: 'select-menu', - domainObject: selectedParent, - applicableSelectedItems: selection.filter((selectionPath) => { - return selectionPath[0].context.layoutItem.type === 'telemetry-view'; - }), - property: function (selectionPath) { - return getPath(selectionPath) + '.displayMode'; - }, - title: 'Set display mode', - options: [ - { - name: 'Label + Value', - value: 'all' - }, - { - name: 'Label only', - value: 'label' - }, - { - name: 'Value only', - value: 'value' - } - ] - }; - } - } - - function getDuplicateButton(selectedParent, selectionPath, selection) { - return { - control: 'button', - domainObject: selectedParent, - icon: 'icon-duplicate', - title: 'Duplicate the selected object', - method: function () { - openmct.objectViews.emit('contextAction', 'duplicateItem', selection); - } - }; - } - - function getPropertyFromPath(object, path) { - let splitPath = path.split('.'); - let property = Object.assign({}, object); - - while (splitPath.length && property) { - property = property[splitPath.shift()]; - } - - return property; - } - - function areAllViews(type, path, selection) { - let allTelemetry = true; - - selection.forEach((selectedItem) => { - let selectedItemContext = selectedItem[0].context; - - if (getPropertyFromPath(selectedItemContext, path) !== type) { - allTelemetry = false; - } - }); - - return allTelemetry; - } - - function getToggleUnitsButton(selectedParent, selection) { - let applicableItems = getAllOfType(selection, 'telemetry-view'); - applicableItems = unitsOnly(applicableItems); - if (!applicableItems.length) { - return; - } - - return { - control: 'toggle-button', - domainObject: selectedParent, - applicableSelectedItems: applicableItems, - property: function (selectionPath) { - return getPath(selectionPath) + '.showUnits'; - }, - options: [ - { - value: true, - icon: 'icon-eye-open', - title: 'Show units', - label: 'Show units' - }, - { - value: false, - icon: 'icon-eye-disabled', - title: 'Hide units', - label: 'Hide units' - } - ] - }; - } - - function unitsOnly(items) { - let results = items.filter((item) => { - let currentItem = item[0]; - let metadata = openmct.telemetry.getMetadata(currentItem.context.item); - if (!metadata) { - return false; - } - - let hasUnits = metadata.valueMetadatas.filter((metadatum) => metadatum.unit).length; - - return hasUnits > 0; - }); - - return results; - } - - function getViewSwitcherMenu(selectedParent, selectionPath, selection) { - if (selection.length === 1) { - // eslint-disable-next-line no-unused-vars - let displayLayoutContext = selectionPath[1].context; - let selectedItemContext = selectionPath[0].context; - let selectedItemType = selectedItemContext.item.type; - - if (selectedItemContext.layoutItem.type === 'telemetry-view') { - selectedItemType = 'telemetry-view'; - } - - let viewOptions = APPLICABLE_VIEWS[selectedItemType]; - - if (viewOptions) { - return { - control: 'menu', - domainObject: selectedParent, - icon: 'icon-object', - title: 'Switch the way this telemetry is displayed', - label: 'View type', - options: viewOptions, - method: function (option) { - openmct.objectViews.emit( - 'contextAction', - 'switchViewType', - selectedItemContext, - option.value, - selection - ); - } - }; - } - } else if (selection.length > 1) { - if (areAllViews('telemetry-view', 'layoutItem.type', selection)) { - return { - control: 'menu', - domainObject: selectedParent, - icon: 'icon-object', - title: 'Merge into a telemetry table or plot', - label: 'View type', - options: APPLICABLE_VIEWS['telemetry-view-multi'], - method: function (option) { - openmct.objectViews.emit( - 'contextAction', - 'mergeMultipleTelemetryViews', - selection, - option.value - ); - } - }; - } else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) { - return { - control: 'menu', - domainObject: selectedParent, - icon: 'icon-object', - title: 'Merge into a stacked plot', - options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'], - method: function (option) { - openmct.objectViews.emit( - 'contextAction', - 'mergeMultipleOverlayPlots', - selection, - option.value - ); - } - }; - } - } - } - - function getToggleGridButton(selection, selectionPath) { - const ICON_GRID_SHOW = 'icon-grid-on'; - const ICON_GRID_HIDE = 'icon-grid-off'; - - let displayLayoutContext; - - if (selection.length === 1 && selectionPath === undefined) { - displayLayoutContext = selection[0][0].context; + return { + control: 'menu', + domainObject, + method: (option) => { + let name = option.name.toLowerCase(); + let form = DIALOG_FORM[name]; + if (form) { + this.#showForm(form, name, selection); } else { - displayLayoutContext = selectionPath[1].context; + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.ADD_ELEMENT, + name, + selection + ); } + }, + key: 'add', + icon: 'icon-plus', + label: 'Add Drawing Object', + options: [ + { + name: 'Box', + class: 'icon-box-round-corners' + }, + { + name: 'Ellipse', + class: 'icon-circle' + }, + { + name: 'Line', + class: 'icon-line-horz' + }, + { + name: 'Text', + class: 'icon-font' + }, + { + name: 'Image', + class: 'icon-image' + } + ] + }; + } + } - return { - control: 'button', - domainObject: displayLayoutContext.item, - icon: ICON_GRID_SHOW, - method: function () { - openmct.objectViews.emit('contextAction', 'toggleGrid'); + #getToggleFrameButton(selectedParent, selection) { + return { + control: 'toggle-button', + domainObject: selectedParent, + applicableSelectedItems: selection.filter( + (selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view' + ), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.hasFrame'; + }, + options: [ + { + value: false, + icon: 'icon-frame-hide', + title: 'Frame visible', + label: 'Hide frame' + }, + { + value: true, + icon: 'icon-frame-show', + title: 'Frame hidden', + label: 'Show frame' + } + ] + }; + } - this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW; + #getRemoveButton(selectedParent, selectionPath, selection) { + const domainObject = selectedParent; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + return { + control: 'button', + domainObject, + icon: 'icon-trash', + title: 'Delete the selected object', + method: () => { + let prompt = this.#openmct.overlays.dialog({ + iconClass: 'alert', + message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, + buttons: [ + { + label: 'OK', + emphasis: 'true', + callback: () => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.REMOVE_ITEM, + this.#getAllTypes(selection), + selection + ); + prompt.dismiss(); + } }, - secondary: true - }; - } - - function getSeparator() { - return { - control: 'separator' - }; - } - - function isMainLayoutSelected(selectionPath) { - let selectedObject = selectionPath[0].context.item; - - return ( - selectedObject && - selectedObject.type === 'layout' && - !selectionPath[0].context.layoutItem - ); - } - - function showForm(formStructure, name, selectionPath) { - openmct.forms.showForm(formStructure).then((changes) => { - openmct.objectViews.emit('contextAction', 'addElement', name, changes); - }); - } - - if (isMainLayoutSelected(selectedObjects[0])) { - return [getToggleGridButton(selectedObjects), getAddButton(selectedObjects)]; - } - - let toolbar = { - 'add-menu': [], - text: [], - url: [], - viewSwitcher: [], - 'toggle-frame': [], - 'display-mode': [], - 'telemetry-value': [], - style: [], - 'unit-toggle': [], - position: [], - duplicate: [], - remove: [], - 'toggle-grid': [] - }; - - selectedObjects.forEach((selectionPath) => { - let selectedParent = selectionPath[1].context.item; - let layoutItem = selectionPath[0].context.layoutItem; - - if (!layoutItem || selectedParent.locked) { - return; - } - - if (layoutItem.type === 'subobject-view') { - if ( - toolbar['add-menu'].length === 0 && - selectionPath[0].context.item.type === 'layout' - ) { - toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)]; - } - - if (toolbar['toggle-frame'].length === 0) { - toolbar['toggle-frame'] = [getToggleFrameButton(selectedParent, selectedObjects)]; - } - - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - - if (toolbar.viewSwitcher.length === 0) { - toolbar.viewSwitcher = [ - getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) - ]; - } - } else if (layoutItem.type === 'telemetry-view') { - if (toolbar['display-mode'].length === 0) { - toolbar['display-mode'] = [getDisplayModeMenu(selectedParent, selectedObjects)]; - } - - if (toolbar['telemetry-value'].length === 0) { - toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)]; - } - - if (toolbar['unit-toggle'].length === 0) { - let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects); - if (toggleUnitsButton) { - toolbar['unit-toggle'] = [toggleUnitsButton]; + { + label: 'Cancel', + callback: function () { + prompt.dismiss(); } } - - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - - if (toolbar.viewSwitcher.length === 0) { - toolbar.viewSwitcher = [ - getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) - ]; - } - } else if (layoutItem.type === 'text-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.text.length === 0) { - toolbar.text = [getTextButton(selectedParent, selectedObjects)]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'image-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getHeightInput(selectedParent, selectedObjects), - getWidthInput(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } else if (layoutItem.type === 'line-view') { - if (toolbar.position.length === 0) { - toolbar.position = [ - getStackOrder(selectedParent, selectionPath), - getSeparator(), - getXInput(selectedParent, selectedObjects), - getYInput(selectedParent, selectedObjects), - getX2Input(selectedParent, selectedObjects), - getY2Input(selectedParent, selectedObjects) - ]; - } - - if (toolbar.remove.length === 0) { - toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; - } - } - - if (toolbar.duplicate.length === 0) { - toolbar.duplicate = [ - getDuplicateButton(selectedParent, selectionPath, selectedObjects) - ]; - } - - if (toolbar['toggle-grid'].length === 0) { - toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; - } + ] }); + } + }; + } - let toolbarArray = Object.values(toolbar); - - return _.flatten( - toolbarArray.reduce((accumulator, group, index) => { - group = group.filter((control) => control !== undefined); - - if (group.length > 0) { - accumulator.push(group); - - if (index < toolbarArray.length - 1) { - accumulator.push(getSeparator()); - } - } - - return accumulator; - }, []) + #getStackOrder(selectedParent, selectionPath, selectedObjects) { + const domainObject = selectedParent; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + return { + control: 'menu', + domainObject, + icon: 'icon-layers', + title: 'Move the selected object above or below other objects', + options: [ + { + name: 'Move to Top', + value: 'top', + class: 'icon-arrow-double-up' + }, + { + name: 'Move Up', + value: 'up', + class: 'icon-arrow-up' + }, + { + name: 'Move Down', + value: 'down', + class: 'icon-arrow-down' + }, + { + name: 'Move to Bottom', + value: 'bottom', + class: 'icon-arrow-double-down' + } + ], + method: (option) => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.ORDER_ITEM, + option.value, + this.#getAllTypes(selectedObjects) ); } }; } - return DisplayLayoutToolbar; -}); + #getXInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: this.#getAllTypes(selection), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.x'; + }, + label: 'X:', + title: 'X position' + }; + } + } + + #getYInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: this.#getAllTypes(selection), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.y'; + }, + label: 'Y:', + title: 'Y position' + }; + } + } + + #getWidthInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: this.#getAllTypes(selection), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.width'; + }, + label: 'W:', + title: 'Resize object width' + }; + } + } + + #getHeightInput(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: this.#getAllTypes(selection), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.height'; + }, + label: 'H:', + title: 'Resize object height' + }; + } + } + + #getX2Input(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'line-view'; + }), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.x2'; + }, + label: 'X2:', + title: 'X2 position' + }; + } + } + + #getY2Input(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'input', + type: 'number', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'line-view'; + }), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.y2'; + }, + label: 'Y2:', + title: 'Y2 position' + }; + } + } + + #getTextButton(selectedParent, selection) { + return { + control: 'button', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'text-view'; + }), + property: (selectionPath) => { + return this.#getPath(selectionPath); + }, + icon: 'icon-pencil', + title: 'Edit text properties', + label: 'Edit text', + dialog: DIALOG_FORM.text + }; + } + + #getTelemetryValueMenu(selectionPath, selection) { + if (selection.length === 1) { + return { + control: 'select-menu', + domainObject: selectionPath[1].context.item, + applicableSelectedItems: selection.filter((path) => { + return path[0].context.layoutItem.type === 'telemetry-view'; + }), + property: (path) => { + return this.#getPath(path) + '.value'; + }, + title: 'Set value', + options: this.#openmct.telemetry + .getMetadata(selectionPath[0].context.item) + .values() + .map((value) => { + return { + name: value.name, + value: value.key + }; + }) + }; + } + } + + #getDisplayModeMenu(selectedParent, selection) { + if (selection.length === 1) { + return { + control: 'select-menu', + domainObject: selectedParent, + applicableSelectedItems: selection.filter((selectionPath) => { + return selectionPath[0].context.layoutItem.type === 'telemetry-view'; + }), + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.displayMode'; + }, + title: 'Set display mode', + options: [ + { + name: 'Label + Value', + value: 'all' + }, + { + name: 'Label only', + value: 'label' + }, + { + name: 'Value only', + value: 'value' + } + ] + }; + } + } + + #getDuplicateButton(selectedParent, selectionPath, selection) { + const domainObject = selectedParent; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + return { + control: 'button', + domainObject, + icon: 'icon-duplicate', + title: 'Duplicate the selected object', + method: () => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.DUPLICATE_ITEM, + selection + ); + } + }; + } + + #getPropertyFromPath(object, path) { + let splitPath = path.split('.'); + let property = Object.assign({}, object); + + while (splitPath.length && property) { + property = property[splitPath.shift()]; + } + + return property; + } + + #areAllViews(type, path, selection) { + let allTelemetry = true; + + selection.forEach((selectedItem) => { + let selectedItemContext = selectedItem[0].context; + + if (this.#getPropertyFromPath(selectedItemContext, path) !== type) { + allTelemetry = false; + } + }); + + return allTelemetry; + } + + #getToggleUnitsButton(selectedParent, selection) { + let applicableItems = this.#getAllOfType(selection, 'telemetry-view'); + applicableItems = this.#unitsOnly(applicableItems); + if (!applicableItems.length) { + return; + } + + return { + control: 'toggle-button', + domainObject: selectedParent, + applicableSelectedItems: applicableItems, + property: (selectionPath) => { + return this.#getPath(selectionPath) + '.showUnits'; + }, + options: [ + { + value: true, + icon: 'icon-eye-open', + title: 'Show units', + label: 'Show units' + }, + { + value: false, + icon: 'icon-eye-disabled', + title: 'Hide units', + label: 'Hide units' + } + ] + }; + } + + #unitsOnly(items) { + let results = items.filter((item) => { + let currentItem = item[0]; + let metadata = this.#openmct.telemetry.getMetadata(currentItem.context.item); + if (!metadata) { + return false; + } + + let hasUnits = metadata.valueMetadatas.filter((metadatum) => metadatum.unit).length; + + return hasUnits > 0; + }); + + return results; + } + + #getViewSwitcherMenu(selectedParent, selectionPath, selection) { + if (selection.length === 1) { + let selectedItemContext = selectionPath[0].context; + let selectedItemType = selectedItemContext.item.type; + + if (selectedItemContext.layoutItem.type === 'telemetry-view') { + selectedItemType = 'telemetry-view'; + } + + let viewOptions = APPLICABLE_VIEWS[selectedItemType]; + + if (viewOptions) { + const domainObject = selectedParent; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + return { + control: 'menu', + domainObject, + icon: 'icon-object', + title: 'Switch the way this telemetry is displayed', + label: 'View type', + options: viewOptions, + method: (option) => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.SWITCH_VIEW_TYPE, + selectedItemContext, + option.value, + selection + ); + } + }; + } + } else if (selection.length > 1) { + const domainObject = selectedParent; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + if (this.#areAllViews('telemetry-view', 'layoutItem.type', selection)) { + return { + control: 'menu', + domainObject, + icon: 'icon-object', + title: 'Merge into a telemetry table or plot', + label: 'View type', + options: APPLICABLE_VIEWS['telemetry-view-multi'], + method: (option) => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.MERGE_MULTIPLE_TELEMETRY_VIEWS, + selection, + option.value + ); + } + }; + } else if (this.#areAllViews('telemetry.plot.overlay', 'item.type', selection)) { + return { + control: 'menu', + domainObject, + icon: 'icon-object', + title: 'Merge into a stacked plot', + options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'], + method: (option) => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.MERGE_MULTIPLE_OVERLAY_PLOTS, + selection, + option.value + ); + } + }; + } + } + } + + #getToggleGridButton(selection, selectionPath) { + const ICON_GRID_SHOW = 'icon-grid-on'; + const ICON_GRID_HIDE = 'icon-grid-off'; + + let displayLayoutContext; + + if (selection.length === 1 && selectionPath === undefined) { + displayLayoutContext = selection[0][0].context; + } else { + displayLayoutContext = selectionPath[1].context; + } + + const domainObject = displayLayoutContext.item; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + + return { + control: 'button', + domainObject, + icon: ICON_GRID_SHOW, + method: () => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.TOGGLE_GRID + ); + + this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW; + }, + secondary: true + }; + } + + #getSeparator() { + return { + control: 'separator' + }; + } + + #isMainLayoutSelected(selectionPath) { + let selectedObject = selectionPath[0].context.item; + + return ( + selectedObject && selectedObject.type === 'layout' && !selectionPath[0].context.layoutItem + ); + } + + #showForm(formStructure, name, selection) { + const domainObject = selection[0][0].context.item; + const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier); + this.#openmct.forms.showForm(formStructure).then((changes) => { + this.#openmct.objectViews.emit( + `${CONTEXT_ACTION}:${keyString}`, + CONTEXT_ACTIONS.ADD_ELEMENT, + name, + changes, + selection + ); + }); + } + + toolbar(selectedObjects) { + if (this.#isMainLayoutSelected(selectedObjects[0])) { + return [this.#getToggleGridButton(selectedObjects), this.#getAddButton(selectedObjects)]; + } + + let toolbar = { + 'add-menu': [], + text: [], + url: [], + viewSwitcher: [], + 'toggle-frame': [], + 'display-mode': [], + 'telemetry-value': [], + style: [], + 'unit-toggle': [], + position: [], + duplicate: [], + remove: [], + 'toggle-grid': [] + }; + + selectedObjects.forEach((selectionPath) => { + let selectedParent = selectionPath[1].context.item; + let layoutItem = selectionPath[0].context.layoutItem; + + if (!layoutItem || selectedParent.locked) { + return; + } + + if (layoutItem.type === 'subobject-view') { + if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') { + toolbar['add-menu'] = [this.#getAddButton(selectedObjects, selectionPath)]; + } + + if (toolbar['toggle-frame'].length === 0) { + toolbar['toggle-frame'] = [this.#getToggleFrameButton(selectedParent, selectedObjects)]; + } + + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath, selectedObjects), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getHeightInput(selectedParent, selectedObjects), + this.#getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + + if (toolbar.viewSwitcher.length === 0) { + toolbar.viewSwitcher = [ + this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) + ]; + } + } else if (layoutItem.type === 'telemetry-view') { + if (toolbar['display-mode'].length === 0) { + toolbar['display-mode'] = [this.#getDisplayModeMenu(selectedParent, selectedObjects)]; + } + + if (toolbar['telemetry-value'].length === 0) { + toolbar['telemetry-value'] = [ + this.#getTelemetryValueMenu(selectionPath, selectedObjects) + ]; + } + + if (toolbar['unit-toggle'].length === 0) { + let toggleUnitsButton = this.#getToggleUnitsButton(selectedParent, selectedObjects); + if (toggleUnitsButton) { + toolbar['unit-toggle'] = [toggleUnitsButton]; + } + } + + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath, selectedObjects), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getHeightInput(selectedParent, selectedObjects), + this.#getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + + if (toolbar.viewSwitcher.length === 0) { + toolbar.viewSwitcher = [ + this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) + ]; + } + } else if (layoutItem.type === 'text-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath, selectedObjects), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getHeightInput(selectedParent, selectedObjects), + this.#getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.text.length === 0) { + toolbar.text = [this.#getTextButton(selectedParent, selectedObjects)]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath, selectedObjects), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getHeightInput(selectedParent, selectedObjects), + this.#getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'image-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath, selectedObjects), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getHeightInput(selectedParent, selectedObjects), + this.#getWidthInput(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } else if (layoutItem.type === 'line-view') { + if (toolbar.position.length === 0) { + toolbar.position = [ + this.#getStackOrder(selectedParent, selectionPath), + this.#getSeparator(), + this.#getXInput(selectedParent, selectedObjects), + this.#getYInput(selectedParent, selectedObjects), + this.#getX2Input(selectedParent, selectedObjects), + this.#getY2Input(selectedParent, selectedObjects) + ]; + } + + if (toolbar.remove.length === 0) { + toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)]; + } + } + + if (toolbar.duplicate.length === 0) { + toolbar.duplicate = [ + this.#getDuplicateButton(selectedParent, selectionPath, selectedObjects) + ]; + } + + if (toolbar['toggle-grid'].length === 0) { + toolbar['toggle-grid'] = [this.#getToggleGridButton(selectedObjects, selectionPath)]; + } + }); + + let toolbarArray = Object.values(toolbar); + + return _.flatten( + toolbarArray.reduce((accumulator, group, index) => { + group = group.filter((control) => control !== undefined); + + if (group.length > 0) { + accumulator.push(group); + + if (index < toolbarArray.length - 1) { + accumulator.push(this.#getSeparator()); + } + } + + return accumulator; + }, []) + ); + } +} diff --git a/src/plugins/displayLayout/components/BoxView.vue b/src/plugins/displayLayout/components/BoxView.vue index b6cdf9c927..3c07541871 100644 --- a/src/plugins/displayLayout/components/BoxView.vue +++ b/src/plugins/displayLayout/components/BoxView.vue @@ -33,6 +33,9 @@ class="c-box-view u-style-receiver js-style-receiver" :class="[styleClass]" :style="style" + role="application" + aria-roledescription="draggable box" + aria-label="Box" > diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue index 7527d57315..9f53448a79 100644 --- a/src/plugins/displayLayout/components/DisplayLayout.vue +++ b/src/plugins/displayLayout/components/DisplayLayout.vue @@ -36,6 +36,8 @@ :grid-size="gridSize" :show-grid="showGrid" :grid-dimensions="gridDimensions" + :aria-label="`${domainObject.name} Layout Grid`" + :aria-hidden="showGrid ? 'false' : 'true'" />
diff --git a/src/plugins/displayLayout/components/LayoutFrame.vue b/src/plugins/displayLayout/components/LayoutFrame.vue index 2898d79791..95568f5e47 100644 --- a/src/plugins/displayLayout/components/LayoutFrame.vue +++ b/src/plugins/displayLayout/components/LayoutFrame.vue @@ -30,13 +30,18 @@ :style="style" > -
+