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
This commit is contained in:
Jesse Mazzella 2023-11-02 13:42:37 -07:00 committed by GitHub
parent bdff210a9c
commit 02f1013770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1175 additions and 858 deletions

View File

@ -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).| |`@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.| |`@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`.| |`@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.| |`@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.| |`@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.| |`@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. 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. 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 ### How to write a great test
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole` - Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`

File diff suppressed because one or more lines are too long

View File

@ -55,6 +55,42 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
await page.goto('./', { waitUntil: 'domcontentloaded' }); 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 // TODO: Visual test for the generated object here
// - Move to using appActions to create the overlay plot // - Move to using appActions to create the overlay plot
// and embedded standard telemetry object // and embedded standard telemetry object

View File

@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * 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 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 suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
@ -31,6 +31,7 @@ const {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
createExampleTelemetryObject createExampleTelemetryObject
} = require('../../../../appActions'); } = require('../../../../appActions');
const path = require('path');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; 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")')]); await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
//Save localStorage for future test execution //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 //Set object identifier from url
conditionSetUrl = page.url(); conditionSetUrl = page.url();
@ -59,7 +62,9 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
}); });
//Load localStorage for subsequent tests //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 //Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({ test('Condition set object properties persist in main view and inspector @localStorage', async ({

View File

@ -19,8 +19,9 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/* global __dirname */
const { test, expect } = require('../../../../pluginFixtures'); const { test, expect } = require('../../../../pluginFixtures');
const path = require('path');
const { const {
createDomainObjectWithDefaults, createDomainObjectWithDefaults,
setStartOffset, setStartOffset,
@ -29,6 +30,88 @@ const {
setIndependentTimeConductorBounds setIndependentTimeConductorBounds
} = require('../../../../appActions'); } = 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', () => { test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */ /** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject; let sineWaveObject;
@ -41,6 +124,7 @@ test.describe('Display Layout', () => {
type: 'Sine Wave Generator' type: 'Sine Wave Generator'
}); });
}); });
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({
page 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 * Util for subscribing to a telemetry object by object identifier
* Limitations: Currently only works to return telemetry once to the node scope * Limitations: Currently only works to return telemetry once to the node scope

View File

@ -20,31 +20,21 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['lodash'], function (_) { import _ from 'lodash';
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;
}
let selectionPath = selection[0]; const CONTEXT_ACTION = 'contextAction';
let selectedObject = selectionPath[0]; const CONTEXT_ACTIONS = Object.freeze({
let selectedParent = selectionPath[1]; 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. const DIALOG_FORM = {
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: { text: {
title: 'Text Element Properties', title: 'Text Element Properties',
sections: [ sections: [
@ -77,8 +67,8 @@ define(['lodash'], function (_) {
} }
] ]
} }
}; };
const VIEW_TYPES = { const VIEW_TYPES = {
'telemetry-view': { 'telemetry-view': {
value: 'telemetry-view', value: 'telemetry-view',
name: 'Alphanumeric', name: 'Alphanumeric',
@ -99,8 +89,8 @@ define(['lodash'], function (_) {
name: 'Table', name: 'Table',
class: 'icon-tabular-scrolling' class: 'icon-tabular-scrolling'
} }
}; };
const APPLICABLE_VIEWS = { const APPLICABLE_VIEWS = {
'telemetry-view': [ 'telemetry-view': [
VIEW_TYPES['telemetry.plot.overlay'], VIEW_TYPES['telemetry.plot.overlay'],
VIEW_TYPES['telemetry.plot.stacked'], VIEW_TYPES['telemetry.plot.stacked'],
@ -127,13 +117,41 @@ define(['lodash'], function (_) {
VIEW_TYPES.table VIEW_TYPES.table
], ],
'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']] 'telemetry.plot.overlay-multi': [VIEW_TYPES['telemetry.plot.stacked']]
}; };
function getPath(selectionPath) { export default class DisplayLayoutToolbar {
#openmct;
constructor(openmct) {
this.#openmct = openmct;
this.name = 'Display Layout Toolbar';
this.key = 'layout';
this.description = 'A toolbar for objects inside a display layout.';
}
forSelection(selection) {
if (!selection || selection.length === 0) {
return false;
}
let selectionPath = selection[0];
let selectedObject = selectionPath[0];
let selectedParent = selectionPath[1];
// 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')
);
}
#getPath(selectionPath) {
return `configuration.items[${selectionPath[0].context.index}]`; return `configuration.items[${selectionPath[0].context.index}]`;
} }
function getAllOfType(selection, specificType) { #getAllOfType(selection, specificType) {
return selection.filter((selectionPath) => { return selection.filter((selectionPath) => {
let type = selectionPath[0].context.layoutItem.type; let type = selectionPath[0].context.layoutItem.type;
@ -141,7 +159,7 @@ define(['lodash'], function (_) {
}); });
} }
function getAllTypes(selection) { #getAllTypes(selection) {
return selection.filter((selectionPath) => { return selection.filter((selectionPath) => {
let type = selectionPath[0].context.layoutItem.type; let type = selectionPath[0].context.layoutItem.type;
@ -157,25 +175,32 @@ define(['lodash'], function (_) {
}); });
} }
function getAddButton(selection, selectionPath) { #getAddButton(selection, selectionPath) {
if (selection.length === 1) { if (selection.length === 1) {
selectionPath = selectionPath || selection[0]; selectionPath = selectionPath || selection[0];
const domainObject = selectionPath[0].context.item;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'menu', control: 'menu',
domainObject: selectionPath[0].context.item, domainObject,
method: function (option) { method: (option) => {
let name = option.name.toLowerCase(); let name = option.name.toLowerCase();
let form = DIALOG_FORM[name]; let form = DIALOG_FORM[name];
if (form) { if (form) {
showForm(form, name, selectionPath); this.#showForm(form, name, selection);
} else { } else {
openmct.objectViews.emit('contextAction', 'addElement', name); this.#openmct.objectViews.emit(
`${CONTEXT_ACTION}:${keyString}`,
CONTEXT_ACTIONS.ADD_ELEMENT,
name,
selection
);
} }
}, },
key: 'add', key: 'add',
icon: 'icon-plus', icon: 'icon-plus',
label: 'Add', label: 'Add Drawing Object',
options: [ options: [
{ {
name: 'Box', name: 'Box',
@ -202,15 +227,15 @@ define(['lodash'], function (_) {
} }
} }
function getToggleFrameButton(selectedParent, selection) { #getToggleFrameButton(selectedParent, selection) {
return { return {
control: 'toggle-button', control: 'toggle-button',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: selection.filter( applicableSelectedItems: selection.filter(
(selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view' (selectionPath) => selectionPath[0].context.layoutItem.type === 'subobject-view'
), ),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.hasFrame'; return this.#getPath(selectionPath) + '.hasFrame';
}, },
options: [ options: [
{ {
@ -229,25 +254,28 @@ define(['lodash'], function (_) {
}; };
} }
function getRemoveButton(selectedParent, selectionPath, selection) { #getRemoveButton(selectedParent, selectionPath, selection) {
const domainObject = selectedParent;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'button', control: 'button',
domainObject: selectedParent, domainObject,
icon: 'icon-trash', icon: 'icon-trash',
title: 'Delete the selected object', title: 'Delete the selected object',
method: function () { method: () => {
let prompt = openmct.overlays.dialog({ let prompt = this.#openmct.overlays.dialog({
iconClass: 'alert', iconClass: 'alert',
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
buttons: [ buttons: [
{ {
label: 'OK', label: 'OK',
emphasis: 'true', emphasis: 'true',
callback: function () { callback: () => {
openmct.objectViews.emit( this.#openmct.objectViews.emit(
'contextAction', `${CONTEXT_ACTION}:${keyString}`,
'removeItem', CONTEXT_ACTIONS.REMOVE_ITEM,
getAllTypes(selection) this.#getAllTypes(selection),
selection
); );
prompt.dismiss(); prompt.dismiss();
} }
@ -264,10 +292,12 @@ define(['lodash'], function (_) {
}; };
} }
function getStackOrder(selectedParent, selectionPath) { #getStackOrder(selectedParent, selectionPath, selectedObjects) {
const domainObject = selectedParent;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'menu', control: 'menu',
domainObject: selectedParent, domainObject,
icon: 'icon-layers', icon: 'icon-layers',
title: 'Move the selected object above or below other objects', title: 'Move the selected object above or below other objects',
options: [ options: [
@ -292,26 +322,26 @@ define(['lodash'], function (_) {
class: 'icon-arrow-double-down' class: 'icon-arrow-double-down'
} }
], ],
method: function (option) { method: (option) => {
openmct.objectViews.emit( this.#openmct.objectViews.emit(
'contextAction', `${CONTEXT_ACTION}:${keyString}`,
'orderItem', CONTEXT_ACTIONS.ORDER_ITEM,
option.value, option.value,
getAllTypes(selectedObjects) this.#getAllTypes(selectedObjects)
); );
} }
}; };
} }
function getXInput(selectedParent, selection) { #getXInput(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
type: 'number', type: 'number',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: getAllTypes(selection), applicableSelectedItems: this.#getAllTypes(selection),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.x'; return this.#getPath(selectionPath) + '.x';
}, },
label: 'X:', label: 'X:',
title: 'X position' title: 'X position'
@ -319,15 +349,15 @@ define(['lodash'], function (_) {
} }
} }
function getYInput(selectedParent, selection) { #getYInput(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
type: 'number', type: 'number',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: getAllTypes(selection), applicableSelectedItems: this.#getAllTypes(selection),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.y'; return this.#getPath(selectionPath) + '.y';
}, },
label: 'Y:', label: 'Y:',
title: 'Y position' title: 'Y position'
@ -335,15 +365,15 @@ define(['lodash'], function (_) {
} }
} }
function getWidthInput(selectedParent, selection) { #getWidthInput(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
type: 'number', type: 'number',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: getAllTypes(selection), applicableSelectedItems: this.#getAllTypes(selection),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.width'; return this.#getPath(selectionPath) + '.width';
}, },
label: 'W:', label: 'W:',
title: 'Resize object width' title: 'Resize object width'
@ -351,15 +381,15 @@ define(['lodash'], function (_) {
} }
} }
function getHeightInput(selectedParent, selection) { #getHeightInput(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
type: 'number', type: 'number',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: getAllTypes(selection), applicableSelectedItems: this.#getAllTypes(selection),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.height'; return this.#getPath(selectionPath) + '.height';
}, },
label: 'H:', label: 'H:',
title: 'Resize object height' title: 'Resize object height'
@ -367,7 +397,7 @@ define(['lodash'], function (_) {
} }
} }
function getX2Input(selectedParent, selection) { #getX2Input(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
@ -376,8 +406,8 @@ define(['lodash'], function (_) {
applicableSelectedItems: selection.filter((selectionPath) => { applicableSelectedItems: selection.filter((selectionPath) => {
return selectionPath[0].context.layoutItem.type === 'line-view'; return selectionPath[0].context.layoutItem.type === 'line-view';
}), }),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.x2'; return this.#getPath(selectionPath) + '.x2';
}, },
label: 'X2:', label: 'X2:',
title: 'X2 position' title: 'X2 position'
@ -385,7 +415,7 @@ define(['lodash'], function (_) {
} }
} }
function getY2Input(selectedParent, selection) { #getY2Input(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'input', control: 'input',
@ -394,8 +424,8 @@ define(['lodash'], function (_) {
applicableSelectedItems: selection.filter((selectionPath) => { applicableSelectedItems: selection.filter((selectionPath) => {
return selectionPath[0].context.layoutItem.type === 'line-view'; return selectionPath[0].context.layoutItem.type === 'line-view';
}), }),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.y2'; return this.#getPath(selectionPath) + '.y2';
}, },
label: 'Y2:', label: 'Y2:',
title: 'Y2 position' title: 'Y2 position'
@ -403,15 +433,15 @@ define(['lodash'], function (_) {
} }
} }
function getTextButton(selectedParent, selection) { #getTextButton(selectedParent, selection) {
return { return {
control: 'button', control: 'button',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: selection.filter((selectionPath) => { applicableSelectedItems: selection.filter((selectionPath) => {
return selectionPath[0].context.layoutItem.type === 'text-view'; return selectionPath[0].context.layoutItem.type === 'text-view';
}), }),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath); return this.#getPath(selectionPath);
}, },
icon: 'icon-pencil', icon: 'icon-pencil',
title: 'Edit text properties', title: 'Edit text properties',
@ -420,7 +450,7 @@ define(['lodash'], function (_) {
}; };
} }
function getTelemetryValueMenu(selectionPath, selection) { #getTelemetryValueMenu(selectionPath, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'select-menu', control: 'select-menu',
@ -428,11 +458,11 @@ define(['lodash'], function (_) {
applicableSelectedItems: selection.filter((path) => { applicableSelectedItems: selection.filter((path) => {
return path[0].context.layoutItem.type === 'telemetry-view'; return path[0].context.layoutItem.type === 'telemetry-view';
}), }),
property: function (path) { property: (path) => {
return getPath(path) + '.value'; return this.#getPath(path) + '.value';
}, },
title: 'Set value', title: 'Set value',
options: openmct.telemetry options: this.#openmct.telemetry
.getMetadata(selectionPath[0].context.item) .getMetadata(selectionPath[0].context.item)
.values() .values()
.map((value) => { .map((value) => {
@ -445,7 +475,7 @@ define(['lodash'], function (_) {
} }
} }
function getDisplayModeMenu(selectedParent, selection) { #getDisplayModeMenu(selectedParent, selection) {
if (selection.length === 1) { if (selection.length === 1) {
return { return {
control: 'select-menu', control: 'select-menu',
@ -453,8 +483,8 @@ define(['lodash'], function (_) {
applicableSelectedItems: selection.filter((selectionPath) => { applicableSelectedItems: selection.filter((selectionPath) => {
return selectionPath[0].context.layoutItem.type === 'telemetry-view'; return selectionPath[0].context.layoutItem.type === 'telemetry-view';
}), }),
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.displayMode'; return this.#getPath(selectionPath) + '.displayMode';
}, },
title: 'Set display mode', title: 'Set display mode',
options: [ options: [
@ -475,19 +505,25 @@ define(['lodash'], function (_) {
} }
} }
function getDuplicateButton(selectedParent, selectionPath, selection) { #getDuplicateButton(selectedParent, selectionPath, selection) {
const domainObject = selectedParent;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'button', control: 'button',
domainObject: selectedParent, domainObject,
icon: 'icon-duplicate', icon: 'icon-duplicate',
title: 'Duplicate the selected object', title: 'Duplicate the selected object',
method: function () { method: () => {
openmct.objectViews.emit('contextAction', 'duplicateItem', selection); this.#openmct.objectViews.emit(
`${CONTEXT_ACTION}:${keyString}`,
CONTEXT_ACTIONS.DUPLICATE_ITEM,
selection
);
} }
}; };
} }
function getPropertyFromPath(object, path) { #getPropertyFromPath(object, path) {
let splitPath = path.split('.'); let splitPath = path.split('.');
let property = Object.assign({}, object); let property = Object.assign({}, object);
@ -498,13 +534,13 @@ define(['lodash'], function (_) {
return property; return property;
} }
function areAllViews(type, path, selection) { #areAllViews(type, path, selection) {
let allTelemetry = true; let allTelemetry = true;
selection.forEach((selectedItem) => { selection.forEach((selectedItem) => {
let selectedItemContext = selectedItem[0].context; let selectedItemContext = selectedItem[0].context;
if (getPropertyFromPath(selectedItemContext, path) !== type) { if (this.#getPropertyFromPath(selectedItemContext, path) !== type) {
allTelemetry = false; allTelemetry = false;
} }
}); });
@ -512,9 +548,9 @@ define(['lodash'], function (_) {
return allTelemetry; return allTelemetry;
} }
function getToggleUnitsButton(selectedParent, selection) { #getToggleUnitsButton(selectedParent, selection) {
let applicableItems = getAllOfType(selection, 'telemetry-view'); let applicableItems = this.#getAllOfType(selection, 'telemetry-view');
applicableItems = unitsOnly(applicableItems); applicableItems = this.#unitsOnly(applicableItems);
if (!applicableItems.length) { if (!applicableItems.length) {
return; return;
} }
@ -523,8 +559,8 @@ define(['lodash'], function (_) {
control: 'toggle-button', control: 'toggle-button',
domainObject: selectedParent, domainObject: selectedParent,
applicableSelectedItems: applicableItems, applicableSelectedItems: applicableItems,
property: function (selectionPath) { property: (selectionPath) => {
return getPath(selectionPath) + '.showUnits'; return this.#getPath(selectionPath) + '.showUnits';
}, },
options: [ options: [
{ {
@ -543,10 +579,10 @@ define(['lodash'], function (_) {
}; };
} }
function unitsOnly(items) { #unitsOnly(items) {
let results = items.filter((item) => { let results = items.filter((item) => {
let currentItem = item[0]; let currentItem = item[0];
let metadata = openmct.telemetry.getMetadata(currentItem.context.item); let metadata = this.#openmct.telemetry.getMetadata(currentItem.context.item);
if (!metadata) { if (!metadata) {
return false; return false;
} }
@ -559,10 +595,8 @@ define(['lodash'], function (_) {
return results; return results;
} }
function getViewSwitcherMenu(selectedParent, selectionPath, selection) { #getViewSwitcherMenu(selectedParent, selectionPath, selection) {
if (selection.length === 1) { if (selection.length === 1) {
// eslint-disable-next-line no-unused-vars
let displayLayoutContext = selectionPath[1].context;
let selectedItemContext = selectionPath[0].context; let selectedItemContext = selectionPath[0].context;
let selectedItemType = selectedItemContext.item.type; let selectedItemType = selectedItemContext.item.type;
@ -573,17 +607,19 @@ define(['lodash'], function (_) {
let viewOptions = APPLICABLE_VIEWS[selectedItemType]; let viewOptions = APPLICABLE_VIEWS[selectedItemType];
if (viewOptions) { if (viewOptions) {
const domainObject = selectedParent;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'menu', control: 'menu',
domainObject: selectedParent, domainObject,
icon: 'icon-object', icon: 'icon-object',
title: 'Switch the way this telemetry is displayed', title: 'Switch the way this telemetry is displayed',
label: 'View type', label: 'View type',
options: viewOptions, options: viewOptions,
method: function (option) { method: (option) => {
openmct.objectViews.emit( this.#openmct.objectViews.emit(
'contextAction', `${CONTEXT_ACTION}:${keyString}`,
'switchViewType', CONTEXT_ACTIONS.SWITCH_VIEW_TYPE,
selectedItemContext, selectedItemContext,
option.value, option.value,
selection selection
@ -592,34 +628,36 @@ define(['lodash'], function (_) {
}; };
} }
} else if (selection.length > 1) { } else if (selection.length > 1) {
if (areAllViews('telemetry-view', 'layoutItem.type', selection)) { const domainObject = selectedParent;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
if (this.#areAllViews('telemetry-view', 'layoutItem.type', selection)) {
return { return {
control: 'menu', control: 'menu',
domainObject: selectedParent, domainObject,
icon: 'icon-object', icon: 'icon-object',
title: 'Merge into a telemetry table or plot', title: 'Merge into a telemetry table or plot',
label: 'View type', label: 'View type',
options: APPLICABLE_VIEWS['telemetry-view-multi'], options: APPLICABLE_VIEWS['telemetry-view-multi'],
method: function (option) { method: (option) => {
openmct.objectViews.emit( this.#openmct.objectViews.emit(
'contextAction', `${CONTEXT_ACTION}:${keyString}`,
'mergeMultipleTelemetryViews', CONTEXT_ACTIONS.MERGE_MULTIPLE_TELEMETRY_VIEWS,
selection, selection,
option.value option.value
); );
} }
}; };
} else if (areAllViews('telemetry.plot.overlay', 'item.type', selection)) { } else if (this.#areAllViews('telemetry.plot.overlay', 'item.type', selection)) {
return { return {
control: 'menu', control: 'menu',
domainObject: selectedParent, domainObject,
icon: 'icon-object', icon: 'icon-object',
title: 'Merge into a stacked plot', title: 'Merge into a stacked plot',
options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'], options: APPLICABLE_VIEWS['telemetry.plot.overlay-multi'],
method: function (option) { method: (option) => {
openmct.objectViews.emit( this.#openmct.objectViews.emit(
'contextAction', `${CONTEXT_ACTION}:${keyString}`,
'mergeMultipleOverlayPlots', CONTEXT_ACTIONS.MERGE_MULTIPLE_OVERLAY_PLOTS,
selection, selection,
option.value option.value
); );
@ -629,7 +667,7 @@ define(['lodash'], function (_) {
} }
} }
function getToggleGridButton(selection, selectionPath) { #getToggleGridButton(selection, selectionPath) {
const ICON_GRID_SHOW = 'icon-grid-on'; const ICON_GRID_SHOW = 'icon-grid-on';
const ICON_GRID_HIDE = 'icon-grid-off'; const ICON_GRID_HIDE = 'icon-grid-off';
@ -641,12 +679,18 @@ define(['lodash'], function (_) {
displayLayoutContext = selectionPath[1].context; displayLayoutContext = selectionPath[1].context;
} }
const domainObject = displayLayoutContext.item;
const keyString = this.#openmct.objects.makeKeyString(domainObject.identifier);
return { return {
control: 'button', control: 'button',
domainObject: displayLayoutContext.item, domainObject,
icon: ICON_GRID_SHOW, icon: ICON_GRID_SHOW,
method: function () { method: () => {
openmct.objectViews.emit('contextAction', 'toggleGrid'); this.#openmct.objectViews.emit(
`${CONTEXT_ACTION}:${keyString}`,
CONTEXT_ACTIONS.TOGGLE_GRID
);
this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW; this.icon = this.icon === ICON_GRID_SHOW ? ICON_GRID_HIDE : ICON_GRID_SHOW;
}, },
@ -654,30 +698,37 @@ define(['lodash'], function (_) {
}; };
} }
function getSeparator() { #getSeparator() {
return { return {
control: 'separator' control: 'separator'
}; };
} }
function isMainLayoutSelected(selectionPath) { #isMainLayoutSelected(selectionPath) {
let selectedObject = selectionPath[0].context.item; let selectedObject = selectionPath[0].context.item;
return ( return (
selectedObject && selectedObject && selectedObject.type === 'layout' && !selectionPath[0].context.layoutItem
selectedObject.type === 'layout' &&
!selectionPath[0].context.layoutItem
); );
} }
function showForm(formStructure, name, selectionPath) { #showForm(formStructure, name, selection) {
openmct.forms.showForm(formStructure).then((changes) => { const domainObject = selection[0][0].context.item;
openmct.objectViews.emit('contextAction', 'addElement', name, changes); 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
);
}); });
} }
if (isMainLayoutSelected(selectedObjects[0])) { toolbar(selectedObjects) {
return [getToggleGridButton(selectedObjects), getAddButton(selectedObjects)]; if (this.#isMainLayoutSelected(selectedObjects[0])) {
return [this.#getToggleGridButton(selectedObjects), this.#getAddButton(selectedObjects)];
} }
let toolbar = { let toolbar = {
@ -705,48 +756,47 @@ define(['lodash'], function (_) {
} }
if (layoutItem.type === 'subobject-view') { if (layoutItem.type === 'subobject-view') {
if ( if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') {
toolbar['add-menu'].length === 0 && toolbar['add-menu'] = [this.#getAddButton(selectedObjects, selectionPath)];
selectionPath[0].context.item.type === 'layout'
) {
toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)];
} }
if (toolbar['toggle-frame'].length === 0) { if (toolbar['toggle-frame'].length === 0) {
toolbar['toggle-frame'] = [getToggleFrameButton(selectedParent, selectedObjects)]; toolbar['toggle-frame'] = [this.#getToggleFrameButton(selectedParent, selectedObjects)];
} }
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath, selectedObjects),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects), this.#getHeightInput(selectedParent, selectedObjects),
getWidthInput(selectedParent, selectedObjects) this.#getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
if (toolbar.viewSwitcher.length === 0) { if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [ toolbar.viewSwitcher = [
getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)
]; ];
} }
} else if (layoutItem.type === 'telemetry-view') { } else if (layoutItem.type === 'telemetry-view') {
if (toolbar['display-mode'].length === 0) { if (toolbar['display-mode'].length === 0) {
toolbar['display-mode'] = [getDisplayModeMenu(selectedParent, selectedObjects)]; toolbar['display-mode'] = [this.#getDisplayModeMenu(selectedParent, selectedObjects)];
} }
if (toolbar['telemetry-value'].length === 0) { if (toolbar['telemetry-value'].length === 0) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)]; toolbar['telemetry-value'] = [
this.#getTelemetryValueMenu(selectionPath, selectedObjects)
];
} }
if (toolbar['unit-toggle'].length === 0) { if (toolbar['unit-toggle'].length === 0) {
let toggleUnitsButton = getToggleUnitsButton(selectedParent, selectedObjects); let toggleUnitsButton = this.#getToggleUnitsButton(selectedParent, selectedObjects);
if (toggleUnitsButton) { if (toggleUnitsButton) {
toolbar['unit-toggle'] = [toggleUnitsButton]; toolbar['unit-toggle'] = [toggleUnitsButton];
} }
@ -754,98 +804,98 @@ define(['lodash'], function (_) {
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath, selectedObjects),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects), this.#getHeightInput(selectedParent, selectedObjects),
getWidthInput(selectedParent, selectedObjects) this.#getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
if (toolbar.viewSwitcher.length === 0) { if (toolbar.viewSwitcher.length === 0) {
toolbar.viewSwitcher = [ toolbar.viewSwitcher = [
getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects) this.#getViewSwitcherMenu(selectedParent, selectionPath, selectedObjects)
]; ];
} }
} else if (layoutItem.type === 'text-view') { } else if (layoutItem.type === 'text-view') {
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath, selectedObjects),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects), this.#getHeightInput(selectedParent, selectedObjects),
getWidthInput(selectedParent, selectedObjects) this.#getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.text.length === 0) { if (toolbar.text.length === 0) {
toolbar.text = [getTextButton(selectedParent, selectedObjects)]; toolbar.text = [this.#getTextButton(selectedParent, selectedObjects)];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
} else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') { } else if (layoutItem.type === 'box-view' || layoutItem.type === 'ellipse-view') {
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath, selectedObjects),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects), this.#getHeightInput(selectedParent, selectedObjects),
getWidthInput(selectedParent, selectedObjects) this.#getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
} else if (layoutItem.type === 'image-view') { } else if (layoutItem.type === 'image-view') {
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath, selectedObjects),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getHeightInput(selectedParent, selectedObjects), this.#getHeightInput(selectedParent, selectedObjects),
getWidthInput(selectedParent, selectedObjects) this.#getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
} else if (layoutItem.type === 'line-view') { } else if (layoutItem.type === 'line-view') {
if (toolbar.position.length === 0) { if (toolbar.position.length === 0) {
toolbar.position = [ toolbar.position = [
getStackOrder(selectedParent, selectionPath), this.#getStackOrder(selectedParent, selectionPath),
getSeparator(), this.#getSeparator(),
getXInput(selectedParent, selectedObjects), this.#getXInput(selectedParent, selectedObjects),
getYInput(selectedParent, selectedObjects), this.#getYInput(selectedParent, selectedObjects),
getX2Input(selectedParent, selectedObjects), this.#getX2Input(selectedParent, selectedObjects),
getY2Input(selectedParent, selectedObjects) this.#getY2Input(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [this.#getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }
} }
if (toolbar.duplicate.length === 0) { if (toolbar.duplicate.length === 0) {
toolbar.duplicate = [ toolbar.duplicate = [
getDuplicateButton(selectedParent, selectionPath, selectedObjects) this.#getDuplicateButton(selectedParent, selectionPath, selectedObjects)
]; ];
} }
if (toolbar['toggle-grid'].length === 0) { if (toolbar['toggle-grid'].length === 0) {
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)]; toolbar['toggle-grid'] = [this.#getToggleGridButton(selectedObjects, selectionPath)];
} }
}); });
@ -859,7 +909,7 @@ define(['lodash'], function (_) {
accumulator.push(group); accumulator.push(group);
if (index < toolbarArray.length - 1) { if (index < toolbarArray.length - 1) {
accumulator.push(getSeparator()); accumulator.push(this.#getSeparator());
} }
} }
@ -867,8 +917,4 @@ define(['lodash'], function (_) {
}, []) }, [])
); );
} }
}; }
}
return DisplayLayoutToolbar;
});

View File

@ -33,6 +33,9 @@
class="c-box-view u-style-receiver js-style-receiver" class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
role="application"
aria-roledescription="draggable box"
aria-label="Box"
></div> ></div>
</template> </template>
</layout-frame> </layout-frame>

View File

@ -36,6 +36,8 @@
:grid-size="gridSize" :grid-size="gridSize"
:show-grid="showGrid" :show-grid="showGrid"
:grid-dimensions="gridDimensions" :grid-dimensions="gridDimensions"
:aria-label="`${domainObject.name} Layout Grid`"
:aria-hidden="showGrid ? 'false' : 'true'"
/> />
<div <div
v-if="shouldDisplayLayoutDimensions" v-if="shouldDisplayLayoutDimensions"

View File

@ -20,7 +20,14 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="l-layout__grid-holder" :class="{ 'c-grid': showGrid }"> <div
class="l-layout__grid-holder"
:class="{ 'c-grid': showGrid }"
role="grid"
:aria-label="'Layout Grid'"
:aria-hidden="showGrid ? 'false' : 'true'"
:aria-live="showGrid ? 'polite' : 'off'"
>
<div <div
v-if="gridSize[0] >= 3" v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x" class="c-grid__x l-grid l-grid-x"

View File

@ -33,6 +33,9 @@
class="c-ellipse-view u-style-receiver js-style-receiver" class="c-ellipse-view u-style-receiver js-style-receiver"
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
role="application"
aria-roledescription="draggable ellipse"
aria-label="Ellipse"
></div> ></div>
</template> </template>
</layout-frame> </layout-frame>

View File

@ -30,13 +30,18 @@
:style="style" :style="style"
> >
<slot name="content"></slot> <slot name="content"></slot>
<div class="c-frame__move-bar" @mousedown.left="startMove($event)"></div> <div
class="c-frame__move-bar"
:aria-label="`Move ${typeName} Frame`"
@mousedown.left="startMove($event)"
></div>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash'; import _ from 'lodash';
import DRAWING_OBJECT_TYPES from '../DrawingObjectTypes';
import LayoutDrag from './../LayoutDrag'; import LayoutDrag from './../LayoutDrag';
export default { export default {
@ -58,6 +63,13 @@ export default {
}, },
emits: ['move', 'end-move'], emits: ['move', 'end-move'],
computed: { computed: {
typeName() {
const { type } = this.item;
if (DRAWING_OBJECT_TYPES[type]) {
return DRAWING_OBJECT_TYPES[type].name;
}
return 'Sub-object';
},
size() { size() {
let { width, height } = this.item; let { width, height } = this.item;

View File

@ -21,7 +21,14 @@
--> -->
<template> <template>
<div class="l-layout__frame c-frame no-frame c-line-view" :class="[styleClass]" :style="style"> <div
class="l-layout__frame c-frame no-frame c-line-view"
:class="[styleClass]"
:style="style"
aria-role="application"
aria-roledescription="draggable line"
aria-label="Line"
>
<svg width="100%" height="100%"> <svg width="100%" height="100%">
<line <line
v-bind="linePosition" v-bind="linePosition"

View File

@ -35,6 +35,9 @@
:data-font="item.font" :data-font="item.font"
:class="[styleClass]" :class="[styleClass]"
:style="style" :style="style"
role="application"
aria-roledescription="draggable text"
aria-label="Text"
> >
<div class="c-text-view__text">{{ item.text }}</div> <div class="c-text-view__text">{{ item.text }}</div>
</div> </div>

View File

@ -85,10 +85,9 @@ class DisplayLayoutView {
}; };
} }
contextAction() { contextAction(action, ...rest) {
const action = arguments[0]; if (this?.component.$refs.displayLayout[action]) {
if (this.component && this.component.$refs.displayLayout[action]) { this.component.$refs.displayLayout[action](...rest);
this.component.$refs.displayLayout[action](...Array.from(arguments).splice(1));
} }
} }

View File

@ -81,10 +81,9 @@ export default class FlexibleLayoutViewProvider {
type: 'flexible-layout' type: 'flexible-layout'
}; };
}, },
contextAction() { contextAction(action, ...args) {
const action = arguments[0]; if (component?.$refs?.flexibleLayout?.[action]) {
if (component && component.$refs.flexibleLayout[action]) { component.$refs.flexibleLayout[action](...args);
component.$refs.flexibleLayout[action](...Array.from(arguments).splice(1));
} }
}, },
onEditModeChange(isEditing) { onEditModeChange(isEditing) {

View File

@ -84,7 +84,8 @@ function ToolbarProvider(openmct) {
let containerIndex = containers.indexOf(container); let containerIndex = containers.indexOf(container);
let frame = container && container.frames.filter((f) => f.id === frameId)[0]; let frame = container && container.frames.filter((f) => f.id === frameId)[0];
let frameIndex = container && container.frames.indexOf(frame); let frameIndex = container && container.frames.indexOf(frame);
const primaryKeyString = openmct.objects.makeKeyString(primary.context.item.identifier);
const tertiaryKeyString = openmct.objects.makeKeyString(tertiary.context.item.identifier);
deleteFrame = { deleteFrame = {
control: 'button', control: 'button',
domainObject: primary.context.item, domainObject: primary.context.item,
@ -98,7 +99,7 @@ function ToolbarProvider(openmct) {
emphasis: 'true', emphasis: 'true',
callback: function () { callback: function () {
openmct.objectViews.emit( openmct.objectViews.emit(
'contextAction', `contextAction:${primaryKeyString}`,
'deleteFrame', 'deleteFrame',
primary.context.frameId primary.context.frameId
); );
@ -135,11 +136,12 @@ function ToolbarProvider(openmct) {
} }
] ]
}; };
addContainer = { addContainer = {
control: 'button', control: 'button',
domainObject: tertiary.context.item, domainObject: tertiary.context.item,
method: function () { method: function (...args) {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments); openmct.objectViews.emit(`contextAction:${tertiaryKeyString}`, 'addContainer', ...args);
}, },
key: 'add', key: 'add',
icon: 'icon-plus-in-rect', icon: 'icon-plus-in-rect',
@ -185,11 +187,14 @@ function ToolbarProvider(openmct) {
title: 'Remove Container' title: 'Remove Container'
}; };
const domainObject = secondary.context.item;
const keyString = openmct.objects.makeKeyString(domainObject.identifier);
addContainer = { addContainer = {
control: 'button', control: 'button',
domainObject: secondary.context.item, domainObject,
method: function () { method: function (...args) {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments); openmct.objectViews.emit(`contextAction:${keyString}`, 'addContainer', ...args);
}, },
key: 'add', key: 'add',
icon: 'icon-plus-in-rect', icon: 'icon-plus-in-rect',
@ -200,11 +205,14 @@ function ToolbarProvider(openmct) {
return []; return [];
} }
const domainObject = primary.context.item;
const keyString = openmct.objects.makeKeyString(domainObject.identifier);
addContainer = { addContainer = {
control: 'button', control: 'button',
domainObject: primary.context.item, domainObject,
method: function () { method: function (...args) {
openmct.objectViews.emit('contextAction', 'addContainer', ...arguments); openmct.objectViews.emit(`contextAction:${keyString}`, 'addContainer', ...args);
}, },
key: 'add', key: 'add',
icon: 'icon-plus-in-rect', icon: 'icon-plus-in-rect',

View File

@ -34,9 +34,6 @@ import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import stalenessMixin from '@/ui/mixins/staleness-mixin'; import stalenessMixin from '@/ui/mixins/staleness-mixin';
export default { export default {
components: {
// IndependentTimeConductor
},
mixins: [stalenessMixin], mixins: [stalenessMixin],
inject: ['openmct'], inject: ['openmct'],
props: { props: {
@ -181,7 +178,9 @@ export default {
this.triggerUnsubscribeFromStaleness(this.domainObject); this.triggerUnsubscribeFromStaleness(this.domainObject);
this.openmct.objectViews.off('clearData', this.clearData); this.openmct.objectViews.off('clearData', this.clearData);
this.openmct.objectViews.off('contextAction', this.performContextAction); if (this.contextActionEvent) {
this.openmct.objectViews.off(this.contextActionEvent, this.performContextAction);
}
}, },
getStyleReceiver() { getStyleReceiver() {
let styleReceiver; let styleReceiver;
@ -301,8 +300,11 @@ export default {
); );
} }
this.contextActionEvent = `contextAction:${this.openmct.objects.makeKeyString(
this.domainObject.identifier
)}`;
this.openmct.objectViews.on('clearData', this.clearData); this.openmct.objectViews.on('clearData', this.clearData);
this.openmct.objectViews.on('contextAction', this.performContextAction); this.openmct.objectViews.on(this.contextActionEvent, this.performContextAction);
this.$nextTick(() => { this.$nextTick(() => {
this.updateStyle(this.styleRuleManager?.currentStyle); this.updateStyle(this.styleRuleManager?.currentStyle);
@ -473,9 +475,9 @@ export default {
} }
} }
}, },
performContextAction() { performContextAction(...args) {
if (this.currentView.contextAction) { if (this?.currentView?.contextAction) {
this.currentView.contextAction(...arguments); this.currentView.contextAction(...args);
} }
}, },
isEditingAllowed() { isEditingAllowed() {

View File

@ -31,15 +31,19 @@
{{ options.label }} {{ options.label }}
</div> </div>
</div> </div>
<div v-if="open" class="c-menu"> <div v-if="open" class="c-menu" role="menu">
<ul> <ul>
<li <li
v-for="(option, index) in options.options" v-for="(option, index) in options.options"
:key="index" :key="index"
:class="option.class" :class="option.class"
role="menuitem"
:aria-labelledby="`${option.name}-menuitem-label`"
@click="onClick(option)" @click="onClick(option)"
> >
<span :id="`${option.name}-menuitem-label`">
{{ option.name }} {{ option.name }}
</span>
</li> </li>
</ul> </ul>
</div> </div>