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:
Jesse Mazzella 2023-01-20 10:27:09 -08:00 committed by GitHub
parent 22621aaaf8
commit f98a2cdd6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1023 additions and 195 deletions

View File

@ -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}`);

View File

@ -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();
});
});

View File

@ -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

View File

@ -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();

View 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");
});

View File

@ -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();

View File

@ -22,6 +22,7 @@
<template>
<mct-tree
id="locator-tree"
:is-selector-tree="true"
:initial-selection="model.parent"
@tree-item-selection="handleItemSelection"

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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('&');

View File

@ -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();

View File

@ -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();
}
}
}

View File

@ -36,6 +36,7 @@
&__type-icon {
width: auto;
font-size: 1em;
min-width: auto;
}
&:hover {

View File

@ -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;
},

View 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>

View 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>

View File

@ -289,7 +289,7 @@
}
&__pane-tree {
width: 300px;
width: 100%;
padding-left: nth($shellPanePad, 2);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}
};

View 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;
}

View File

@ -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);
},

View File

@ -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;

View File

@ -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)) {

View File

@ -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');
},

View File

@ -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;
}

View File

@ -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);
}