mirror of
https://github.com/nasa/openmct.git
synced 2025-02-20 09:26:45 +00:00
feat: Recent Objects (#6103)
* clicking recent objects selects that object * clicking target navigates to but does not select that object * max 20 recent objects
This commit is contained in:
parent
22621aaaf8
commit
f98a2cdd6b
@ -49,11 +49,11 @@ test.describe('AppActions', () => {
|
||||
parent: e2eFolder.uuid
|
||||
});
|
||||
|
||||
await page.goto(timer1.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer1.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name);
|
||||
await page.goto(timer2.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer2.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name);
|
||||
await page.goto(timer3.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(timer3.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name);
|
||||
});
|
||||
|
||||
@ -73,11 +73,11 @@ test.describe('AppActions', () => {
|
||||
name: 'Folder Baz',
|
||||
parent: folder2.uuid
|
||||
});
|
||||
await page.goto(folder1.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder1.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name);
|
||||
await page.goto(folder2.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder2.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name);
|
||||
await page.goto(folder3.url, { waitUntil: 'networkidle' });
|
||||
await page.goto(folder3.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name);
|
||||
|
||||
expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`);
|
||||
|
@ -43,48 +43,76 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
const treePane = page.locator('#tree-pane');
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const locatorTree = page.locator('#locator-tree');
|
||||
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
|
||||
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
|
||||
});
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
@ -114,7 +142,7 @@ test.describe('Move & link item tests', () => {
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
@ -138,7 +166,7 @@ test.describe('Move & link item tests', () => {
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
@ -158,48 +186,76 @@ test.describe('Move & link item tests', () => {
|
||||
name: 'Child Folder',
|
||||
parent: parentFolder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const grandchildFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'Grandchild Folder',
|
||||
parent: childFolder.uuid
|
||||
});
|
||||
|
||||
// Attempt to link parent to its own grandparent
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
// Attempt to move parent to its own grandparent
|
||||
await page.locator('button[title="Show selected item in tree"]').click();
|
||||
|
||||
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
|
||||
const treePane = page.locator('#tree-pane');
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: 'Parent Folder'
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Move/
|
||||
}).click();
|
||||
|
||||
const locatorTree = page.locator('#locator-tree');
|
||||
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: parentFolder.name
|
||||
});
|
||||
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
|
||||
|
||||
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
});
|
||||
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await childFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
|
||||
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
|
||||
|
||||
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
|
||||
name: grandchildFolder.name
|
||||
});
|
||||
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
|
||||
await grandchildFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
|
||||
|
||||
await parentFolderLocatorTreeItem.click();
|
||||
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
|
||||
await page.locator('[aria-label="Cancel"]').click();
|
||||
|
||||
// Link Child Folder from Parent Folder to My Items
|
||||
await page.locator('.c-disclosure-triangle >> nth=0').click();
|
||||
await page.locator('.c-disclosure-triangle >> nth=1').click();
|
||||
|
||||
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
|
||||
// Move Child Folder from Parent Folder to My Items
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: new RegExp(childFolder.name)
|
||||
}).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-link').click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
|
||||
await page.getByRole('menuitem', {
|
||||
name: /Link/
|
||||
}).click();
|
||||
await myItemsLocatorTreeItem.click();
|
||||
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
|
||||
name: myItemsFolderName
|
||||
});
|
||||
|
||||
// Expect that Child Folder is in My Items, the root folder
|
||||
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
|
||||
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -98,8 +98,8 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
||||
|
||||
//Edit Condition Set Name from main view
|
||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set');
|
||||
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter');
|
||||
// Click Save Button
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click Save and Finish Editing Option
|
||||
|
@ -24,6 +24,7 @@ const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
|
||||
|
||||
test.describe('Display Layout', () => {
|
||||
/** @type {import('../../../../appActions').CreatedObjectInfo} */
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
@ -47,7 +48,12 @@ test.describe('Display Layout', () => {
|
||||
// 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -74,7 +80,12 @@ test.describe('Display Layout', () => {
|
||||
// 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -105,7 +116,12 @@ test.describe('Display Layout', () => {
|
||||
// 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
@ -139,7 +155,12 @@ test.describe('Display Layout', () => {
|
||||
// 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 Display Layout and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
|
||||
const treePane = page.locator('#tree-pane');
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
|
85
e2e/tests/functional/recentObjects.e2e.spec.js
Normal file
85
e2e/tests/functional/recentObjects.e2e.spec.js
Normal file
@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../pluginFixtures.js');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions.js');
|
||||
|
||||
test.describe('Recent Objects', () => {
|
||||
test('Recent Objects CRUD operations', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a folder and nest a Clock within it
|
||||
const recentObjectsList = page.locator('[aria-label="Recent Objects"]');
|
||||
const folderA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
parent: folderA.uuid
|
||||
});
|
||||
|
||||
// Drag the Recent Objects panel up a bit
|
||||
await page.locator('div:nth-child(2) > .l-pane__handle').hover();
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(0, 100);
|
||||
await page.mouse.up();
|
||||
|
||||
// Verify that both created objects appear in the list and are in the correct order
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
|
||||
|
||||
// Navigate to the folder by clicking on the main object name in the recent objects list item
|
||||
await recentObjectsList.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');
|
||||
|
||||
// Verify rename has been applied in recent objects list item and objects paths
|
||||
expect(page.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
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
|
||||
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.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it");
|
||||
test.fixme("Clicking on an object in the path of a recent object navigates to the object");
|
||||
test.fixme("Tests for context menu actions from recent objects");
|
||||
});
|
@ -72,7 +72,7 @@ test.describe('Grand Search', () => {
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Clock A').click()
|
||||
page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
<template>
|
||||
<mct-tree
|
||||
id="locator-tree"
|
||||
:is-selector-tree="true"
|
||||
:initial-selection="model.parent"
|
||||
@tree-item-selection="handleItemSelection"
|
||||
|
@ -189,13 +189,11 @@ export default class ObjectAPI {
|
||||
/**
|
||||
* Get a domain object.
|
||||
*
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @param {boolean} forceRemote defaults to false. If true, will skip cached and
|
||||
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
|
||||
* @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and
|
||||
* dirty/in-transaction objects use and the provider.get method
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
get(identifier, abortSignal, forceRemote = false) {
|
||||
@ -220,7 +218,7 @@ export default class ObjectAPI {
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No Provider Matched');
|
||||
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
|
||||
}
|
||||
|
||||
if (!provider.get) {
|
||||
@ -740,6 +738,46 @@ export default class ObjectAPI {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and construct an `objectPath` from a `navigationPath`.
|
||||
*
|
||||
* A `navigationPath` is a string of the form `"/browse/<keyString>/<keyString>/..."` that is used
|
||||
* by the Open MCT router to navigate to a specific object.
|
||||
*
|
||||
* Throws an error if the `navigationPath` is malformed.
|
||||
*
|
||||
* @param {string} navigationPath
|
||||
* @returns {DomainObject[]} objectPath
|
||||
*/
|
||||
async getRelativeObjectPath(navigationPath) {
|
||||
if (!navigationPath.startsWith('/browse/')) {
|
||||
throw new Error(`Malformed navigation path: "${navigationPath}"`);
|
||||
}
|
||||
|
||||
navigationPath = navigationPath.replace('/browse/', '');
|
||||
|
||||
if (!navigationPath || navigationPath === '/') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove any query params and split on '/'
|
||||
const keyStrings = navigationPath.split('?')?.[0].split('/');
|
||||
|
||||
if (keyStrings[0] !== 'ROOT') {
|
||||
keyStrings.unshift('ROOT');
|
||||
}
|
||||
|
||||
const objectPath = (await Promise.all(
|
||||
keyStrings.map(
|
||||
keyString => this.supportsMutation(keyString)
|
||||
? this.getMutable(utils.parseKeyString(keyString))
|
||||
: this.get(utils.parseKeyString(keyString))
|
||||
)
|
||||
)).reverse();
|
||||
|
||||
return objectPath;
|
||||
}
|
||||
|
||||
isObjectPathToALink(domainObject, objectPath) {
|
||||
return objectPath !== undefined
|
||||
&& objectPath.length > 1
|
||||
|
@ -31,6 +31,7 @@ export default class DuplicateAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@ -45,7 +46,9 @@ export default class DuplicateAction {
|
||||
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
|
||||
}
|
||||
|
||||
onSave(changes) {
|
||||
async onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
@ -59,7 +62,9 @@ export default class DuplicateAction {
|
||||
const parentDomainObjectpath = changes.location || [this.parent];
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
return duplicationTask.duplicate(this.object, parent);
|
||||
await duplicationTask.duplicate(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
}
|
||||
|
||||
showForm(domainObject, parentDomainObject) {
|
||||
@ -142,4 +147,20 @@ export default class DuplicateAction {
|
||||
&& parentType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ export default class LinkAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
@ -48,7 +49,9 @@ export default class LinkAction {
|
||||
}
|
||||
|
||||
onSave(changes) {
|
||||
let inNavigationPath = this.inNavigationPath();
|
||||
this.startTransaction();
|
||||
|
||||
const inNavigationPath = this.inNavigationPath();
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
@ -57,6 +60,8 @@ export default class LinkAction {
|
||||
const parent = parentDomainObjectpath[0];
|
||||
|
||||
this.linkInNewParent(this.object, parent);
|
||||
|
||||
return this.saveTransaction();
|
||||
}
|
||||
|
||||
linkInNewParent(child, newParent) {
|
||||
@ -128,4 +133,19 @@ export default class LinkAction {
|
||||
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);
|
||||
};
|
||||
}
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export default class MoveAction {
|
||||
this.priority = 7;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.transaction = null;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
@ -60,6 +61,8 @@ export default class MoveAction {
|
||||
}
|
||||
|
||||
async onSave(changes) {
|
||||
this.startTransaction();
|
||||
|
||||
let inNavigationPath = this.inNavigationPath(this.object);
|
||||
if (inNavigationPath && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
@ -99,6 +102,8 @@ export default class MoveAction {
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveTransaction();
|
||||
|
||||
this.navigateTo(newObjectPath);
|
||||
}
|
||||
|
||||
@ -189,4 +194,20 @@ export default class MoveAction {
|
||||
&& childType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
async saveTransaction() {
|
||||
if (!this.transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.transaction.commit();
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@
|
||||
@import "../ui/layout/mct-tree.scss";
|
||||
@import "../ui/layout/search/search.scss";
|
||||
@import "../ui/layout/pane.scss";
|
||||
@import "../ui/layout/recent-objects.scss";
|
||||
@import "../ui/layout/status-bar/indicators.scss";
|
||||
@import "../ui/layout/status-bar/notification-banner.scss";
|
||||
@import "../ui/preview/preview.scss";
|
||||
|
@ -60,8 +60,15 @@ export function identifierToString(openmct, objectPath) {
|
||||
return '#/browse/' + openmct.objects.getRelativePath(objectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../../openmct').OpenMCT} openmct
|
||||
* @param {Array<import('../api/objects/ObjectAPI').DomainObject>} objectPath
|
||||
* @param {any} customUrlParams
|
||||
* @returns {string} url
|
||||
*/
|
||||
export default function objectPathToUrl(openmct, objectPath, customUrlParams = {}) {
|
||||
let url = identifierToString(openmct, objectPath);
|
||||
|
||||
let urlParams = paramsToArray(openmct, customUrlParams);
|
||||
if (urlParams.length) {
|
||||
url += '?' + urlParams.join('&');
|
||||
|
@ -26,7 +26,7 @@ describe('the url tool', function () {
|
||||
];
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', () => {
|
||||
openmct.router.setPath(' http://localhost:8020/foobar?testParam1=testValue1');
|
||||
openmct.router.setPath('/browse/mine?testParam1=testValue1');
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
|
@ -22,11 +22,11 @@
|
||||
|
||||
<template>
|
||||
<ul
|
||||
v-if="orderedOriginalPath.length"
|
||||
v-if="orderedPath.length"
|
||||
class="c-location"
|
||||
>
|
||||
<li
|
||||
v-for="pathObject in orderedOriginalPath"
|
||||
v-for="pathObject in orderedPath"
|
||||
:key="pathObject.key"
|
||||
class="c-location__item"
|
||||
>
|
||||
@ -65,11 +65,17 @@ export default {
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
orderedOriginalPath: []
|
||||
orderedPath: []
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
@ -79,9 +85,14 @@ export default {
|
||||
this.keyString = keyString;
|
||||
this.originalPath = [];
|
||||
|
||||
const rawOriginalPath = await this.openmct.objects.getOriginalPath(keyString);
|
||||
let rawPath = null;
|
||||
if (this.objectPath === null) {
|
||||
rawPath = await this.openmct.objects.getOriginalPath(keyString);
|
||||
} else {
|
||||
rawPath = this.objectPath;
|
||||
}
|
||||
|
||||
const pathWithDomainObject = rawOriginalPath.map((domainObject, index, pathArray) => {
|
||||
const pathWithDomainObject = rawPath.map((domainObject, index, pathArray) => {
|
||||
let key = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const objectPath = pathArray.slice(index);
|
||||
|
||||
@ -93,10 +104,10 @@ export default {
|
||||
});
|
||||
if (this.showObjectItself) {
|
||||
// remove ROOT only
|
||||
this.orderedOriginalPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();
|
||||
this.orderedPath = pathWithDomainObject.slice(0, pathWithDomainObject.length - 1).reverse();
|
||||
} else {
|
||||
// remove ROOT and object itself from path
|
||||
this.orderedOriginalPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
|
||||
this.orderedPath = pathWithDomainObject.slice(1, pathWithDomainObject.length - 1).reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
&__type-icon {
|
||||
width: auto;
|
||||
font-size: 1em;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -53,11 +53,12 @@
|
||||
type="horizontal"
|
||||
>
|
||||
<pane
|
||||
id="tree-pane"
|
||||
class="l-shell__pane-tree"
|
||||
style="width: 300px;"
|
||||
handle="after"
|
||||
label="Browse"
|
||||
hide-param="hideTree"
|
||||
:persist-position="true"
|
||||
@start-resizing="onStartResizing"
|
||||
@end-resizing="onEndResizing"
|
||||
>
|
||||
@ -75,11 +76,30 @@
|
||||
@click="handleSyncTreeNavigation"
|
||||
>
|
||||
</button>
|
||||
<mct-tree
|
||||
:sync-tree-navigation="triggerSync"
|
||||
:reset-tree-navigation="triggerReset"
|
||||
class="l-shell__tree"
|
||||
/>
|
||||
<multipane
|
||||
type="vertical"
|
||||
>
|
||||
<pane
|
||||
id="tree-pane"
|
||||
>
|
||||
<mct-tree
|
||||
ref="mctTree"
|
||||
:sync-tree-navigation="triggerSync"
|
||||
:reset-tree-navigation="triggerReset"
|
||||
class="l-shell__tree"
|
||||
/>
|
||||
</pane>
|
||||
<pane
|
||||
handle="before"
|
||||
label="Recently Viewed"
|
||||
:persist-position="true"
|
||||
>
|
||||
<RecentObjectsList
|
||||
class="l-shell__tree"
|
||||
@openAndScrollTo="openAndScrollTo($event)"
|
||||
/>
|
||||
</pane>
|
||||
</multipane>
|
||||
</pane>
|
||||
<pane class="l-shell__pane-main">
|
||||
<browse-bar
|
||||
@ -109,6 +129,7 @@
|
||||
handle="before"
|
||||
label="Inspect"
|
||||
hide-param="hideInspector"
|
||||
:persist-position="true"
|
||||
@start-resizing="onStartResizing"
|
||||
@end-resizing="onEndResizing"
|
||||
>
|
||||
@ -134,6 +155,7 @@ import Toolbar from '../toolbar/Toolbar.vue';
|
||||
import AppLogo from './AppLogo.vue';
|
||||
import Indicators from './status-bar/Indicators.vue';
|
||||
import NotificationBanner from './status-bar/NotificationBanner.vue';
|
||||
import RecentObjectsList from './RecentObjectsList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -148,7 +170,8 @@ export default {
|
||||
Toolbar,
|
||||
AppLogo,
|
||||
Indicators,
|
||||
NotificationBanner
|
||||
NotificationBanner,
|
||||
RecentObjectsList
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data: function () {
|
||||
@ -245,6 +268,10 @@ export default {
|
||||
|
||||
this.hasToolbar = structure.length > 0;
|
||||
},
|
||||
openAndScrollTo(navigationPath) {
|
||||
this.$refs.mctTree.openAndScrollTo(navigationPath);
|
||||
this.$refs.mctTree.targetedPath = navigationPath;
|
||||
},
|
||||
setActionCollection(actionCollection) {
|
||||
this.actionCollection = actionCollection;
|
||||
},
|
||||
|
200
src/ui/layout/RecentObjectsList.vue
Normal file
200
src/ui/layout/RecentObjectsList.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div
|
||||
class="c-tree-and-search l-shell__tree"
|
||||
>
|
||||
<ul
|
||||
class="c-tree-and-search__tree c-tree c-tree__scrollable"
|
||||
>
|
||||
<recent-objects-list-item
|
||||
v-for="(recentObject) in recentObjects"
|
||||
:key="recentObject.navigationPath"
|
||||
:object-path="recentObject.objectPath"
|
||||
:navigation-path="recentObject.navigationPath"
|
||||
:domain-object="recentObject.domainObject"
|
||||
@openAndScrollTo="openAndScrollTo($event)"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const MAX_RECENT_ITEMS = 20;
|
||||
const LOCAL_STORAGE_KEY__RECENT_OBJECTS = 'mct-recent-objects';
|
||||
import RecentObjectsListItem from './RecentObjectsListItem.vue';
|
||||
export default {
|
||||
name: 'RecentObjectsList',
|
||||
components: {
|
||||
RecentObjectsListItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
recents: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
recentObjects() {
|
||||
return this.recents.filter((recentObject) => {
|
||||
return recentObject.location !== null;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.compositionCollections = {};
|
||||
this.openmct.router.on('change:path', this.onPathChange);
|
||||
this.getSavedRecentItems();
|
||||
},
|
||||
destroyed() {
|
||||
this.openmct.router.off('change:path', this.onPathChange);
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Add a composition collection to the map and register its remove handler
|
||||
* @param {string} navigationPath
|
||||
*/
|
||||
addCompositionListenerFor(navigationPath) {
|
||||
this.compositionCollections[navigationPath].removeHandler = this.compositionRemoveHandler(navigationPath);
|
||||
this.compositionCollections[navigationPath].collection.on('remove',
|
||||
this.compositionCollections[navigationPath].removeHandler);
|
||||
},
|
||||
/**
|
||||
* Handler for composition collection remove events.
|
||||
* Removes the object and any of its children from the recents list.
|
||||
* @param {string} navigationPath
|
||||
*/
|
||||
compositionRemoveHandler(navigationPath) {
|
||||
/**
|
||||
* @param {import('../../api/objects/ObjectAPI').Identifier | string} identifier
|
||||
*/
|
||||
return (identifier) => {
|
||||
// Construct the navigationPath of the removed object itself
|
||||
const removedNavigationPath = `${navigationPath}/${this.openmct.objects.makeKeyString(identifier)}`;
|
||||
|
||||
// Remove the object and any of its children from the recents list
|
||||
this.recents = this.recents.filter((recentObject) => {
|
||||
return !recentObject.navigationPath.includes(removedNavigationPath);
|
||||
});
|
||||
|
||||
this.removeCompositionListenerFor(removedNavigationPath);
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Restores the RecentObjects list from localStorage, retrieves composition collections,
|
||||
* and registers composition listeners for composable objects.
|
||||
*/
|
||||
getSavedRecentItems() {
|
||||
const savedRecentsString = localStorage.getItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);
|
||||
const savedRecents = savedRecentsString ? JSON.parse(savedRecentsString) : [];
|
||||
|
||||
// Get composition collections and add composition listeners for composable objects
|
||||
savedRecents.forEach((recentObject) => {
|
||||
const { domainObject, navigationPath } = recentObject;
|
||||
if (this.shouldTrackCompositionFor(domainObject)) {
|
||||
this.compositionCollections[navigationPath] = {};
|
||||
this.compositionCollections[navigationPath].collection = this.openmct.composition.get(domainObject);
|
||||
this.addCompositionListenerFor(navigationPath);
|
||||
}
|
||||
});
|
||||
|
||||
this.recents = savedRecents;
|
||||
},
|
||||
/**
|
||||
* Handler for 'change:path' router events.
|
||||
* Adds or moves to the top the object at the given path to the recents list.
|
||||
* Registers compositionCollection listeners for composable objects.
|
||||
* Enforces the MAX_RECENT_ITEMS limit.
|
||||
* @param {string} navigationPath
|
||||
*/
|
||||
async onPathChange(navigationPath) {
|
||||
// Short-circuit if the path is not a navigationPath
|
||||
if (!navigationPath.startsWith('/browse/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectPath = await this.openmct.objects.getRelativeObjectPath(navigationPath);
|
||||
if (!objectPath.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const domainObject = objectPath[0];
|
||||
|
||||
// Get rid of '/ROOT' if it exists in the navigationPath.
|
||||
// Handles for the case of navigating to "My Items" from a RecentObjectsListItem.
|
||||
// Could lead to dupes of "My Items" in the RecentObjectsList if we don't drop the 'ROOT' here.
|
||||
if (navigationPath.includes('/ROOT')) {
|
||||
navigationPath = navigationPath.replace('/ROOT', '');
|
||||
}
|
||||
|
||||
if (this.shouldTrackCompositionFor(domainObject, navigationPath)) {
|
||||
this.compositionCollections[navigationPath] = {};
|
||||
this.compositionCollections[navigationPath].collection = this.openmct.composition.get(domainObject);
|
||||
this.addCompositionListenerFor(navigationPath);
|
||||
}
|
||||
|
||||
// Don't add deleted objects to the recents list
|
||||
if (domainObject?.location === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the object to the top if its already existing in the recents list
|
||||
const existingIndex = this.recents.findIndex((recentObject) => {
|
||||
return navigationPath === recentObject.navigationPath;
|
||||
});
|
||||
if (existingIndex !== -1) {
|
||||
this.recents.splice(existingIndex, 1);
|
||||
}
|
||||
|
||||
this.recents.unshift({
|
||||
objectPath,
|
||||
navigationPath,
|
||||
domainObject
|
||||
});
|
||||
|
||||
// Enforce a max number of recent items
|
||||
while (this.recents.length > MAX_RECENT_ITEMS) {
|
||||
const poppedRecentItem = this.recents.pop();
|
||||
this.removeCompositionListenerFor(poppedRecentItem.navigationPath);
|
||||
}
|
||||
|
||||
this.setSavedRecentItems();
|
||||
},
|
||||
/**
|
||||
* Delete the composition collection and unregister its remove handler
|
||||
* @param {string} navigationPath
|
||||
*/
|
||||
removeCompositionListenerFor(navigationPath) {
|
||||
if (this.compositionCollections[navigationPath]) {
|
||||
this.compositionCollections[navigationPath].collection.off('remove',
|
||||
this.compositionCollections[navigationPath].removeHandler);
|
||||
delete this.compositionCollections[navigationPath];
|
||||
}
|
||||
},
|
||||
openAndScrollTo(navigationPath) {
|
||||
this.$emit("openAndScrollTo", navigationPath);
|
||||
},
|
||||
/**
|
||||
* Saves the Recent Objects list to localStorage.
|
||||
*/
|
||||
setSavedRecentItems() {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS, JSON.stringify(this.recents));
|
||||
},
|
||||
/**
|
||||
* Returns true if the `domainObject` supports composition and we are not already
|
||||
* tracking its composition.
|
||||
* @param {import('../../api/objects/ObjectAPI').DomainObject} domainObject
|
||||
* @param {string} navigationPath
|
||||
*/
|
||||
shouldTrackCompositionFor(domainObject, navigationPath) {
|
||||
return this.compositionCollections[navigationPath] === undefined
|
||||
&& this.openmct.composition.supportsComposition(domainObject);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
134
src/ui/layout/RecentObjectsListItem.vue
Normal file
134
src/ui/layout/RecentObjectsListItem.vue
Normal file
@ -0,0 +1,134 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="c-recentobjects-listitem c-recentobjects-listitem--object"
|
||||
:class="isAlias"
|
||||
:aria-label="`${domainObject.name}`"
|
||||
>
|
||||
<div
|
||||
class="c-recentobjects-listitem__type-icon recent-object-icon"
|
||||
:class="resultTypeIcon"
|
||||
></div>
|
||||
<div
|
||||
class="c-recentobjects-listitem__body"
|
||||
>
|
||||
<span
|
||||
class="c-recentobjects-listitem__title"
|
||||
:name="domainObject.name"
|
||||
draggable="true"
|
||||
@dragstart="dragStart"
|
||||
@click.prevent="clickedRecent"
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</span>
|
||||
|
||||
<ObjectPath
|
||||
class="c-recentobjects-listitem__object-path"
|
||||
:read-only="false"
|
||||
:domain-object="domainObject"
|
||||
:show-original-path="false"
|
||||
:object-path="objectPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="c-recentobjects-listitem__target-button">
|
||||
<button
|
||||
class="c-icon-button icon-target"
|
||||
@click="openAndScrollTo(navigationPath)"
|
||||
></button>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ObjectPath from '../components/ObjectPath.vue';
|
||||
import PreviewAction from '../preview/PreviewAction';
|
||||
|
||||
export default {
|
||||
name: 'RecentObjectsListItem',
|
||||
components: {
|
||||
ObjectPath
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
navigationPath: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isAlias() {
|
||||
return this.openmct.objects.isObjectPathToALink(this.domainObject, this.objectPath) ? { 'is-alias': true } : undefined;
|
||||
},
|
||||
resultTypeIcon() {
|
||||
return this.openmct.types.get(this.domainObject.type).definition.cssClass;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.previewAction.on('isVisible', this.togglePreviewState);
|
||||
},
|
||||
destroyed() {
|
||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||
},
|
||||
methods: {
|
||||
clickedRecent(_event) {
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
this.preview();
|
||||
} else {
|
||||
this.openmct.router.navigate(`#${this.navigationPath}`);
|
||||
}
|
||||
},
|
||||
togglePreviewState(previewState) {
|
||||
this.$emit('preview-changed', previewState);
|
||||
},
|
||||
preview() {
|
||||
if (this.previewAction.appliesTo(this.objectPath)) {
|
||||
this.previewAction.invoke(this.objectPath);
|
||||
}
|
||||
},
|
||||
dragStart(event) {
|
||||
const navigatedObject = this.openmct.router.path[0];
|
||||
const serializedPath = JSON.stringify(this.objectPath);
|
||||
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
if (this.openmct.composition.checkPolicy(navigatedObject, this.domainObject)) {
|
||||
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject));
|
||||
}
|
||||
|
||||
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
|
||||
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
|
||||
},
|
||||
openAndScrollTo(navigationPath) {
|
||||
this.$emit('openAndScrollTo', navigationPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -289,7 +289,7 @@
|
||||
}
|
||||
|
||||
&__pane-tree {
|
||||
width: 300px;
|
||||
width: 100%;
|
||||
padding-left: nth($shellPanePad, 2);
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,10 @@
|
||||
color: $colorItemTreeSelectedFg;
|
||||
}
|
||||
}
|
||||
&.is-targeted-item {
|
||||
$c: $colorBodyFg;
|
||||
@include pulseProp($animName: flashTarget, $dur: 500ms, $iter: 8, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0));
|
||||
}
|
||||
|
||||
&.is-new {
|
||||
animation-name: animTemporaryHighlight;
|
||||
|
@ -88,10 +88,12 @@
|
||||
:item-height="itemHeight"
|
||||
:open-items="openTreeItems"
|
||||
:loading-items="treeItemLoading"
|
||||
:targeted-path="targetedPath"
|
||||
@tree-item-mounted="scrollToCheck($event)"
|
||||
@tree-item-destroyed="removeCompositionListenerFor($event)"
|
||||
@tree-item-action="treeItemAction(treeItem, $event)"
|
||||
@tree-item-selection="treeItemSelection(treeItem)"
|
||||
@targeted-path-animation-end="targetedPathAnimationEnd()"
|
||||
/>
|
||||
<!-- main loading -->
|
||||
<div
|
||||
@ -174,19 +176,18 @@ export default {
|
||||
itemOffset: 0,
|
||||
activeSearch: false,
|
||||
mainTreeTopMargin: undefined,
|
||||
selectedItem: {}
|
||||
selectedItem: {},
|
||||
targetedPath: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
childrenHeight() {
|
||||
let childrenCount = this.focusedItems.length || 1;
|
||||
const childrenCount = this.focusedItems.length || 1;
|
||||
|
||||
return (this.itemHeight * childrenCount) - this.mainTreeTopMargin; // 5px margin
|
||||
},
|
||||
childrenHeightStyles() {
|
||||
let height = this.childrenHeight + 'px';
|
||||
|
||||
return { height };
|
||||
return { height: `${this.childrenHeight}px` };
|
||||
},
|
||||
focusedItems() {
|
||||
return this.activeSearch ? this.searchResultItems : this.treeItems;
|
||||
@ -195,9 +196,7 @@ export default {
|
||||
return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER;
|
||||
},
|
||||
scrollableStyles() {
|
||||
let height = this.mainTreeHeight + 'px';
|
||||
|
||||
return { height };
|
||||
return { height: `${this.mainTreeHeight}px` };
|
||||
},
|
||||
showNoItems() {
|
||||
return this.visibleItems.length === 0 && !this.activeSearch && this.searchValue === '' && !this.isLoading;
|
||||
@ -209,7 +208,7 @@ export default {
|
||||
if (!this.isSelectorTree) {
|
||||
return {};
|
||||
} else {
|
||||
return { 'min-height': this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT + 'px' };
|
||||
return { minHeight: `${this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT}px`};
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -311,6 +310,9 @@ export default {
|
||||
this.openTreeItem(parentItem);
|
||||
}
|
||||
},
|
||||
targetedPathAnimationEnd() {
|
||||
this.targetedPath = undefined;
|
||||
},
|
||||
treeItemSelection(item) {
|
||||
this.selectedItem = item;
|
||||
this.$emit('tree-item-selection', item);
|
||||
@ -457,6 +459,9 @@ export default {
|
||||
|
||||
this.treeItemSelection(item);
|
||||
}
|
||||
|
||||
this.scrollToCheck(navigationPath);
|
||||
this.scrollToPath = null;
|
||||
});
|
||||
},
|
||||
scrollToCheck(navigationPath) {
|
||||
@ -480,9 +485,9 @@ export default {
|
||||
behavior: 'smooth'
|
||||
});
|
||||
} else if (this.scrollToPath) {
|
||||
this.scrollToPath = undefined;
|
||||
delete this.scrollToPath;
|
||||
this.scrollToPath = null;
|
||||
}
|
||||
|
||||
},
|
||||
scrollEndEvent() {
|
||||
if (!this.$refs.scrollable) {
|
||||
@ -494,12 +499,14 @@ export default {
|
||||
if (!this.isItemInView(this.scrollToPath)) {
|
||||
this.scrollTo(this.scrollToPath);
|
||||
} else {
|
||||
this.scrollToPath = undefined;
|
||||
delete this.scrollToPath;
|
||||
this.scrollToPath = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setTargetedItem(navigationPath) {
|
||||
this.targetedItem = navigationPath;
|
||||
},
|
||||
isItemInView(navigationPath) {
|
||||
const indexOfScroll = this.treeItems.findIndex(item => item.navigationPath === navigationPath);
|
||||
const scrollTopAmount = indexOfScroll * this.itemHeight;
|
||||
|
@ -83,6 +83,8 @@
|
||||
&[class*="--vertical"] {
|
||||
padding-top: $interiorMargin;
|
||||
padding-bottom: $interiorMargin;
|
||||
min-height: 30px; // For Recents holder
|
||||
|
||||
&.l-pane--collapsed {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
|
@ -1,20 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="l-pane"
|
||||
:class="{
|
||||
'l-pane--horizontal-handle-before': type === 'horizontal' && handle === 'before',
|
||||
'l-pane--horizontal-handle-after': type === 'horizontal' && handle === 'after',
|
||||
'l-pane--vertical-handle-before': type === 'vertical' && handle === 'before',
|
||||
'l-pane--vertical-handle-after': type === 'vertical' && handle === 'after',
|
||||
'l-pane--collapsed': collapsed,
|
||||
'l-pane--reacts': !handle,
|
||||
'l-pane--resizing': resizing === true
|
||||
}"
|
||||
:class="paneClasses"
|
||||
>
|
||||
<div
|
||||
v-if="handle"
|
||||
class="l-pane__handle"
|
||||
@mousedown="start"
|
||||
@mousedown.prevent="startResizing"
|
||||
></div>
|
||||
<div class="l-pane__header">
|
||||
<span
|
||||
@ -42,6 +34,7 @@
|
||||
|
||||
<script>
|
||||
const COLLAPSE_THRESHOLD_PX = 40;
|
||||
const LOCAL_STORAGE_KEY__PANE_POSITIONS = 'mct-pane-positions';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@ -60,6 +53,10 @@ export default {
|
||||
hideParam: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
persistPosition: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -70,7 +67,25 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isCollapsable() {
|
||||
return this.hideParam && this.hideParam.length > 0;
|
||||
return this.hideParam?.length > 0;
|
||||
},
|
||||
localStorageKey() {
|
||||
if (!this.label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.label.toLowerCase().replace(/ /g, '-');
|
||||
},
|
||||
paneClasses() {
|
||||
return {
|
||||
'l-pane--horizontal-handle-before': this.type === 'horizontal' && this.handle === 'before',
|
||||
'l-pane--horizontal-handle-after': this.type === 'horizontal' && this.handle === 'after',
|
||||
'l-pane--vertical-handle-before': this.type === 'vertical' && this.handle === 'before',
|
||||
'l-pane--vertical-handle-after': this.type === 'vertical' && this.handle === 'after',
|
||||
'l-pane--collapsed': this.collapsed,
|
||||
'l-pane--reacts': !this.handle,
|
||||
'l-pane--resizing': this.resizing === true
|
||||
};
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
@ -78,62 +93,33 @@ export default {
|
||||
this.styleProp = (this.type === 'horizontal') ? 'width' : 'height';
|
||||
},
|
||||
async mounted() {
|
||||
if (this.persistPosition) {
|
||||
const savedPosition = this.getSavedPosition();
|
||||
if (savedPosition) {
|
||||
this.$el.style[this.styleProp] = savedPosition;
|
||||
}
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
// Hide tree and/or inspector pane if specified in URL
|
||||
if (this.isCollapsable) {
|
||||
this.handleHideUrl();
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
toggleCollapse: function (e) {
|
||||
if (this.collapsed) {
|
||||
this.handleExpand();
|
||||
this.removeHideParam(this.hideParam);
|
||||
} else {
|
||||
this.handleCollapse();
|
||||
this.addHideParam(this.hideParam);
|
||||
}
|
||||
},
|
||||
handleHideUrl: function () {
|
||||
const hideParam = this.openmct.router.getSearchParam(this.hideParam);
|
||||
|
||||
if (hideParam === 'true') {
|
||||
this.handleCollapse();
|
||||
}
|
||||
},
|
||||
addHideParam: function (target) {
|
||||
addHideParam(target) {
|
||||
this.openmct.router.setSearchParam(target, 'true');
|
||||
},
|
||||
removeHideParam: function (target) {
|
||||
this.openmct.router.deleteSearchParam(target);
|
||||
endResizing(_event) {
|
||||
document.body.removeEventListener('mousemove', this.updatePosition);
|
||||
document.body.removeEventListener('mouseup', this.endResizing);
|
||||
this.resizing = false;
|
||||
this.$emit('end-resizing');
|
||||
this.trackSize();
|
||||
},
|
||||
handleCollapse: function () {
|
||||
this.currentSize = (this.dragCollapse === true) ? this.initial : this.$el.style[this.styleProp];
|
||||
this.$el.style[this.styleProp] = '';
|
||||
this.collapsed = true;
|
||||
},
|
||||
handleExpand: function () {
|
||||
this.$el.style[this.styleProp] = this.currentSize;
|
||||
delete this.currentSize;
|
||||
delete this.dragCollapse;
|
||||
this.collapsed = false;
|
||||
},
|
||||
trackSize: function () {
|
||||
if (!this.dragCollapse === true) {
|
||||
if (this.type === 'vertical') {
|
||||
this.initial = this.$el.offsetHeight;
|
||||
} else if (this.type === 'horizontal') {
|
||||
this.initial = this.$el.offsetWidth;
|
||||
}
|
||||
}
|
||||
},
|
||||
getPosition: function (event) {
|
||||
return this.type === 'horizontal'
|
||||
? event.pageX
|
||||
: event.pageY;
|
||||
},
|
||||
getNewSize: function (event) {
|
||||
let delta = this.startPosition - this.getPosition(event);
|
||||
getNewSize(event) {
|
||||
const delta = this.startPosition - this.getPosition(event);
|
||||
if (this.handle === "before") {
|
||||
return `${this.initial + delta}px`;
|
||||
}
|
||||
@ -142,33 +128,88 @@ export default {
|
||||
return `${this.initial - delta}px`;
|
||||
}
|
||||
},
|
||||
updatePosition: function (event) {
|
||||
let size = this.getNewSize(event);
|
||||
let intSize = parseInt(size.substr(0, size.length - 2), 10);
|
||||
if (intSize < COLLAPSE_THRESHOLD_PX && this.isCollapsable === true) {
|
||||
this.dragCollapse = true;
|
||||
this.end();
|
||||
this.toggleCollapse();
|
||||
} else {
|
||||
this.$el.style[this.styleProp] = size;
|
||||
getSavedPosition() {
|
||||
if (!this.localStorageKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const savedPositionsString = localStorage.getItem(LOCAL_STORAGE_KEY__PANE_POSITIONS);
|
||||
const savedPositions = savedPositionsString ? JSON.parse(savedPositionsString) : {};
|
||||
|
||||
return savedPositions[this.localStorageKey];
|
||||
},
|
||||
getPosition(event) {
|
||||
return this.type === 'horizontal'
|
||||
? event.pageX
|
||||
: event.pageY;
|
||||
},
|
||||
handleCollapse() {
|
||||
this.currentSize = (this.dragCollapse === true) ? this.initial : this.$el.style[this.styleProp];
|
||||
this.$el.style[this.styleProp] = '';
|
||||
this.collapsed = true;
|
||||
},
|
||||
handleExpand() {
|
||||
this.$el.style[this.styleProp] = this.currentSize;
|
||||
delete this.currentSize;
|
||||
delete this.dragCollapse;
|
||||
this.collapsed = false;
|
||||
},
|
||||
handleHideUrl() {
|
||||
const hideParam = this.openmct.router.getSearchParam(this.hideParam);
|
||||
|
||||
if (hideParam === 'true') {
|
||||
this.handleCollapse();
|
||||
}
|
||||
},
|
||||
start: function (event) {
|
||||
event.preventDefault(); // stop from firing drag event
|
||||
|
||||
removeHideParam(target) {
|
||||
this.openmct.router.deleteSearchParam(target);
|
||||
},
|
||||
setSavedPosition(panePosition) {
|
||||
const panePositionsString = localStorage.getItem(LOCAL_STORAGE_KEY__PANE_POSITIONS);
|
||||
const panePositions = panePositionsString ? JSON.parse(panePositionsString) : {};
|
||||
panePositions[this.localStorageKey] = panePosition;
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY__PANE_POSITIONS, JSON.stringify(panePositions));
|
||||
},
|
||||
startResizing(event) {
|
||||
this.startPosition = this.getPosition(event);
|
||||
document.body.addEventListener('mousemove', this.updatePosition);
|
||||
document.body.addEventListener('mouseup', this.end);
|
||||
document.body.addEventListener('mouseup', this.endResizing);
|
||||
this.resizing = true;
|
||||
this.$emit('start-resizing');
|
||||
this.trackSize();
|
||||
},
|
||||
end: function (event) {
|
||||
document.body.removeEventListener('mousemove', this.updatePosition);
|
||||
document.body.removeEventListener('mouseup', this.end);
|
||||
this.resizing = false;
|
||||
this.$emit('end-resizing');
|
||||
this.trackSize();
|
||||
toggleCollapse(_event) {
|
||||
if (this.collapsed) {
|
||||
this.handleExpand();
|
||||
this.removeHideParam(this.hideParam);
|
||||
} else {
|
||||
this.handleCollapse();
|
||||
this.addHideParam(this.hideParam);
|
||||
}
|
||||
},
|
||||
trackSize() {
|
||||
if (!this.dragCollapse) {
|
||||
if (this.type === 'vertical') {
|
||||
this.initial = this.$el.offsetHeight;
|
||||
} else if (this.type === 'horizontal') {
|
||||
this.initial = this.$el.offsetWidth;
|
||||
}
|
||||
|
||||
if (this.persistPosition) {
|
||||
this.setSavedPosition(`${this.initial}px`);
|
||||
}
|
||||
}
|
||||
},
|
||||
updatePosition(event) {
|
||||
const size = this.getNewSize(event);
|
||||
const intSize = parseInt(size.substr(0, size.length - 2), 10);
|
||||
if (intSize < COLLAPSE_THRESHOLD_PX && this.isCollapsable === true) {
|
||||
this.dragCollapse = true;
|
||||
this.endResizing();
|
||||
this.toggleCollapse();
|
||||
} else {
|
||||
this.$el.style[this.styleProp] = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
121
src/ui/layout/recent-objects.scss
Normal file
121
src/ui/layout/recent-objects.scss
Normal file
@ -0,0 +1,121 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
.c-recentobjects-listitem {
|
||||
display: flex;
|
||||
padding: $interiorMargin $interiorMarginSm;
|
||||
align-items: flex-start;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
|
||||
+ .c-recentobjects-listitem {
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
}
|
||||
|
||||
&.is-alias {
|
||||
// Object is an alias to an original.
|
||||
[class~='recent-object-icon'] {
|
||||
@include isAlias();
|
||||
&:after {
|
||||
bottom: 20%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__object-path {
|
||||
padding: 0 $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__target-button{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&__type-icon,
|
||||
&__more-options-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__type-icon {
|
||||
color: $colorItemTreeIcon;
|
||||
font-size: 2.2em;
|
||||
|
||||
// TEMP: uses object-label component, hide label part
|
||||
.c-object-label__name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__more-options-button {
|
||||
display: none; // TEMP until enabled
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1 1 auto;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
|
||||
.c-location {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
|
||||
&__item {
|
||||
|
||||
> * + * {
|
||||
background: blue !important;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
border-radius: $basicCr;
|
||||
color: pullForward($colorBodyFg, 30%);
|
||||
cursor: pointer;
|
||||
padding: $interiorMarginSm;
|
||||
|
||||
&:hover {
|
||||
background-color: $colorItemTreeHoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
.c-tag {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.c-recentobjects-listitem:hover .c-recentobjects-listitem__target-button {
|
||||
opacity: 100;
|
||||
}
|
@ -67,7 +67,7 @@
|
||||
|
||||
<script>
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import objectPathToUrl from '../../../tools/url';
|
||||
import { identifierToString } from '../../../../src/tools/url';
|
||||
|
||||
export default {
|
||||
name: 'AnnotationSearchResult',
|
||||
@ -128,11 +128,7 @@ export default {
|
||||
methods: {
|
||||
clickedResult() {
|
||||
const objectPath = this.domainObject.originalPath;
|
||||
let resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||
// get rid of ROOT if extant
|
||||
if (resultUrl.includes('/ROOT')) {
|
||||
resultUrl = resultUrl.split('/ROOT').join('');
|
||||
}
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
},
|
||||
|
@ -104,7 +104,7 @@ export default {
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(keyStringForObject);
|
||||
|
||||
return {
|
||||
originalPath: originalPathObjects,
|
||||
objectPath: originalPathObjects,
|
||||
...domainObject
|
||||
};
|
||||
}));
|
||||
@ -126,7 +126,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.openmct.objects.isReachable(result?.originalPath);
|
||||
return this.openmct.objects.isReachable(result?.objectPath);
|
||||
});
|
||||
this.objectSearchResults = filterAnnotationsAndValidPaths;
|
||||
this.searchLoading = false;
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
<script>
|
||||
import ObjectPath from '../../components/ObjectPath.vue';
|
||||
import objectPathToUrl from '../../../tools/url';
|
||||
import identifierToString from '../../../tools/url';
|
||||
import PreviewAction from '../../preview/PreviewAction';
|
||||
|
||||
export default {
|
||||
@ -100,9 +100,10 @@ export default {
|
||||
event.preventDefault();
|
||||
this.preview();
|
||||
} else {
|
||||
const objectPath = this.result.originalPath;
|
||||
let resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||
// get rid of ROOT if extant
|
||||
const { objectPath } = this.result;
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
|
||||
// Remove the vestigial 'ROOT' identifier from url if it exists
|
||||
if (resultUrl.includes('/ROOT')) {
|
||||
resultUrl = resultUrl.split('/ROOT').join('');
|
||||
}
|
||||
@ -114,14 +115,14 @@ export default {
|
||||
this.$emit('preview-changed', previewState);
|
||||
},
|
||||
preview() {
|
||||
const objectPath = this.result.originalPath;
|
||||
const { objectPath } = this.result;
|
||||
if (this.previewAction.appliesTo(objectPath)) {
|
||||
this.previewAction.invoke(objectPath);
|
||||
}
|
||||
},
|
||||
dragStart(event) {
|
||||
const navigatedObject = this.openmct.router.path[0];
|
||||
const objectPath = this.result.originalPath;
|
||||
const { objectPath } = this.result;
|
||||
const serializedPath = JSON.stringify(objectPath);
|
||||
const keyString = this.openmct.objects.makeKeyString(this.result.identifier);
|
||||
if (this.openmct.composition.checkPolicy(navigatedObject, this.result)) {
|
||||
|
@ -9,10 +9,12 @@
|
||||
class="c-tree__item"
|
||||
:class="{
|
||||
'is-alias': isAlias,
|
||||
'is-navigated-object': shouldHightlight,
|
||||
'is-navigated-object': shouldHighlight,
|
||||
'is-targeted-item': isTargetedItem,
|
||||
'is-context-clicked': contextClickActive,
|
||||
'is-new': isNewItem
|
||||
}"
|
||||
@animationend="targetedPathAnimationEnd($event)"
|
||||
@click.capture="itemClick"
|
||||
@contextmenu.capture="handleContextMenu"
|
||||
>
|
||||
@ -58,6 +60,10 @@ export default {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
targetedPath: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
selectedItem: {
|
||||
type: Object,
|
||||
required: true
|
||||
@ -122,6 +128,9 @@ export default {
|
||||
isSelectedItem() {
|
||||
return this.selectedItem.objectPath === this.node.objectPath;
|
||||
},
|
||||
isTargetedItem() {
|
||||
return this.targetedPath === this.navigationPath;
|
||||
},
|
||||
isNewItem() {
|
||||
return this.isNew;
|
||||
},
|
||||
@ -131,7 +140,7 @@ export default {
|
||||
isOpen() {
|
||||
return this.openItems.includes(this.navigationPath);
|
||||
},
|
||||
shouldHightlight() {
|
||||
shouldHighlight() {
|
||||
if (this.isSelectorTree) {
|
||||
return this.isSelectedItem;
|
||||
} else {
|
||||
@ -164,6 +173,10 @@ export default {
|
||||
this.$emit('tree-item-destoyed', this.navigationPath);
|
||||
},
|
||||
methods: {
|
||||
targetedPathAnimationEnd($event) {
|
||||
$event.target.classList.remove('is-targeted-item');
|
||||
this.$emit('targeted-path-animation-end');
|
||||
},
|
||||
itemAction() {
|
||||
this.$emit('tree-item-action', this.isOpen || this.isLoading ? 'close' : 'open');
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import objectPathToUrl from '../../tools/url';
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
'objectPath': {
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
@ -20,7 +20,7 @@ export default {
|
||||
return '#' + this.navigateToPath;
|
||||
}
|
||||
|
||||
let url = objectPathToUrl(this.openmct, this.objectPath);
|
||||
const url = objectPathToUrl(this.openmct, this.objectPath);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
@ -130,11 +130,10 @@ class ApplicationRouter extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to given hash and update current location object and notify listeners about location change
|
||||
* Navigate to given hash, update current location object, and notify listeners about location change
|
||||
*
|
||||
* @param {string} paramName name of searchParam to get from current url searchParams
|
||||
*
|
||||
* @returns {string} value of paramName from current url searchParams
|
||||
* @param {string} hash The URL hash to navigate to in the form of "#/browse/mine/{keyString}/{keyString}".
|
||||
* Should not include any params.
|
||||
*/
|
||||
navigate(hash) {
|
||||
this.handleLocationChange(hash.substring(1));
|
||||
@ -227,7 +226,7 @@ class ApplicationRouter extends EventEmitter {
|
||||
|
||||
this.started = true;
|
||||
|
||||
this.locationBar.onChange(p => this.hashChaged(p));
|
||||
this.locationBar.onChange(p => this.hashChanged(p));
|
||||
this.locationBar.start({
|
||||
root: location.pathname
|
||||
});
|
||||
@ -390,7 +389,7 @@ class ApplicationRouter extends EventEmitter {
|
||||
*
|
||||
* @param {string} hash new hash for url
|
||||
*/
|
||||
hashChaged(hash) {
|
||||
hashChanged(hash) {
|
||||
this.emit('change:hash', hash);
|
||||
this.handleLocationChange(hash);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user