Compare commits
	
		
			13 Commits
		
	
	
		
			v3.0.0
			...
			release/3.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0b949d16f0 | ||
| 
						 | 
					1423c23297 | ||
| 
						 | 
					b043f26e49 | ||
| 
						 | 
					856d88597e | ||
| 
						 | 
					c3ac07ebaf | ||
| 
						 | 
					2002396d0e | ||
| 
						 | 
					7980abcb38 | ||
| 
						 | 
					dedfd3b6f7 | ||
| 
						 | 
					5b7b722ae8 | ||
| 
						 | 
					f5433c0d3b | ||
| 
						 | 
					5619994e83 | ||
| 
						 | 
					339640e0d6 | ||
| 
						 | 
					32b68cf0df | 
@@ -13,7 +13,7 @@ module.exports = {
 | 
			
		||||
  extends: [
 | 
			
		||||
    'eslint:recommended',
 | 
			
		||||
    'plugin:compat/recommended',
 | 
			
		||||
    'plugin:vue/recommended',
 | 
			
		||||
    'plugin:vue/vue3-recommended',
 | 
			
		||||
    'plugin:you-dont-need-lodash-underscore/compatible',
 | 
			
		||||
    'plugin:prettier/recommended'
 | 
			
		||||
  ],
 | 
			
		||||
@@ -28,6 +28,8 @@ module.exports = {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  rules: {
 | 
			
		||||
    'vue/no-deprecated-dollar-listeners-api': 'warn',
 | 
			
		||||
    'vue/no-deprecated-events-api': 'warn',
 | 
			
		||||
    'vue/no-v-for-template-key': 'off',
 | 
			
		||||
    'vue/no-v-for-template-key-on-child': 'error',
 | 
			
		||||
    'prettier/prettier': 'error',
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@
 | 
			
		||||
 * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
 | 
			
		||||
 * @property {string} [name] the desired name of the created domain object.
 | 
			
		||||
 * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
 | 
			
		||||
 * @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -65,7 +66,10 @@ const { expect } = require('@playwright/test');
 | 
			
		||||
 * @param {CreateObjectOptions} options
 | 
			
		||||
 * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
 | 
			
		||||
 */
 | 
			
		||||
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
 | 
			
		||||
async function createDomainObjectWithDefaults(
 | 
			
		||||
  page,
 | 
			
		||||
  { type, name, parent = 'mine', customParameters = {} }
 | 
			
		||||
) {
 | 
			
		||||
  if (!name) {
 | 
			
		||||
    name = `${type}:${genUuid()}`;
 | 
			
		||||
  }
 | 
			
		||||
@@ -94,6 +98,13 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
 | 
			
		||||
    await notesInput.fill(page.testNotes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If there are any further parameters, fill them in
 | 
			
		||||
  for (const [key, value] of Object.entries(customParameters)) {
 | 
			
		||||
    const input = page.locator(`form[name="mctForm"] ${key}`);
 | 
			
		||||
    await input.fill('');
 | 
			
		||||
    await input.fill(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Click OK button and wait for Navigate event
 | 
			
		||||
  await Promise.all([
 | 
			
		||||
    page.waitForLoadState(),
 | 
			
		||||
@@ -177,7 +188,7 @@ async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
 | 
			
		||||
  await page.click(`li:text("Plan")`);
 | 
			
		||||
 | 
			
		||||
  // Modify the name input field of the domain object to accept 'name'
 | 
			
		||||
  const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
 | 
			
		||||
  const nameInput = page.getByLabel('Title', { exact: true });
 | 
			
		||||
  await nameInput.fill('');
 | 
			
		||||
  await nameInput.fill(name);
 | 
			
		||||
 | 
			
		||||
@@ -410,8 +421,18 @@ async function setEndOffset(page, offset) {
 | 
			
		||||
  await setTimeConductorOffset(page, offset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor bounds in fixed time mode
 | 
			
		||||
 *
 | 
			
		||||
 * NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
 | 
			
		||||
 * navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {string} startDate
 | 
			
		||||
 * @param {string} endDate
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeConductorBounds(page, startDate, endDate) {
 | 
			
		||||
  // Bring up the time conductor popup
 | 
			
		||||
  expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
 | 
			
		||||
  await page.click('.l-shell__time-conductor.c-compact-tc');
 | 
			
		||||
 | 
			
		||||
  await setTimeBounds(page, startDate, endDate);
 | 
			
		||||
@@ -419,20 +440,31 @@ async function setTimeConductorBounds(page, startDate, endDate) {
 | 
			
		||||
  await page.keyboard.press('Enter');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the independent time conductor bounds in fixed time mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {string} startDate
 | 
			
		||||
 * @param {string} endDate
 | 
			
		||||
 */
 | 
			
		||||
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
 | 
			
		||||
  // Activate Independent Time Conductor in Fixed Time Mode
 | 
			
		||||
  await page.getByRole('switch').click();
 | 
			
		||||
 | 
			
		||||
  // Bring up the time conductor popup
 | 
			
		||||
  await page.click('.c-conductor-holder--compact .c-compact-tc');
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.itc-popout')).toBeVisible();
 | 
			
		||||
  await expect(page.locator('.itc-popout')).toBeInViewport();
 | 
			
		||||
 | 
			
		||||
  await setTimeBounds(page, startDate, endDate);
 | 
			
		||||
 | 
			
		||||
  await page.keyboard.press('Enter');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the bounds of the visible conductor in fixed time mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {string} startDate
 | 
			
		||||
 * @param {string} endDate
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeBounds(page, startDate, endDate) {
 | 
			
		||||
  if (startDate) {
 | 
			
		||||
    // Fill start time
 | 
			
		||||
@@ -549,6 +581,21 @@ async function getCanvasPixels(page, canvasSelector) {
 | 
			
		||||
  return getTelemValuePromise;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {string} myItemsFolderName
 | 
			
		||||
 * @param {string} url
 | 
			
		||||
 * @param {string} newName
 | 
			
		||||
 */
 | 
			
		||||
async function renameObjectFromContextMenu(page, url, newName) {
 | 
			
		||||
  await openObjectTreeContextMenu(page, url);
 | 
			
		||||
  await page.click('li:text("Edit Properties")');
 | 
			
		||||
  const nameInput = page.getByLabel('Title', { exact: true });
 | 
			
		||||
  await nameInput.fill('');
 | 
			
		||||
  await nameInput.fill(newName);
 | 
			
		||||
  await page.click('[aria-label="Save"]');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
module.exports = {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
@@ -567,5 +614,6 @@ module.exports = {
 | 
			
		||||
  setTimeConductorBounds,
 | 
			
		||||
  setIndependentTimeConductorBounds,
 | 
			
		||||
  selectInspectorTab,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
  waitForPlotsToRender,
 | 
			
		||||
  renameObjectFromContextMenu
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -260,6 +260,7 @@ test.describe('Display Layout', () => {
 | 
			
		||||
  test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    await setFixedTimeMode(page);
 | 
			
		||||
    // Create another Sine Wave Generator
 | 
			
		||||
    const anotherSineWaveObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator'
 | 
			
		||||
@@ -316,10 +317,20 @@ test.describe('Display Layout', () => {
 | 
			
		||||
 | 
			
		||||
    // wait for annotations requests to be batched and requested
 | 
			
		||||
    await page.waitForLoadState('networkidle');
 | 
			
		||||
 | 
			
		||||
    // Network requests for the composite telemetry with multiple items should be:
 | 
			
		||||
    // 1.  a single batched request for annotations
 | 
			
		||||
    expect(networkRequests.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
    await setRealTimeMode(page);
 | 
			
		||||
    networkRequests = [];
 | 
			
		||||
 | 
			
		||||
    await page.reload();
 | 
			
		||||
 | 
			
		||||
    // wait for annotations to not load (if we have any, we've got a problem)
 | 
			
		||||
    await page.waitForLoadState('networkidle');
 | 
			
		||||
 | 
			
		||||
    // In real time mode, we don't fetch annotations at all
 | 
			
		||||
    expect(networkRequests.length).toBe(0);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,10 @@ const {
 | 
			
		||||
test.describe('Flexible Layout', () => {
 | 
			
		||||
  let sineWaveObject;
 | 
			
		||||
  let clockObject;
 | 
			
		||||
  let treePane;
 | 
			
		||||
  let sineWaveGeneratorTreeItem;
 | 
			
		||||
  let clockTreeItem;
 | 
			
		||||
  let flexibleLayout;
 | 
			
		||||
  test.beforeEach(async ({ page }) => {
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
 | 
			
		||||
@@ -41,23 +45,27 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
    clockObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Clock'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create a Flexible Layout
 | 
			
		||||
    flexibleLayout = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Flexible Layout'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Define the Sine Wave Generator and Clock tree items
 | 
			
		||||
    treePane = page.getByRole('tree', {
 | 
			
		||||
      name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(sineWaveObject.name)
 | 
			
		||||
    });
 | 
			
		||||
    clockTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(clockObject.name)
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
      name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(sineWaveObject.name)
 | 
			
		||||
    });
 | 
			
		||||
    const clockTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(clockObject.name)
 | 
			
		||||
    });
 | 
			
		||||
    // Create a Flexible Layout
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Flexible Layout'
 | 
			
		||||
    });
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
    // Edit Flexible Layout
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
 | 
			
		||||
@@ -78,19 +86,79 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
    dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
 | 
			
		||||
    await expect(dragWrapper).toHaveAttribute('draggable', 'false');
 | 
			
		||||
  });
 | 
			
		||||
  test('changing toolbar settings in edit mode is immediately reflected and persists upon save', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/6942'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
 | 
			
		||||
    // Expand the 'My Items' folder in the left tree
 | 
			
		||||
    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
 | 
			
		||||
    // Add the Sine Wave Generator and Clock to the Flexible Layout
 | 
			
		||||
    await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
 | 
			
		||||
    await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
 | 
			
		||||
 | 
			
		||||
    // Click on the first frame to select it
 | 
			
		||||
    await page.locator('.c-fl-container__frame').first().click();
 | 
			
		||||
    await expect(page.locator('.c-fl-container__frame > .c-frame').first()).toHaveAttribute(
 | 
			
		||||
      's-selected',
 | 
			
		||||
      ''
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Assert the toolbar is visible
 | 
			
		||||
    await expect(page.locator('.c-toolbar')).toBeInViewport();
 | 
			
		||||
 | 
			
		||||
    // Assert the layout is in columns orientation
 | 
			
		||||
    expect(await page.locator('.c-fl--rows').count()).toEqual(0);
 | 
			
		||||
 | 
			
		||||
    // Change the layout to rows orientation
 | 
			
		||||
    await page.getByTitle('Columns layout').click();
 | 
			
		||||
 | 
			
		||||
    // Assert the layout is in rows orientation
 | 
			
		||||
    expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
 | 
			
		||||
 | 
			
		||||
    // Assert the frame of the first item is visible
 | 
			
		||||
    await expect(page.locator('.c-so-view').first()).not.toHaveClass(/c-so-view--no-frame/);
 | 
			
		||||
 | 
			
		||||
    // Hide the frame of the first item
 | 
			
		||||
    await page.getByTitle('Frame visible').click();
 | 
			
		||||
 | 
			
		||||
    // Assert the frame is hidden
 | 
			
		||||
    await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
 | 
			
		||||
 | 
			
		||||
    // Assert there are 2 containers
 | 
			
		||||
    expect(await page.locator('.c-fl-container').count()).toEqual(2);
 | 
			
		||||
 | 
			
		||||
    // Add a container
 | 
			
		||||
    await page.getByTitle('Add Container').click();
 | 
			
		||||
 | 
			
		||||
    // Assert there are 3 containers
 | 
			
		||||
    expect(await page.locator('.c-fl-container').count()).toEqual(3);
 | 
			
		||||
 | 
			
		||||
    // Save Flexible Layout
 | 
			
		||||
    await page.locator('button[title="Save"]').click();
 | 
			
		||||
    await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
 | 
			
		||||
 | 
			
		||||
    // Nav away and back
 | 
			
		||||
    await page.goto(sineWaveObject.url);
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
 | 
			
		||||
    // Wait for the first frame to be visible so we know the layout has loaded
 | 
			
		||||
    await expect(page.locator('.c-fl-container').nth(0)).toBeInViewport();
 | 
			
		||||
 | 
			
		||||
    // Assert the settings have persisted
 | 
			
		||||
    expect(await page.locator('.c-fl-container').count()).toEqual(3);
 | 
			
		||||
    expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
 | 
			
		||||
    await expect(page.locator('.c-so-view').first()).toHaveClass(/c-so-view--no-frame/);
 | 
			
		||||
  });
 | 
			
		||||
  test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
      name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(sineWaveObject.name)
 | 
			
		||||
    });
 | 
			
		||||
    // Create a Display Layout
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Flexible Layout'
 | 
			
		||||
    });
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
    // Edit Flexible Layout
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
 | 
			
		||||
@@ -121,17 +189,7 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/3117'
 | 
			
		||||
    });
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
      name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(sineWaveObject.name)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create a Flexible Layout
 | 
			
		||||
    const flexibleLayout = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Flexible Layout'
 | 
			
		||||
    });
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
    // Edit Flexible Layout
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
 | 
			
		||||
@@ -167,19 +225,13 @@ test.describe('Flexible Layout', () => {
 | 
			
		||||
    const exampleImageryObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Example Imagery'
 | 
			
		||||
    });
 | 
			
		||||
    // Create a Flexible Layout
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Flexible Layout'
 | 
			
		||||
    });
 | 
			
		||||
    // Edit Display Layout
 | 
			
		||||
 | 
			
		||||
    await page.goto(flexibleLayout.url);
 | 
			
		||||
    // Edit Flexible Layout
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
 | 
			
		||||
    // Expand the 'My Items' folder in the left tree
 | 
			
		||||
    await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
 | 
			
		||||
    // Add the Sine Wave Generator to the Flexible Layout and save changes
 | 
			
		||||
    const treePane = page.getByRole('tree', {
 | 
			
		||||
      name: 'Main Tree'
 | 
			
		||||
    });
 | 
			
		||||
    const exampleImageryTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(exampleImageryObject.name)
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -79,25 +79,25 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
    // Test independent fixed time with global fixed time
 | 
			
		||||
    // flip on independent time conductor
 | 
			
		||||
    await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
 | 
			
		||||
 | 
			
		||||
    // Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
 | 
			
		||||
    await expect(page.locator('#independentTCToggle')).toBeChecked();
 | 
			
		||||
    await expect(page.locator('.c-compact-tc').first()).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
 | 
			
		||||
    await page.getByRole('textbox', { name: 'Start date' }).fill('');
 | 
			
		||||
 | 
			
		||||
    await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
 | 
			
		||||
    await page.keyboard.press('Tab');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'Start time' }).fill('');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'Start time' }).type('01:01:00');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
 | 
			
		||||
    await page.keyboard.press('Tab');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End date' }).fill('');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End date' }).type('2021-12-30');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
 | 
			
		||||
    await page.keyboard.press('Tab');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End time' }).fill('');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End time' }).type('01:11:00');
 | 
			
		||||
    await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
 | 
			
		||||
    await page.keyboard.press('Tab');
 | 
			
		||||
    await page.keyboard.press('Enter');
 | 
			
		||||
    // expect(await page.getByRole('button', { name: 'Submit time bounds' }).isEnabled()).toBe(true);
 | 
			
		||||
    // await page.getByRole('button', { name: 'Submit time bounds' }).click();
 | 
			
		||||
 | 
			
		||||
    // check image date
 | 
			
		||||
    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    // flip it off
 | 
			
		||||
    await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
 | 
			
		||||
@@ -106,9 +106,12 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
 | 
			
		||||
    // Test independent fixed time with global realtime
 | 
			
		||||
    await setRealTimeMode(page);
 | 
			
		||||
    await expect(
 | 
			
		||||
      page.getByRole('switch', { name: 'Enable Independent Time Conductor' })
 | 
			
		||||
    ).toBeEnabled();
 | 
			
		||||
    await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
 | 
			
		||||
    // check image date to be in the past
 | 
			
		||||
    await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
 | 
			
		||||
    // flip it off
 | 
			
		||||
    await page.getByRole('switch', { name: 'Disable Independent Time Conductor' }).click();
 | 
			
		||||
    // timestamp shouldn't be in the past anymore
 | 
			
		||||
 
 | 
			
		||||
@@ -29,10 +29,11 @@ const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  setRealTimeMode,
 | 
			
		||||
  setFixedTimeMode,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
  waitForPlotsToRender,
 | 
			
		||||
  selectInspectorTab
 | 
			
		||||
} = require('../../../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
test.describe('Plot Tagging', () => {
 | 
			
		||||
  /**
 | 
			
		||||
   * Given a canvas and a set of points, tags the points on the canvas.
 | 
			
		||||
   * @param {import('@playwright/test').Page} page
 | 
			
		||||
@@ -41,7 +42,7 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
   * @param {Number} yEnd a telemetry item with a plot
 | 
			
		||||
   * @returns {Promise}
 | 
			
		||||
   */
 | 
			
		||||
  async function createTags({ page, canvas, xEnd, yEnd }) {
 | 
			
		||||
  async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
 | 
			
		||||
    //Alt+Shift Drag Start to select some points to tag
 | 
			
		||||
@@ -90,15 +91,17 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
    //Wait for canvas to stabilize.
 | 
			
		||||
    await waitForPlotsToRender(page);
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to stablize.
 | 
			
		||||
    await expect(canvas).toBeInViewport();
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
 | 
			
		||||
    // click on the tagged plot point
 | 
			
		||||
    await canvas.click({
 | 
			
		||||
      position: {
 | 
			
		||||
        x: 325,
 | 
			
		||||
        y: 377
 | 
			
		||||
        x: 100,
 | 
			
		||||
        y: 100
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -146,7 +149,10 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
    // wait for plots to load
 | 
			
		||||
    await waitForPlotsToRender(page);
 | 
			
		||||
 | 
			
		||||
    await page.getByText('Annotations').click();
 | 
			
		||||
    await expect(page.getByRole('tab', { name: 'Annotations' })).not.toHaveClass(/is-current/);
 | 
			
		||||
    await selectInspectorTab(page, 'Annotations');
 | 
			
		||||
    await expect(page.getByRole('tab', { name: 'Annotations' })).toHaveClass(/is-current/);
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
@@ -171,8 +177,6 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/6822'
 | 
			
		||||
    });
 | 
			
		||||
    //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | 
			
		||||
    test.slow();
 | 
			
		||||
 | 
			
		||||
    const overlayPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Overlay Plot'
 | 
			
		||||
@@ -181,13 +185,19 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
    const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Alpha Sine Wave',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
      parent: overlayPlot.uuid,
 | 
			
		||||
      customParameters: {
 | 
			
		||||
        '[aria-label="Data Rate (hz)"]': '0.01'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Beta Sine Wave',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
      parent: overlayPlot.uuid,
 | 
			
		||||
      customParameters: {
 | 
			
		||||
        '[aria-label="Data Rate (hz)"]': '0.02'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(overlayPlot.url);
 | 
			
		||||
@@ -200,9 +210,7 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
 | 
			
		||||
    await createTags({
 | 
			
		||||
      page,
 | 
			
		||||
      canvas,
 | 
			
		||||
      xEnd: 700,
 | 
			
		||||
      yEnd: 480
 | 
			
		||||
      canvas
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await setFixedTimeMode(page);
 | 
			
		||||
@@ -232,15 +240,15 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
 | 
			
		||||
  test('Tags work with Plot View of telemetry items', async ({ page }) => {
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator'
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      customParameters: {
 | 
			
		||||
        '[aria-label="Data Rate (hz)"]': '0.01'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
    await createTags({
 | 
			
		||||
      page,
 | 
			
		||||
      canvas,
 | 
			
		||||
      xEnd: 700,
 | 
			
		||||
      yEnd: 480
 | 
			
		||||
      canvas
 | 
			
		||||
    });
 | 
			
		||||
    await basicTagsTests(page);
 | 
			
		||||
  });
 | 
			
		||||
@@ -253,13 +261,19 @@ test.describe.fixme('Plot Tagging', () => {
 | 
			
		||||
    const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Alpha Sine Wave',
 | 
			
		||||
      parent: stackedPlot.uuid
 | 
			
		||||
      parent: stackedPlot.uuid,
 | 
			
		||||
      customParameters: {
 | 
			
		||||
        '[aria-label="Data Rate (hz)"]': '0.01'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Beta Sine Wave',
 | 
			
		||||
      parent: stackedPlot.uuid
 | 
			
		||||
      parent: stackedPlot.uuid,
 | 
			
		||||
      customParameters: {
 | 
			
		||||
        '[aria-label="Data Rate (hz)"]': '0.02'
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(stackedPlot.url);
 | 
			
		||||
 
 | 
			
		||||
@@ -59,59 +59,57 @@ test.describe('Recent Objects', () => {
 | 
			
		||||
    await page.mouse.move(0, 100);
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
  });
 | 
			
		||||
  test.fixme(
 | 
			
		||||
    'Navigated objects show up in recents, object renames and deletions are reflected',
 | 
			
		||||
    async ({ page }) => {
 | 
			
		||||
      test.info().annotations.push({
 | 
			
		||||
        type: 'issue',
 | 
			
		||||
        description: 'https://github.com/nasa/openmct/issues/6818'
 | 
			
		||||
      });
 | 
			
		||||
  test('Navigated objects show up in recents, object renames and deletions are reflected', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/6818'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
      // Verify that both created objects appear in the list and are in the correct order
 | 
			
		||||
      await assertInitialRecentObjectsListState();
 | 
			
		||||
    // Verify that both created objects appear in the list and are in the correct order
 | 
			
		||||
    await assertInitialRecentObjectsListState();
 | 
			
		||||
 | 
			
		||||
      // Navigate to the folder by clicking on the main object name in the recent objects list item
 | 
			
		||||
      await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
 | 
			
		||||
      await page.waitForURL(`**/${folderA.uuid}?*`);
 | 
			
		||||
      expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
 | 
			
		||||
    // Navigate to the folder by clicking on the main object name in the recent objects list item
 | 
			
		||||
    await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
 | 
			
		||||
    await page.waitForURL(`**/${folderA.uuid}?*`);
 | 
			
		||||
    expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
      // Rename
 | 
			
		||||
      folderA.name = `${folderA.name}-NEW!`;
 | 
			
		||||
      await page.locator('.l-browse-bar__object-name').fill('');
 | 
			
		||||
      await page.locator('.l-browse-bar__object-name').fill(folderA.name);
 | 
			
		||||
      await page.keyboard.press('Enter');
 | 
			
		||||
    // Rename
 | 
			
		||||
    folderA.name = `${folderA.name}-NEW!`;
 | 
			
		||||
    await page.locator('.l-browse-bar__object-name').fill('');
 | 
			
		||||
    await page.locator('.l-browse-bar__object-name').fill(folderA.name);
 | 
			
		||||
    await page.keyboard.press('Enter');
 | 
			
		||||
 | 
			
		||||
      // Verify rename has been applied in recent objects list item and objects paths
 | 
			
		||||
      expect(
 | 
			
		||||
        await page
 | 
			
		||||
          .getByRole('navigation', {
 | 
			
		||||
            name: clock.name
 | 
			
		||||
          })
 | 
			
		||||
          .locator('a')
 | 
			
		||||
          .filter({
 | 
			
		||||
            hasText: folderA.name
 | 
			
		||||
          })
 | 
			
		||||
          .count()
 | 
			
		||||
      ).toBeGreaterThan(0);
 | 
			
		||||
      expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
      // Delete
 | 
			
		||||
      await page.click('button[title="Show selected item in tree"]');
 | 
			
		||||
      // Delete the folder via the left tree pane treeitem context menu
 | 
			
		||||
    // Verify rename has been applied in recent objects list item and objects paths
 | 
			
		||||
    expect(
 | 
			
		||||
      await page
 | 
			
		||||
        .getByRole('treeitem', { name: new RegExp(folderA.name) })
 | 
			
		||||
        .getByRole('navigation', {
 | 
			
		||||
          name: clock.name
 | 
			
		||||
        })
 | 
			
		||||
        .locator('a')
 | 
			
		||||
        .click({
 | 
			
		||||
          button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
      await page.getByRole('menuitem', { name: /Remove/ }).click();
 | 
			
		||||
      await page.getByRole('button', { name: 'OK' }).click();
 | 
			
		||||
        .filter({
 | 
			
		||||
          hasText: folderA.name
 | 
			
		||||
        })
 | 
			
		||||
        .count()
 | 
			
		||||
    ).toBeGreaterThan(0);
 | 
			
		||||
    expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
      // Verify that the folder and clock are no longer in the recent objects list
 | 
			
		||||
      await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
 | 
			
		||||
      await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
    await page.click('button[title="Show selected item in tree"]');
 | 
			
		||||
    // Delete the folder via the left tree pane treeitem context menu
 | 
			
		||||
    await page
 | 
			
		||||
      .getByRole('treeitem', { name: new RegExp(folderA.name) })
 | 
			
		||||
      .locator('a')
 | 
			
		||||
      .click({
 | 
			
		||||
        button: 'right'
 | 
			
		||||
      });
 | 
			
		||||
    await page.getByRole('menuitem', { name: /Remove/ }).click();
 | 
			
		||||
    await page.getByRole('button', { name: 'OK' }).click();
 | 
			
		||||
 | 
			
		||||
    // Verify that the folder and clock are no longer in the recent objects list
 | 
			
		||||
    await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
 | 
			
		||||
    await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('Clicking on an object in the path of a recent object navigates to the object', async ({
 | 
			
		||||
    page,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								e2e/tests/functional/renaming.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								e2e/tests/functional/renaming.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2023, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests for renaming objects, and their global application effects.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../baseFixtures.js');
 | 
			
		||||
const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  renameObjectFromContextMenu
 | 
			
		||||
} = require('../../appActions.js');
 | 
			
		||||
 | 
			
		||||
test.describe('Renaming objects', () => {
 | 
			
		||||
  test.beforeEach(async ({ page }) => {
 | 
			
		||||
    // Go to baseURL
 | 
			
		||||
    await page.goto('./', { waitUntil: 'networkidle' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('When renaming objects, the browse bar and various components all update', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    const folder = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Folder'
 | 
			
		||||
    });
 | 
			
		||||
    // Create a new 'Clock' object with default settings
 | 
			
		||||
    const clock = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Clock',
 | 
			
		||||
      parent: folder.uuid
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Rename
 | 
			
		||||
    clock.name = `${clock.name}-NEW!`;
 | 
			
		||||
    await renameObjectFromContextMenu(page, clock.url, clock.name);
 | 
			
		||||
    // check inspector for new name
 | 
			
		||||
    const titleValue = await page
 | 
			
		||||
      .getByLabel('Title inspector properties')
 | 
			
		||||
      .getByLabel('inspector property value')
 | 
			
		||||
      .textContent();
 | 
			
		||||
    expect(titleValue).toBe(clock.name);
 | 
			
		||||
    // check browse bar for new name
 | 
			
		||||
    await expect(page.locator(`.l-browse-bar >> text=${clock.name}`)).toBeVisible();
 | 
			
		||||
    // check tree item for new name
 | 
			
		||||
    await expect(
 | 
			
		||||
      page.getByRole('listitem', {
 | 
			
		||||
        name: clock.name
 | 
			
		||||
      })
 | 
			
		||||
    ).toBeVisible();
 | 
			
		||||
    // check recent objects for new name
 | 
			
		||||
    await expect(
 | 
			
		||||
      page.getByRole('navigation', {
 | 
			
		||||
        name: clock.name
 | 
			
		||||
      })
 | 
			
		||||
    ).toBeVisible();
 | 
			
		||||
    // check title for new name
 | 
			
		||||
    const title = await page.title();
 | 
			
		||||
    expect(title).toBe(clock.name);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
const { test, expect } = require('../../pluginFixtures.js');
 | 
			
		||||
const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  openObjectTreeContextMenu
 | 
			
		||||
  renameObjectFromContextMenu
 | 
			
		||||
} = require('../../appActions.js');
 | 
			
		||||
 | 
			
		||||
test.describe('Main Tree', () => {
 | 
			
		||||
@@ -249,18 +249,3 @@ async function expandTreePaneItemByName(page, name) {
 | 
			
		||||
  });
 | 
			
		||||
  await treeItem.locator('.c-disclosure-triangle').click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {string} myItemsFolderName
 | 
			
		||||
 * @param {string} url
 | 
			
		||||
 * @param {string} newName
 | 
			
		||||
 */
 | 
			
		||||
async function renameObjectFromContextMenu(page, url, newName) {
 | 
			
		||||
  await openObjectTreeContextMenu(page, url);
 | 
			
		||||
  await page.click('li:text("Edit Properties")');
 | 
			
		||||
  const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
 | 
			
		||||
  await nameInput.fill('');
 | 
			
		||||
  await nameInput.fill(newName);
 | 
			
		||||
  await page.click('[aria-label="Save"]');
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										273
									
								
								e2e/tests/performance/tagging.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								e2e/tests/performance/tagging.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2023, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Tests to verify plot tagging performance.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../pluginFixtures');
 | 
			
		||||
const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  setRealTimeMode,
 | 
			
		||||
  setFixedTimeMode,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
} = require('../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe.fixme('Plot Tagging Performance', () => {
 | 
			
		||||
  /**
 | 
			
		||||
   * Given a canvas and a set of points, tags the points on the canvas.
 | 
			
		||||
   * @param {import('@playwright/test').Page} page
 | 
			
		||||
   * @param {HTMLCanvasElement} canvas a telemetry item with a plot
 | 
			
		||||
   * @param {Number} xEnd a telemetry item with a plot
 | 
			
		||||
   * @param {Number} yEnd a telemetry item with a plot
 | 
			
		||||
   * @returns {Promise}
 | 
			
		||||
   */
 | 
			
		||||
  async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) {
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
 | 
			
		||||
    //Alt+Shift Drag Start to select some points to tag
 | 
			
		||||
    await page.keyboard.down('Alt');
 | 
			
		||||
    await page.keyboard.down('Shift');
 | 
			
		||||
 | 
			
		||||
    await canvas.dragTo(canvas, {
 | 
			
		||||
      sourcePosition: {
 | 
			
		||||
        x: 1,
 | 
			
		||||
        y: 1
 | 
			
		||||
      },
 | 
			
		||||
      targetPosition: {
 | 
			
		||||
        x: xEnd,
 | 
			
		||||
        y: yEnd
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //Alt Drag End
 | 
			
		||||
    await page.keyboard.up('Alt');
 | 
			
		||||
    await page.keyboard.up('Shift');
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to stablize.
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
 | 
			
		||||
    // add some tags
 | 
			
		||||
    await page.getByText('Annotations').click();
 | 
			
		||||
    await page.getByRole('button', { name: /Add Tag/ }).click();
 | 
			
		||||
    await page.getByPlaceholder('Type to select tag').click();
 | 
			
		||||
    await page.getByText('Driving').click();
 | 
			
		||||
 | 
			
		||||
    await page.getByRole('button', { name: /Add Tag/ }).click();
 | 
			
		||||
    await page.getByPlaceholder('Type to select tag').click();
 | 
			
		||||
    await page.getByText('Science').click();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
 | 
			
		||||
   * @param {import('@playwright/test').Page} page
 | 
			
		||||
   * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
 | 
			
		||||
   * @returns {Promise}
 | 
			
		||||
   */
 | 
			
		||||
  async function testTelemetryItem(page, telemetryItem) {
 | 
			
		||||
    // Check that telemetry item also received the tag
 | 
			
		||||
    await page.goto(telemetryItem.url);
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to stablize.
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
 | 
			
		||||
    // click on the tagged plot point
 | 
			
		||||
    await canvas.click({
 | 
			
		||||
      position: {
 | 
			
		||||
        x: 100,
 | 
			
		||||
        y: 100
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('Science')).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('Driving')).toBeHidden();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Given a page, tests that tags are searchable, deletable, and persist across reloads.
 | 
			
		||||
   * @param {import('@playwright/test').Page} page
 | 
			
		||||
   * @returns {Promise}
 | 
			
		||||
   */
 | 
			
		||||
  async function basicTagsTests(page) {
 | 
			
		||||
    // Search for Driving
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
 | 
			
		||||
    // Clicking elsewhere should cause annotation selection to be cleared
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
 | 
			
		||||
    // click on the search result
 | 
			
		||||
    await page
 | 
			
		||||
      .getByRole('searchbox', { name: 'OpenMCT Search' })
 | 
			
		||||
      .getByText(/Sine Wave/)
 | 
			
		||||
      .first()
 | 
			
		||||
      .click();
 | 
			
		||||
 | 
			
		||||
    // Delete Driving
 | 
			
		||||
    await page.hover('[aria-label="Tag"]:has-text("Driving")');
 | 
			
		||||
    await page.locator('[aria-label="Remove tag Driving"]').click();
 | 
			
		||||
 | 
			
		||||
    // Search for Science
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
 | 
			
		||||
    await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science');
 | 
			
		||||
    await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling');
 | 
			
		||||
 | 
			
		||||
    // Search for Driving
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
 | 
			
		||||
    await expect(page.getByText('No results found')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    //Reload Page
 | 
			
		||||
    await page.reload({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
    // wait for plots to load
 | 
			
		||||
    await waitForPlotsToRender(page);
 | 
			
		||||
 | 
			
		||||
    await page.getByText('Annotations').click();
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
    // click on the tagged plot point
 | 
			
		||||
    await canvas.click({
 | 
			
		||||
      position: {
 | 
			
		||||
        x: 100,
 | 
			
		||||
        y: 100
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('Science')).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('Driving')).toBeHidden();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  test.beforeEach(async ({ page }) => {
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('Tags work with Overlay Plots', async ({ page }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/6822'
 | 
			
		||||
    });
 | 
			
		||||
    //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | 
			
		||||
    test.slow();
 | 
			
		||||
 | 
			
		||||
    const overlayPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Overlay Plot'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Alpha Sine Wave',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Beta Sine Wave',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(overlayPlot.url);
 | 
			
		||||
 | 
			
		||||
    let canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
    // Switch to real-time mode
 | 
			
		||||
    // Adding tags should pause the plot
 | 
			
		||||
    await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
    await createTags({
 | 
			
		||||
      page,
 | 
			
		||||
      canvas
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await setFixedTimeMode(page);
 | 
			
		||||
 | 
			
		||||
    await basicTagsTests(page);
 | 
			
		||||
    await testTelemetryItem(page, alphaSineWave);
 | 
			
		||||
 | 
			
		||||
    // set to real time mode
 | 
			
		||||
    await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
    // Search for Science
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
    await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
 | 
			
		||||
    // click on the search result
 | 
			
		||||
    await page
 | 
			
		||||
      .getByRole('searchbox', { name: 'OpenMCT Search' })
 | 
			
		||||
      .getByText('Alpha Sine Wave')
 | 
			
		||||
      .first()
 | 
			
		||||
      .click();
 | 
			
		||||
    // wait for plots to load
 | 
			
		||||
    await expect(page.locator('.js-series-data-loaded')).toBeVisible();
 | 
			
		||||
    // expect plot to be paused
 | 
			
		||||
    await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    await setFixedTimeMode(page);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('Tags work with Plot View of telemetry items', async ({ page }) => {
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator'
 | 
			
		||||
    });
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
    await createTags({
 | 
			
		||||
      page,
 | 
			
		||||
      canvas
 | 
			
		||||
    });
 | 
			
		||||
    await basicTagsTests(page);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('Tags work with Stacked Plots', async ({ page }) => {
 | 
			
		||||
    const stackedPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Stacked Plot'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Alpha Sine Wave',
 | 
			
		||||
      parent: stackedPlot.uuid
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Beta Sine Wave',
 | 
			
		||||
      parent: stackedPlot.uuid
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(stackedPlot.url);
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
    await createTags({
 | 
			
		||||
      page,
 | 
			
		||||
      canvas,
 | 
			
		||||
      xEnd: 700,
 | 
			
		||||
      yEnd: 215
 | 
			
		||||
    });
 | 
			
		||||
    await basicTagsTests(page);
 | 
			
		||||
    await testTelemetryItem(page, alphaSineWave);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "openmct",
 | 
			
		||||
  "version": "3.0.0",
 | 
			
		||||
  "version": "3.0.1",
 | 
			
		||||
  "description": "The Open MCT core platform",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/eslint-parser": "7.22.5",
 | 
			
		||||
@@ -81,7 +81,8 @@
 | 
			
		||||
    "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
 | 
			
		||||
    "start": "npx webpack serve --config ./.webpack/webpack.dev.js",
 | 
			
		||||
    "start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
 | 
			
		||||
    "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
 | 
			
		||||
    "lint": "eslint example src e2e --ext .js openmct.js --max-warnings=0 && eslint example src --ext .vue",
 | 
			
		||||
    "lint:spelling": "cspell \"**/*.{js,md,vue}\" --show-context --gitignore",
 | 
			
		||||
    "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
 | 
			
		||||
    "build:prod": "webpack --config ./.webpack/webpack.prod.js",
 | 
			
		||||
    "build:dev": "webpack --config ./.webpack/webpack.dev.js",
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,9 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
 | 
			
		||||
    <div class="c-form-row__label" :title="row.description">
 | 
			
		||||
    <label class="c-form-row__label" :title="row.description" :for="`form-${row.key}`">
 | 
			
		||||
      {{ row.name }}
 | 
			
		||||
    </div>
 | 
			
		||||
    </label>
 | 
			
		||||
    <div class="c-form-row__state-indicator" :class="reqClass"></div>
 | 
			
		||||
    <div v-if="row.control" ref="rowElement" class="c-form-row__controls"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <span class="form-control shell">
 | 
			
		||||
    <span class="field control" :class="model.cssClass">
 | 
			
		||||
      <input v-model="field" type="text" :size="model.size" @input="updateText()" />
 | 
			
		||||
      <input
 | 
			
		||||
        :id="`form-${model.key}`"
 | 
			
		||||
        v-model="field"
 | 
			
		||||
        :name="model.key"
 | 
			
		||||
        type="text"
 | 
			
		||||
        :size="model.size"
 | 
			
		||||
        @input="updateText()"
 | 
			
		||||
      />
 | 
			
		||||
    </span>
 | 
			
		||||
  </span>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -554,28 +554,34 @@ export default class ObjectAPI {
 | 
			
		||||
   */
 | 
			
		||||
  async getTelemetryPath(identifier, telemetryIdentifier) {
 | 
			
		||||
    const objectDetails = await this.get(identifier);
 | 
			
		||||
    const telemetryPath = [];
 | 
			
		||||
    if (objectDetails.composition && !['folder'].includes(objectDetails.type)) {
 | 
			
		||||
      let sourceTelemetry = objectDetails.composition[0];
 | 
			
		||||
    let telemetryPath = [];
 | 
			
		||||
    if (objectDetails?.type === 'folder') {
 | 
			
		||||
      return telemetryPath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let sourceTelemetry = null;
 | 
			
		||||
    if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
 | 
			
		||||
      sourceTelemetry = identifier;
 | 
			
		||||
    } else if (objectDetails.composition) {
 | 
			
		||||
      sourceTelemetry = objectDetails.composition[0];
 | 
			
		||||
      if (telemetryIdentifier) {
 | 
			
		||||
        sourceTelemetry = objectDetails.composition.find(
 | 
			
		||||
          (telemetrySource) =>
 | 
			
		||||
            this.makeKeyString(telemetrySource) === this.makeKeyString(telemetryIdentifier)
 | 
			
		||||
        sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
 | 
			
		||||
          utils.identifierEquals(telemetrySource, telemetryIdentifier)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      const compositionElement = await this.get(sourceTelemetry);
 | 
			
		||||
      if (!['yamcs.telemetry', 'generator'].includes(compositionElement.type)) {
 | 
			
		||||
        return telemetryPath;
 | 
			
		||||
      }
 | 
			
		||||
      const telemetryKey = compositionElement.identifier.key;
 | 
			
		||||
      const telemetryPathObjects = await this.getOriginalPath(telemetryKey);
 | 
			
		||||
      telemetryPathObjects.forEach((pathObject) => {
 | 
			
		||||
        if (pathObject.type === 'root') {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        telemetryPath.unshift(pathObject.name);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const compositionElement = await this.get(sourceTelemetry);
 | 
			
		||||
    if (!['yamcs.telemetry', 'generator', 'yamcs.aggregate'].includes(compositionElement.type)) {
 | 
			
		||||
      return telemetryPath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const telemetryPathObjects = await this.getOriginalPath(compositionElement.identifier);
 | 
			
		||||
    telemetryPath = telemetryPathObjects
 | 
			
		||||
      .reverse()
 | 
			
		||||
      .filter((pathObject) => pathObject.type !== 'root')
 | 
			
		||||
      .map((pathObject) => pathObject.name);
 | 
			
		||||
 | 
			
		||||
    return telemetryPath;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,13 +57,22 @@ class TooltipAPI {
 | 
			
		||||
   * @private for platform-internal use
 | 
			
		||||
   */
 | 
			
		||||
  showTooltip(tooltip) {
 | 
			
		||||
    this.removeAllTooltips();
 | 
			
		||||
    this.activeToolTips.push(tooltip);
 | 
			
		||||
    tooltip.show();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * API method to allow for removing all tooltips
 | 
			
		||||
   */
 | 
			
		||||
  removeAllTooltips() {
 | 
			
		||||
    if (!this.activeToolTips?.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    for (let i = this.activeToolTips.length - 1; i > -1; i--) {
 | 
			
		||||
      this.activeToolTips[i].destroy();
 | 
			
		||||
      this.activeToolTips.splice(i, 1);
 | 
			
		||||
    }
 | 
			
		||||
    this.activeToolTips.push(tooltip);
 | 
			
		||||
 | 
			
		||||
    tooltip.show();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
  height: auto;
 | 
			
		||||
  width: auto;
 | 
			
		||||
  padding: $interiorMargin;
 | 
			
		||||
  overflow-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.c-tooltip {
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,12 @@ define([], function () {
 | 
			
		||||
      this.updateRowData.bind(this)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.openmct.telemetry.request(this.domainObject, { size: 1 }).then(
 | 
			
		||||
    const options = {
 | 
			
		||||
      size: 1,
 | 
			
		||||
      strategy: 'latest',
 | 
			
		||||
      timeContext: this.openmct.time.getContextForView([])
 | 
			
		||||
    };
 | 
			
		||||
    this.openmct.telemetry.request(this.domainObject, options).then(
 | 
			
		||||
      function (history) {
 | 
			
		||||
        if (!this.initialized && history.length > 0) {
 | 
			
		||||
          this.updateRowData(history[history.length - 1]);
 | 
			
		||||
 
 | 
			
		||||
@@ -98,9 +98,11 @@ export default function () {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function getScatterPlotFormControl(openmct) {
 | 
			
		||||
    let destroyComponent;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      show(element, model, onChange) {
 | 
			
		||||
        const { vNode } = mount(
 | 
			
		||||
        const { vNode, destroy } = mount(
 | 
			
		||||
          {
 | 
			
		||||
            el: element,
 | 
			
		||||
            components: {
 | 
			
		||||
@@ -122,8 +124,12 @@ export default function () {
 | 
			
		||||
            element
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
        destroyComponent = destroy;
 | 
			
		||||
 | 
			
		||||
        return vNode;
 | 
			
		||||
      },
 | 
			
		||||
      destroy() {
 | 
			
		||||
        destroyComponent();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ export default function plugin(appliesToObjects, options = { indicator: true })
 | 
			
		||||
 | 
			
		||||
  return function install(openmct) {
 | 
			
		||||
    if (installIndicator) {
 | 
			
		||||
      const { vNode } = mount(
 | 
			
		||||
      const { vNode, destroy } = mount(
 | 
			
		||||
        {
 | 
			
		||||
          components: {
 | 
			
		||||
            GlobalClearIndicator
 | 
			
		||||
@@ -49,7 +49,8 @@ export default function plugin(appliesToObjects, options = { indicator: true })
 | 
			
		||||
      let indicator = {
 | 
			
		||||
        element: vNode.el,
 | 
			
		||||
        key: 'global-clear-indicator',
 | 
			
		||||
        priority: openmct.priority.DEFAULT
 | 
			
		||||
        priority: openmct.priority.DEFAULT,
 | 
			
		||||
        destroy: destroy
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      openmct.indicators.add(indicator);
 | 
			
		||||
 
 | 
			
		||||
@@ -201,9 +201,11 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestLAD(telemetryObjects, requestOptions) {
 | 
			
		||||
    //We pass in the global time context here
 | 
			
		||||
    let options = {
 | 
			
		||||
      strategy: 'latest',
 | 
			
		||||
      size: 1
 | 
			
		||||
      size: 1,
 | 
			
		||||
      timeContext: this.openmct.time.getContextForView([])
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (requestOptions !== undefined) {
 | 
			
		||||
 
 | 
			
		||||
@@ -189,9 +189,11 @@ export default class TelemetryCriterion extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestLAD(telemetryObjects, requestOptions) {
 | 
			
		||||
    //We pass in the global time context here
 | 
			
		||||
    let options = {
 | 
			
		||||
      strategy: 'latest',
 | 
			
		||||
      size: 1
 | 
			
		||||
      size: 1,
 | 
			
		||||
      timeContext: this.openmct.time.getContextForView([])
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (requestOptions !== undefined) {
 | 
			
		||||
 
 | 
			
		||||
@@ -83,13 +83,19 @@ describe('The telemetry criterion', function () {
 | 
			
		||||
    });
 | 
			
		||||
    openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
 | 
			
		||||
 | 
			
		||||
    openmct.time = jasmine.createSpyObj('timeAPI', ['timeSystem', 'bounds', 'getAllTimeSystems']);
 | 
			
		||||
    openmct.time = jasmine.createSpyObj('timeAPI', [
 | 
			
		||||
      'timeSystem',
 | 
			
		||||
      'bounds',
 | 
			
		||||
      'getAllTimeSystems',
 | 
			
		||||
      'getContextForView'
 | 
			
		||||
    ]);
 | 
			
		||||
    openmct.time.timeSystem.and.returnValue({ key: 'system' });
 | 
			
		||||
    openmct.time.bounds.and.returnValue({
 | 
			
		||||
      start: 0,
 | 
			
		||||
      end: 1
 | 
			
		||||
    });
 | 
			
		||||
    openmct.time.getAllTimeSystems.and.returnValue([{ key: 'system' }]);
 | 
			
		||||
    openmct.time.getContextForView.and.returnValue({});
 | 
			
		||||
 | 
			
		||||
    testCriterionDefinition = {
 | 
			
		||||
      id: 'test-criterion-id',
 | 
			
		||||
 
 | 
			
		||||
@@ -20,14 +20,14 @@
 | 
			
		||||
 at runtime from the About dialog for additional information.
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <layout-frame
 | 
			
		||||
  <LayoutFrame
 | 
			
		||||
    :item="item"
 | 
			
		||||
    :grid-size="gridSize"
 | 
			
		||||
    :is-editing="isEditing"
 | 
			
		||||
    @move="(gridDelta) => $emit('move', gridDelta)"
 | 
			
		||||
    @endMove="() => $emit('endMove')"
 | 
			
		||||
  >
 | 
			
		||||
    <object-frame
 | 
			
		||||
    <ObjectFrame
 | 
			
		||||
      v-if="domainObject"
 | 
			
		||||
      ref="objectFrame"
 | 
			
		||||
      :domain-object="domainObject"
 | 
			
		||||
@@ -37,7 +37,7 @@
 | 
			
		||||
      :layout-font-size="item.fontSize"
 | 
			
		||||
      :layout-font="item.font"
 | 
			
		||||
    />
 | 
			
		||||
  </layout-frame>
 | 
			
		||||
  </LayoutFrame>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 
 | 
			
		||||
@@ -221,6 +221,8 @@ export default class DuplicateTask {
 | 
			
		||||
      // parse reviver to replace identifiers
 | 
			
		||||
      clonedParent = JSON.parse(clonedParent, (key, value) => {
 | 
			
		||||
        if (
 | 
			
		||||
          value !== null &&
 | 
			
		||||
          value !== undefined &&
 | 
			
		||||
          Object.prototype.hasOwnProperty.call(value, 'key') &&
 | 
			
		||||
          Object.prototype.hasOwnProperty.call(value, 'namespace') &&
 | 
			
		||||
          value.key === oldId.key &&
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,6 @@
 | 
			
		||||
      <template v-for="(container, index) in containers" :key="`component-${container.id}`">
 | 
			
		||||
        <drop-hint
 | 
			
		||||
          v-if="index === 0 && containers.length > 1"
 | 
			
		||||
          :key="`hint-top-${container.id}`"
 | 
			
		||||
          class="c-fl-frame__drop-hint"
 | 
			
		||||
          :index="-1"
 | 
			
		||||
          :allow-drop="allowContainerDrop"
 | 
			
		||||
@@ -59,7 +58,6 @@
 | 
			
		||||
 | 
			
		||||
        <resize-handle
 | 
			
		||||
          v-if="index !== containers.length - 1"
 | 
			
		||||
          :key="`handle-${container.id}`"
 | 
			
		||||
          :index="index"
 | 
			
		||||
          :orientation="rowsLayout ? 'vertical' : 'horizontal'"
 | 
			
		||||
          :is-editing="isEditing"
 | 
			
		||||
@@ -70,7 +68,6 @@
 | 
			
		||||
 | 
			
		||||
        <drop-hint
 | 
			
		||||
          v-if="containers.length > 1"
 | 
			
		||||
          :key="`hint-bottom-${container.id}`"
 | 
			
		||||
          class="c-fl-frame__drop-hint"
 | 
			
		||||
          :index="index"
 | 
			
		||||
          :allow-drop="allowContainerDrop"
 | 
			
		||||
@@ -137,15 +134,16 @@ export default {
 | 
			
		||||
    ResizeHandle,
 | 
			
		||||
    DropHint
 | 
			
		||||
  },
 | 
			
		||||
  inject: ['openmct', 'objectPath', 'layoutObject'],
 | 
			
		||||
  inject: ['openmct', 'objectPath', 'domainObject'],
 | 
			
		||||
  props: {
 | 
			
		||||
    isEditing: Boolean
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      domainObject: this.layoutObject,
 | 
			
		||||
      newFrameLocation: [],
 | 
			
		||||
      identifierMap: {}
 | 
			
		||||
      identifierMap: {},
 | 
			
		||||
      containers: this.domainObject.configuration.containers,
 | 
			
		||||
      rowsLayout: this.domainObject.configuration.rowsLayout
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
@@ -156,22 +154,22 @@ export default {
 | 
			
		||||
        return 'Columns';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    containers() {
 | 
			
		||||
      return this.domainObject.configuration.containers;
 | 
			
		||||
    },
 | 
			
		||||
    rowsLayout() {
 | 
			
		||||
      return this.domainObject.configuration.rowsLayout;
 | 
			
		||||
    },
 | 
			
		||||
    allContainersAreEmpty() {
 | 
			
		||||
      return this.containers.every((container) => container.frames.length === 0);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
  created() {
 | 
			
		||||
    this.buildIdentifierMap();
 | 
			
		||||
    this.composition = this.openmct.composition.get(this.domainObject);
 | 
			
		||||
    this.composition.on('remove', this.removeChildObject);
 | 
			
		||||
    this.composition.on('add', this.addFrame);
 | 
			
		||||
    this.composition.load();
 | 
			
		||||
    this.openmct.objects.observe(this.domainObject, 'configuration.containers', (containers) => {
 | 
			
		||||
      this.containers = containers;
 | 
			
		||||
    });
 | 
			
		||||
    this.openmct.objects.observe(this.domainObject, 'configuration.rowsLayout', (rowsLayout) => {
 | 
			
		||||
      this.rowsLayout = rowsLayout;
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.composition.off('remove', this.removeChildObject);
 | 
			
		||||
@@ -211,20 +209,16 @@ export default {
 | 
			
		||||
      let container = this.containers.filter((c) => c.id === containerId)[0];
 | 
			
		||||
      let containerIndex = this.containers.indexOf(container);
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
                remove associated domainObjects from composition
 | 
			
		||||
            */
 | 
			
		||||
      // remove associated domainObjects from composition
 | 
			
		||||
      container.frames.forEach((f) => {
 | 
			
		||||
        this.removeFromComposition(f.domainObjectIdentifier);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.containers.splice(containerIndex, 1);
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
                add a container when there are no containers in the FL,
 | 
			
		||||
                to prevent user from not being able to add a frame via
 | 
			
		||||
                drag and drop.
 | 
			
		||||
            */
 | 
			
		||||
      // add a container when there are no containers in the FL,
 | 
			
		||||
      // to prevent user from not being able to add a frame via
 | 
			
		||||
      // drag and drop.
 | 
			
		||||
      if (this.containers.length === 0) {
 | 
			
		||||
        this.containers.push(new Container(100));
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,17 +47,16 @@ export default class FlexibleLayoutViewProvider {
 | 
			
		||||
    let component = null;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      show: function (element, isEditing) {
 | 
			
		||||
      show(element, isEditing) {
 | 
			
		||||
        const { vNode, destroy } = mount(
 | 
			
		||||
          {
 | 
			
		||||
            el: element,
 | 
			
		||||
            components: {
 | 
			
		||||
              FlexibleLayoutComponent
 | 
			
		||||
            },
 | 
			
		||||
            provide: {
 | 
			
		||||
              openmct: openmct,
 | 
			
		||||
              openmct,
 | 
			
		||||
              objectPath,
 | 
			
		||||
              layoutObject: domainObject
 | 
			
		||||
              domainObject
 | 
			
		||||
            },
 | 
			
		||||
            data() {
 | 
			
		||||
              return {
 | 
			
		||||
@@ -75,7 +74,7 @@ export default class FlexibleLayoutViewProvider {
 | 
			
		||||
        component = vNode.componentInstance;
 | 
			
		||||
        _destroy = destroy;
 | 
			
		||||
      },
 | 
			
		||||
      getSelectionContext: function () {
 | 
			
		||||
      getSelectionContext() {
 | 
			
		||||
        return {
 | 
			
		||||
          item: domainObject,
 | 
			
		||||
          addContainer: component.$refs.flexibleLayout.addContainer,
 | 
			
		||||
@@ -84,10 +83,10 @@ export default class FlexibleLayoutViewProvider {
 | 
			
		||||
          type: 'flexible-layout'
 | 
			
		||||
        };
 | 
			
		||||
      },
 | 
			
		||||
      onEditModeChange: function (isEditing) {
 | 
			
		||||
      onEditModeChange(isEditing) {
 | 
			
		||||
        component.isEditing = isEditing;
 | 
			
		||||
      },
 | 
			
		||||
      destroy: function (element) {
 | 
			
		||||
      destroy() {
 | 
			
		||||
        if (_destroy) {
 | 
			
		||||
          _destroy();
 | 
			
		||||
          component = null;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,10 @@ describe('the plugin', function () {
 | 
			
		||||
  let mockComposition;
 | 
			
		||||
 | 
			
		||||
  const testViewObject = {
 | 
			
		||||
    identifier: {
 | 
			
		||||
      namespace: '',
 | 
			
		||||
      key: 'test-object'
 | 
			
		||||
    },
 | 
			
		||||
    id: 'test-object',
 | 
			
		||||
    type: 'flexible-layout',
 | 
			
		||||
    configuration: {
 | 
			
		||||
@@ -116,6 +120,10 @@ describe('the plugin', function () {
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      flexibleLayoutItem = {
 | 
			
		||||
        identifier: {
 | 
			
		||||
          namespace: '',
 | 
			
		||||
          key: 'test-object'
 | 
			
		||||
        },
 | 
			
		||||
        id: 'test-object',
 | 
			
		||||
        type: 'flexible-layout',
 | 
			
		||||
        configuration: {
 | 
			
		||||
 
 | 
			
		||||
@@ -167,9 +167,11 @@ export default function () {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  function getGaugeFormController(openmct) {
 | 
			
		||||
    let destroyComponent;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      show(element, model, onChange) {
 | 
			
		||||
        const { vNode } = mount(
 | 
			
		||||
        const { vNode, destroy } = mount(
 | 
			
		||||
          {
 | 
			
		||||
            el: element,
 | 
			
		||||
            components: {
 | 
			
		||||
@@ -191,8 +193,12 @@ export default function () {
 | 
			
		||||
            element
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
        destroyComponent = destroy;
 | 
			
		||||
 | 
			
		||||
        return vNode.componentInstance;
 | 
			
		||||
      },
 | 
			
		||||
      destroy() {
 | 
			
		||||
        destroyComponent();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -638,7 +638,11 @@ export default {
 | 
			
		||||
 | 
			
		||||
      this.valueKey = this.metadata.valuesForHints(['range'])[0].source;
 | 
			
		||||
 | 
			
		||||
      this.openmct.telemetry.request(domainObject, { strategy: 'latest' }).then((values) => {
 | 
			
		||||
      const options = {
 | 
			
		||||
        strategy: 'latest',
 | 
			
		||||
        timeContext: this.openmct.time.getContextForView([])
 | 
			
		||||
      };
 | 
			
		||||
      this.openmct.telemetry.request(domainObject, options).then((values) => {
 | 
			
		||||
        const length = values.length;
 | 
			
		||||
        this.updateValue(values[length - 1]);
 | 
			
		||||
      });
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,9 @@ export default {
 | 
			
		||||
    if (this.unlisten) {
 | 
			
		||||
      this.unlisten();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.destroyImageryContainer) {
 | 
			
		||||
      this.destroyImageryContainer();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    setTimeContext() {
 | 
			
		||||
@@ -237,7 +240,10 @@ export default {
 | 
			
		||||
        imageryContainer = existingContainer;
 | 
			
		||||
        imageryContainer.style.maxWidth = `${containerWidth}px`;
 | 
			
		||||
      } else {
 | 
			
		||||
        const { vNode } = mount(
 | 
			
		||||
        if (this.destroyImageryContainer) {
 | 
			
		||||
          this.destroyImageryContainer();
 | 
			
		||||
        }
 | 
			
		||||
        const { vNode, destroy } = mount(
 | 
			
		||||
          {
 | 
			
		||||
            components: {
 | 
			
		||||
              SwimLane
 | 
			
		||||
@@ -257,6 +263,7 @@ export default {
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.destroyImageryContainer = destroy;
 | 
			
		||||
        const component = vNode.componentInstance;
 | 
			
		||||
        this.$refs.imageryHolder.appendChild(component.$el);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -864,6 +864,7 @@ export default {
 | 
			
		||||
      if (this.domainObject.configuration) {
 | 
			
		||||
        const persistedLayers = this.domainObject.configuration.layers;
 | 
			
		||||
        if (!persistedLayers) {
 | 
			
		||||
          this.layers.forEach((layer) => (layer.visible = false));
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <li class="c-inspect-properties__row">
 | 
			
		||||
    <div class="c-inspect-properties__label">
 | 
			
		||||
  <li class="c-inspect-properties__row" :aria-label="`${detail.name} inspector properties`">
 | 
			
		||||
    <div class="c-inspect-properties__label" aria-label="inspector property name">
 | 
			
		||||
      {{ detail.name }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="c-inspect-properties__value">
 | 
			
		||||
    <div class="c-inspect-properties__value" aria-label="inspector property value">
 | 
			
		||||
      {{ detail.value }}
 | 
			
		||||
    </div>
 | 
			
		||||
  </li>
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,43 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  async mounted() {
 | 
			
		||||
    this.nameChangeListeners = {};
 | 
			
		||||
    await this.createPathBreadCrumb();
 | 
			
		||||
  },
 | 
			
		||||
  unmounted() {
 | 
			
		||||
    Object.values(this.nameChangeListeners).forEach((unlisten) => {
 | 
			
		||||
      unlisten();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateObjectPathName(keyString, newName) {
 | 
			
		||||
      this.pathBreadCrumb = this.pathBreadCrumb.map((pathObject) => {
 | 
			
		||||
        if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {
 | 
			
		||||
          return {
 | 
			
		||||
            ...pathObject,
 | 
			
		||||
            domainObject: { ...pathObject.domainObject, name: newName }
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return pathObject;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    removeNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString]();
 | 
			
		||||
        delete this.nameChangeListeners[keyString];
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (!this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString] = this.openmct.objects.observe(
 | 
			
		||||
          domainObject,
 | 
			
		||||
          'name',
 | 
			
		||||
          this.updateObjectPathName.bind(this, keyString)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    async createPathBreadCrumb() {
 | 
			
		||||
      if (!this.domainObject && this.parentDomainObject) {
 | 
			
		||||
        this.setPathBreadCrumb([this.parentDomainObject]);
 | 
			
		||||
@@ -98,7 +132,15 @@ export default {
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.pathBreadCrumb.forEach((pathObject) => {
 | 
			
		||||
        this.removeNameListenerFor(pathObject.domainObject);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.pathBreadCrumb = pathBreadCrumb;
 | 
			
		||||
 | 
			
		||||
      this.pathBreadCrumb.forEach((pathObject) => {
 | 
			
		||||
        this.addNameListenerFor(pathObject.domainObject);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -230,7 +230,22 @@ export default {
 | 
			
		||||
      return `detail-${component}`;
 | 
			
		||||
    },
 | 
			
		||||
    updateSelection(selection) {
 | 
			
		||||
      this.removeListener();
 | 
			
		||||
      this.selection.splice(0, this.selection.length, ...selection);
 | 
			
		||||
      if (this.domainObject) {
 | 
			
		||||
        this.addListener();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    removeListener() {
 | 
			
		||||
      if (this.nameListener) {
 | 
			
		||||
        this.nameListener();
 | 
			
		||||
        this.nameListener = null;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addListener() {
 | 
			
		||||
      this.nameListener = this.openmct.objects.observe(this.context?.item, 'name', (newValue) => {
 | 
			
		||||
        this.context.item = { ...this.context?.item, name: newValue };
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,10 @@ export default {
 | 
			
		||||
      drawerElement.innerHTML = '<div></div>';
 | 
			
		||||
      const divElement = document.querySelector('.l-shell__drawer div');
 | 
			
		||||
 | 
			
		||||
      mount(
 | 
			
		||||
      if (this.destroySnapshotContainer) {
 | 
			
		||||
        this.destroySnapshotContainer();
 | 
			
		||||
      }
 | 
			
		||||
      const { destroy } = mount(
 | 
			
		||||
        {
 | 
			
		||||
          el: divElement,
 | 
			
		||||
          components: {
 | 
			
		||||
@@ -113,6 +116,7 @@ export default {
 | 
			
		||||
          element: divElement
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      this.destroySnapshotContainer = destroy;
 | 
			
		||||
    },
 | 
			
		||||
    updateSnapshotIndicatorTitle() {
 | 
			
		||||
      const snapshotCount = this.snapshotContainer.getSnapshots().length;
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ function installBaseNotebookFunctionality(openmct) {
 | 
			
		||||
  openmct.actions.register(new CopyToNotebookAction(openmct));
 | 
			
		||||
  openmct.actions.register(new ExportNotebookAsTextAction(openmct));
 | 
			
		||||
 | 
			
		||||
  const { vNode } = mount(
 | 
			
		||||
  const { vNode, destroy } = mount(
 | 
			
		||||
    {
 | 
			
		||||
      components: {
 | 
			
		||||
        NotebookSnapshotIndicator
 | 
			
		||||
@@ -102,7 +102,8 @@ function installBaseNotebookFunctionality(openmct) {
 | 
			
		||||
  const indicator = {
 | 
			
		||||
    element: vNode.el,
 | 
			
		||||
    key: 'notebook-snapshot-indicator',
 | 
			
		||||
    priority: openmct.priority.DEFAULT
 | 
			
		||||
    priority: openmct.priority.DEFAULT,
 | 
			
		||||
    destroy: destroy
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  openmct.indicators.add(indicator);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import NotificationIndicator from './components/NotificationIndicator.vue';
 | 
			
		||||
 | 
			
		||||
export default function plugin() {
 | 
			
		||||
  return function install(openmct) {
 | 
			
		||||
    const { vNode } = mount(
 | 
			
		||||
    const { vNode, destroy } = mount(
 | 
			
		||||
      {
 | 
			
		||||
        components: {
 | 
			
		||||
          NotificationIndicator
 | 
			
		||||
@@ -42,7 +42,8 @@ export default function plugin() {
 | 
			
		||||
    let indicator = {
 | 
			
		||||
      key: 'notifications-indicator',
 | 
			
		||||
      element: vNode.el,
 | 
			
		||||
      priority: openmct.priority.DEFAULT
 | 
			
		||||
      priority: openmct.priority.DEFAULT,
 | 
			
		||||
      destroy: destroy
 | 
			
		||||
    };
 | 
			
		||||
    openmct.indicators.add(indicator);
 | 
			
		||||
  };
 | 
			
		||||
 
 | 
			
		||||
@@ -248,6 +248,7 @@ export default {
 | 
			
		||||
      highlights: [],
 | 
			
		||||
      annotatedPoints: [],
 | 
			
		||||
      annotationSelections: [],
 | 
			
		||||
      annotationsEverLoaded: false,
 | 
			
		||||
      lockHighlightPoint: false,
 | 
			
		||||
      yKeyOptions: [],
 | 
			
		||||
      yAxisLabel: '',
 | 
			
		||||
@@ -394,7 +395,11 @@ export default {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.openmct.objectViews.on('clearData', this.clearData);
 | 
			
		||||
    this.$on('loadingComplete', this.loadAnnotations);
 | 
			
		||||
    this.$on('loadingComplete', () => {
 | 
			
		||||
      if (this.annotationViewingAndEditingAllowed) {
 | 
			
		||||
        this.loadAnnotations();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this.openmct.selection.on('change', this.updateSelection);
 | 
			
		||||
    this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
 | 
			
		||||
 | 
			
		||||
@@ -636,6 +641,7 @@ export default {
 | 
			
		||||
      if (rawAnnotationsForPlot) {
 | 
			
		||||
        this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
 | 
			
		||||
      }
 | 
			
		||||
      this.annotationsEverLoaded = true;
 | 
			
		||||
    },
 | 
			
		||||
    loadSeriesData(series) {
 | 
			
		||||
      //this check ensures that duplicate requests don't happen on load
 | 
			
		||||
@@ -789,6 +795,7 @@ export default {
 | 
			
		||||
      };
 | 
			
		||||
      this.config.xAxis.set('range', newRange);
 | 
			
		||||
      if (!isTick) {
 | 
			
		||||
        this.annotatedPoints = [];
 | 
			
		||||
        this.clearPanZoomHistory();
 | 
			
		||||
        this.synchronizeIfBoundsMatch();
 | 
			
		||||
        this.loadMoreData(newRange, true);
 | 
			
		||||
@@ -1785,6 +1792,9 @@ export default {
 | 
			
		||||
      });
 | 
			
		||||
      this.config.xAxis.set('frozen', true);
 | 
			
		||||
      this.setStatus();
 | 
			
		||||
      if (!this.annotationsEverLoaded) {
 | 
			
		||||
        this.loadAnnotations();
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    resumeRealtimeData() {
 | 
			
		||||
 
 | 
			
		||||
@@ -826,56 +826,32 @@ export default {
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
 | 
			
		||||
      if (!yRange) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
 | 
			
		||||
      const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        xValue > xRange.min && xValue < xRange.max && yValue > yRange.min && yValue < yRange.max
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    drawAnnotatedPoints(yAxisId) {
 | 
			
		||||
      // we should do this by series, and then plot all the points at once instead
 | 
			
		||||
      // of doing it one by one
 | 
			
		||||
      if (this.annotatedPoints && this.annotatedPoints.length) {
 | 
			
		||||
        const uniquePointsToDraw = [];
 | 
			
		||||
        const xRange = this.config.xAxis.get('displayRange');
 | 
			
		||||
        let yRange;
 | 
			
		||||
        if (yAxisId === this.config.yAxis.get('id')) {
 | 
			
		||||
          yRange = this.config.yAxis.get('displayRange');
 | 
			
		||||
        } else if (this.config.additionalYAxes.length) {
 | 
			
		||||
          const yAxisForId = this.config.additionalYAxes.find(
 | 
			
		||||
            (yAxis) => yAxis.get('id') === yAxisId
 | 
			
		||||
          );
 | 
			
		||||
          yRange = yAxisForId.get('displayRange');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const annotatedPoints = this.annotatedPoints.filter(
 | 
			
		||||
          this.matchByYAxisId.bind(this, yAxisId)
 | 
			
		||||
        );
 | 
			
		||||
        annotatedPoints.forEach((annotatedPoint) => {
 | 
			
		||||
          // if the annotation is outside the range, don't draw it
 | 
			
		||||
          if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
 | 
			
		||||
            const canvasXValue = this.offset[yAxisId].xVal(
 | 
			
		||||
              annotatedPoint.point,
 | 
			
		||||
              annotatedPoint.series
 | 
			
		||||
            );
 | 
			
		||||
            const canvasYValue = this.offset[yAxisId].yVal(
 | 
			
		||||
              annotatedPoint.point,
 | 
			
		||||
              annotatedPoint.series
 | 
			
		||||
            );
 | 
			
		||||
            const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
 | 
			
		||||
            const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
 | 
			
		||||
              return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
 | 
			
		||||
            });
 | 
			
		||||
            if (!drawnPoint) {
 | 
			
		||||
              uniquePointsToDraw.push(pointToDraw);
 | 
			
		||||
              this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
 | 
			
		||||
            }
 | 
			
		||||
          // annotation points are all within range (checked in MctPlot with FlatBush), so we don't need to check
 | 
			
		||||
          const canvasXValue = this.offset[yAxisId].xVal(
 | 
			
		||||
            annotatedPoint.point,
 | 
			
		||||
            annotatedPoint.series
 | 
			
		||||
          );
 | 
			
		||||
          const canvasYValue = this.offset[yAxisId].yVal(
 | 
			
		||||
            annotatedPoint.point,
 | 
			
		||||
            annotatedPoint.series
 | 
			
		||||
          );
 | 
			
		||||
          const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
 | 
			
		||||
          const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
 | 
			
		||||
            return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
 | 
			
		||||
          });
 | 
			
		||||
          if (!drawnPoint) {
 | 
			
		||||
            uniquePointsToDraw.push(pointToDraw);
 | 
			
		||||
            this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -197,7 +197,7 @@ export default {
 | 
			
		||||
        this.composition.load();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { vNode } = mount(
 | 
			
		||||
      const { vNode, destroy } = mount(
 | 
			
		||||
        {
 | 
			
		||||
          components: {
 | 
			
		||||
            Plot
 | 
			
		||||
@@ -249,6 +249,7 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      this.component = vNode.componentInstance;
 | 
			
		||||
      this._destroy = destroy;
 | 
			
		||||
 | 
			
		||||
      if (this.isEditing) {
 | 
			
		||||
        this.setSelection();
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,13 @@ export default class RemoteClock extends DefaultClock {
 | 
			
		||||
    this.openmct.objects
 | 
			
		||||
      .get(this.identifier)
 | 
			
		||||
      .then((domainObject) => {
 | 
			
		||||
        // The start method is called when at least one listener registers with the clock.
 | 
			
		||||
        // When the clock is changed, listeners are unregistered from the clock and the stop method is called.
 | 
			
		||||
        // Sometimes, the objects.get call above does not resolve before the stop method is called.
 | 
			
		||||
        // So when we proceed with the clock subscription below, we first need to ensure that there is at least one listener for our clock.
 | 
			
		||||
        if (this.eventNames().length === 0) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.openmct.time.on('timeSystem', this._timeSystemChange);
 | 
			
		||||
        this.timeTelemetryObject = domainObject;
 | 
			
		||||
        this.metadata = this.openmct.telemetry.getMetadata(domainObject);
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,7 @@ export default {
 | 
			
		||||
    this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
 | 
			
		||||
    this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    clearInterval(this.resizeTimer);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data: function () {
 | 
			
		||||
  data() {
 | 
			
		||||
    const activeClock = this.getActiveClock();
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
@@ -66,11 +66,11 @@ export default {
 | 
			
		||||
      clocks: []
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted: function () {
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.loadClocks(this.configuration.menuOptions);
 | 
			
		||||
    this.openmct.time.on(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
 | 
			
		||||
  },
 | 
			
		||||
  destroyed: function () {
 | 
			
		||||
  unmounted() {
 | 
			
		||||
    this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,7 @@ export default {
 | 
			
		||||
    this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
 | 
			
		||||
    this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.updateMode);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.openmct.time.off(TIME_CONTEXT_EVENTS.boundsChanged, this.addTimespan);
 | 
			
		||||
    this.openmct.time.off(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.addTimespan);
 | 
			
		||||
    this.openmct.time.off(TIME_CONTEXT_EVENTS.timeSystemChanged, this.updateTimeSystem);
 | 
			
		||||
 
 | 
			
		||||
@@ -184,7 +184,7 @@ export default {
 | 
			
		||||
    this.$emit('popupLoaded');
 | 
			
		||||
    this.setTimeContext();
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.stopFollowingTimeContext();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ export default {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.openmct.time.off(TIME_CONTEXT_EVENTS.clockChanged, this.setViewFromClock);
 | 
			
		||||
  },
 | 
			
		||||
  mounted: function () {
 | 
			
		||||
 
 | 
			
		||||
@@ -194,7 +194,7 @@ export default {
 | 
			
		||||
      deep: true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
  created() {
 | 
			
		||||
    this.initialize();
 | 
			
		||||
  },
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ export default {
 | 
			
		||||
    this.timeConductorOptionsHolder = this.$el;
 | 
			
		||||
    this.timeConductorOptionsHolder.addEventListener('click', this.showPopup);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.clearPopup();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -157,7 +157,7 @@ export default {
 | 
			
		||||
    this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
 | 
			
		||||
    this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.getTimeSystem())));
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    this.clearAllValidation();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -173,7 +173,7 @@ export default {
 | 
			
		||||
    this.setOffsets();
 | 
			
		||||
    document.addEventListener('click', this.hide);
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
  beforeUnmount() {
 | 
			
		||||
    document.removeEventListener('click', this.hide);
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ import UserIndicator from './components/UserIndicator.vue';
 | 
			
		||||
 | 
			
		||||
export default function UserIndicatorPlugin() {
 | 
			
		||||
  function addIndicator(openmct) {
 | 
			
		||||
    const { vNode } = mount(
 | 
			
		||||
    const { vNode, destroy } = mount(
 | 
			
		||||
      {
 | 
			
		||||
        components: {
 | 
			
		||||
          UserIndicator
 | 
			
		||||
@@ -43,7 +43,8 @@ export default function UserIndicatorPlugin() {
 | 
			
		||||
    openmct.indicators.add({
 | 
			
		||||
      key: 'user-indicator',
 | 
			
		||||
      element: vNode.el,
 | 
			
		||||
      priority: openmct.priority.HIGH
 | 
			
		||||
      priority: openmct.priority.HIGH,
 | 
			
		||||
      destroy: destroy
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,7 @@ export default {
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  async mounted() {
 | 
			
		||||
    this.nameChangeListeners = {};
 | 
			
		||||
    const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
    if (keyString && this.keyString !== keyString) {
 | 
			
		||||
@@ -108,8 +109,16 @@ export default {
 | 
			
		||||
        // remove ROOT and object itself from path
 | 
			
		||||
        this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
 | 
			
		||||
      }
 | 
			
		||||
      this.orderedPath.forEach((pathObject) => {
 | 
			
		||||
        this.addNameListenerFor(pathObject.domainObject);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  unmounted() {
 | 
			
		||||
    Object.values(this.nameChangeListeners).forEach((unlisten) => {
 | 
			
		||||
      unlisten();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    /**
 | 
			
		||||
     * Generate the hash url for the given object path, removing the '/ROOT' prefix if present.
 | 
			
		||||
@@ -120,6 +129,34 @@ export default {
 | 
			
		||||
      const path = `/browse/${this.openmct.objects.getRelativePath(objectPath)}`;
 | 
			
		||||
 | 
			
		||||
      return path.replace('ROOT/', '');
 | 
			
		||||
    },
 | 
			
		||||
    updateObjectPathName(keyString, newName) {
 | 
			
		||||
      this.orderedPath = this.orderedPath.map((pathObject) => {
 | 
			
		||||
        if (this.openmct.objects.makeKeyString(pathObject.domainObject.identifier) === keyString) {
 | 
			
		||||
          return {
 | 
			
		||||
            ...pathObject,
 | 
			
		||||
            domainObject: { ...pathObject.domainObject, name: newName }
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return pathObject;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    removeNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString]();
 | 
			
		||||
        delete this.nameChangeListeners[keyString];
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    addNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (!this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString] = this.openmct.objects.observe(
 | 
			
		||||
          domainObject,
 | 
			
		||||
          'name',
 | 
			
		||||
          this.updateObjectPathName.bind(this, keyString)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -104,12 +104,19 @@ export default {
 | 
			
		||||
    if (this.statusUnsubscribe) {
 | 
			
		||||
      this.statusUnsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.nameUnsubscribe) {
 | 
			
		||||
      this.nameUnsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateSelection(selection) {
 | 
			
		||||
      if (this.statusUnsubscribe) {
 | 
			
		||||
        this.statusUnsubscribe();
 | 
			
		||||
        this.statusUnsubscribe = undefined;
 | 
			
		||||
        this.statusUnsubscribe = null;
 | 
			
		||||
      }
 | 
			
		||||
      if (this.nameUnsubscribe) {
 | 
			
		||||
        this.nameUnsubscribe();
 | 
			
		||||
        this.nameUnsubscribe = null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (selection.length === 0 || selection[0].length === 0) {
 | 
			
		||||
@@ -132,6 +139,11 @@ export default {
 | 
			
		||||
          this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
          this.status = this.openmct.status.get(this.keyString);
 | 
			
		||||
          this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
 | 
			
		||||
          this.nameUnsubscribe = this.openmct.objects.observe(
 | 
			
		||||
            this.domainObject,
 | 
			
		||||
            'name',
 | 
			
		||||
            this.updateName
 | 
			
		||||
          );
 | 
			
		||||
        } else if (selection[0][0].context.layoutItem) {
 | 
			
		||||
          this.layoutItem = selection[0][0].context.layoutItem;
 | 
			
		||||
        }
 | 
			
		||||
@@ -144,6 +156,9 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    updateStatus(status) {
 | 
			
		||||
      this.status = status;
 | 
			
		||||
    },
 | 
			
		||||
    updateName(newName) {
 | 
			
		||||
      this.domainObject = { ...this.domainObject, name: newName };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -59,13 +59,47 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.compositionCollections = {};
 | 
			
		||||
    this.nameChangeListeners = {};
 | 
			
		||||
    this.openmct.router.on('change:path', this.onPathChange);
 | 
			
		||||
    this.getSavedRecentItems();
 | 
			
		||||
  },
 | 
			
		||||
  unmounted() {
 | 
			
		||||
    this.openmct.router.off('change:path', this.onPathChange);
 | 
			
		||||
    Object.values(this.nameChangeListeners).forEach((unlisten) => {
 | 
			
		||||
      unlisten();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    addNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (!this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString] = this.openmct.objects.observe(
 | 
			
		||||
          domainObject,
 | 
			
		||||
          'name',
 | 
			
		||||
          this.updateRecentObjectName.bind(this, keyString)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    updateRecentObjectName(keyString, newName) {
 | 
			
		||||
      this.recents = this.recents.map((recentObject) => {
 | 
			
		||||
        if (
 | 
			
		||||
          this.openmct.objects.makeKeyString(recentObject.domainObject.identifier) === keyString
 | 
			
		||||
        ) {
 | 
			
		||||
          return {
 | 
			
		||||
            ...recentObject,
 | 
			
		||||
            domainObject: { ...recentObject.domainObject, name: newName }
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return recentObject;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    removeNameListenerFor(domainObject) {
 | 
			
		||||
      const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
      if (this.nameChangeListeners[keyString]) {
 | 
			
		||||
        this.nameChangeListeners[keyString]();
 | 
			
		||||
        delete this.nameChangeListeners[keyString];
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a composition collection to the map and register its remove handler
 | 
			
		||||
     * @param {string} navigationPath
 | 
			
		||||
@@ -112,6 +146,7 @@ export default {
 | 
			
		||||
      // Get composition collections and add composition listeners for composable objects
 | 
			
		||||
      savedRecents.forEach((recentObject) => {
 | 
			
		||||
        const { domainObject, navigationPath } = recentObject;
 | 
			
		||||
        this.addNameListenerFor(domainObject);
 | 
			
		||||
        if (this.shouldTrackCompositionFor(domainObject)) {
 | 
			
		||||
          this.compositionCollections[navigationPath] = {};
 | 
			
		||||
          this.compositionCollections[navigationPath].collection =
 | 
			
		||||
@@ -161,6 +196,8 @@ export default {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.addNameListenerFor(domainObject);
 | 
			
		||||
 | 
			
		||||
      // Move the object to the top if its already existing in the recents list
 | 
			
		||||
      const existingIndex = this.recents.findIndex((recentObject) => {
 | 
			
		||||
        return navigationPath === recentObject.navigationPath;
 | 
			
		||||
@@ -179,6 +216,7 @@ export default {
 | 
			
		||||
      while (this.recents.length > MAX_RECENT_ITEMS) {
 | 
			
		||||
        const poppedRecentItem = this.recents.pop();
 | 
			
		||||
        this.removeCompositionListenerFor(poppedRecentItem.navigationPath);
 | 
			
		||||
        this.removeNameListenerFor(poppedRecentItem.domainObject);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.setSavedRecentItems();
 | 
			
		||||
@@ -236,6 +274,9 @@ export default {
 | 
			
		||||
            label: 'OK',
 | 
			
		||||
            callback: () => {
 | 
			
		||||
              localStorage.removeItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);
 | 
			
		||||
              Object.values(this.nameChangeListeners).forEach((unlisten) => {
 | 
			
		||||
                unlisten();
 | 
			
		||||
              });
 | 
			
		||||
              this.recents = [];
 | 
			
		||||
              dialog.dismiss();
 | 
			
		||||
              this.$emit('setClearButtonDisabled', true);
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@
 | 
			
		||||
        <div :style="childrenHeightStyles">
 | 
			
		||||
          <tree-item
 | 
			
		||||
            v-for="(treeItem, index) in visibleItems"
 | 
			
		||||
            :key="`${treeItem.navigationPath}-${index}`"
 | 
			
		||||
            :key="`${treeItem.navigationPath}-${index}-${treeItem.object.name}`"
 | 
			
		||||
            :node="treeItem"
 | 
			
		||||
            :is-selector-tree="isSelectorTree"
 | 
			
		||||
            :selected-item="selectedItem"
 | 
			
		||||
 
 | 
			
		||||
@@ -37,9 +37,13 @@ define([], function () {
 | 
			
		||||
      openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateDocumentTitleOnNameMutation(domainObject) {
 | 
			
		||||
      if (typeof domainObject.name === 'string' && domainObject.name !== document.title) {
 | 
			
		||||
        document.title = domainObject.name;
 | 
			
		||||
    function updateDocumentTitleOnNameMutation(newName) {
 | 
			
		||||
      if (typeof newName === 'string' && newName !== document.title) {
 | 
			
		||||
        document.title = newName;
 | 
			
		||||
        openmct.layout.$refs.browseBar.domainObject = {
 | 
			
		||||
          ...openmct.layout.$refs.browseBar.domainObject,
 | 
			
		||||
          name: newName
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -80,7 +84,11 @@ define([], function () {
 | 
			
		||||
        let currentProvider = openmct.objectViews.getByProviderKey(currentViewKey);
 | 
			
		||||
        document.title = browseObject.name; //change document title to current object in main view
 | 
			
		||||
        // assign listener to global for later clearing
 | 
			
		||||
        unobserve = openmct.objects.observe(browseObject, '*', updateDocumentTitleOnNameMutation);
 | 
			
		||||
        unobserve = openmct.objects.observe(
 | 
			
		||||
          browseObject,
 | 
			
		||||
          'name',
 | 
			
		||||
          updateDocumentTitleOnNameMutation
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) {
 | 
			
		||||
          viewObject(browseObject, currentProvider);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user