Files
openmct/src/ui/layout/mct-tree.vue
Jesse Mazzella 16e1ac2529 Fixes for e2e tests following the Vue 3 compat upgrade (#6837)
* clock, timeConductor and appActions fixes

* Ensure realtime uses upstream context when available
Eliminate ambiguity when looking for time conductor locator

* Fix log plot e2e tests

* Fix displayLayout e2e tests

* Specify global time conductor to fix issues with duplicate selectors with independent time contexts

* a11y: ARIA for conductor and independent time conductor

* a11y: fix label collisions, specify 'Menu' in label

* Add watch mode

* fix(e2e): update appActions and tests to use a11y locators for ITC

* Don't remove the itc popup from the DOM. Just show/hide it once it's added the first time.

* test(e2e): disable one imagery test due to known bug

* Add fixme to tagging tests, issue described in 6822

* Fix locator for time conductor popups

* Improve how time bounds are set in independent time conductor.
Fix tests for flexible layout and timestrip

* Fix some tests for itc for display layouts

* Fix Inspector tabs remounting on change

* fix autoscale test and snapshot

* Fix telemetry table test

* Fix timestrip test

* e2e: move test info annotations to within test

* 6826: Fixes padStart error due to using it on a number rather than a string

* fix(e2e): update snapshots

* fix(e2e): fix restricted notebook locator

* fix(restrictedNotebook): fix issue causing sections not to update on lock

* fix(restrictedNotebook): fix issue causing snapshots to not be able to be deleted from a locked page

- Using `this.$delete(arr, index)` does not update the `length` property on the underlying target object, so it can lead to bizarre issues where your array is of length 4 but it has 3 objects in it.

* fix: replace all instances of `$delete` with `Array.splice()` or `delete`

* fix(e2e): fix grand search test

* fix(#3117): can remove item from displayLayout via tree context menu while viewing another item

* fix: remove typo 

* Wait for background image to load

* fix(#6832): timelist events can tick down

* fix: ensure that menuitems have the raw objects so emits work

* fix: assign new arrays instead of editing state in-place

* refactor(timelist): use `getClock()` instead of `clock()`

* Revert "refactor(timelist): use `getClock()` instead of `clock()`"

This reverts commit d888553112.

* refactor(timelist): use new timeAPI

* Stop ticking when the independent time context is disabled (#6833)

* Turn off the clock ticket for independent time conductor when it is disabled

* Fix linting issues

---------

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>

* test: update couchdb notebook test

* fix: codeQL warnings

* fix(tree-item): infinite spinner issue

- Using `indexOf()` with an object was failing due to some items in the tree being Proxy-wrapped and others not. So instead, use `findIndex()` with a predicate that compares the navigationPaths of both objects

* [Timer] Remove "refresh" call, it is not needed (#6841)

* removing an unneccessary refresh that waas causing many get requests
* lets just pretend this never happened

* fix(mct-tree): maintain reactivity of all tree items

* Hide change role button in the indicator in cases where there is only… (#6840)

Hide change role button in the indicator in cases where there is only a single role available for the current user

---------

Co-authored-by: Shefali <simplyrender@gmail.com>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: David Tsay <david.e.tsay@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2023-07-28 02:06:41 +00:00

1114 lines
34 KiB
Vue

<!--
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.
-->
<template>
<div
ref="treeContainer"
class="c-tree-and-search"
:class="{
'c-selector': isSelectorTree
}"
>
<div ref="search" class="c-tree-and-search__search">
<search
v-show="isSelectorTree"
ref="shell-search"
class="c-search"
:value="searchValue"
@input="searchTree"
@clear="searchTree"
/>
</div>
<!-- search loading -->
<div
v-if="searchLoading && activeSearch"
class="c-tree__item c-tree-and-search__loading loading"
>
<span class="c-tree__item__label">Searching...</span>
</div>
<!-- no results -->
<div v-if="showNoSearchResults" class="c-tree-and-search__no-results">No results found</div>
<!-- main tree -->
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
role="tree"
:aria-label="getAriaLabel"
aria-expanded="true"
>
<div
ref="dummyItem"
class="c-tree__item-h"
style="left: -1000px; position: absolute; visibility: hidden"
>
<div class="c-tree__item">
<span class="c-tree__item__view-control c-nav__up is-enabled"></span>
<a class="c-tree__item__label c-object-label" draggable="true" href="#">
<div class="c-tree__item__type-icon c-object-label__type-icon icon-folder">
<span title="Open MCT"></span>
</div>
<div class="c-tree__item__name c-object-label__name">Open MCT</div>
</a>
<span class="c-tree__item__view-control c-nav__down"></span>
</div>
</div>
<div
ref="scrollable"
class="c-tree__scrollable"
:style="scrollableStyles"
@scroll="updateVisibleItems()"
>
<div :style="childrenHeightStyles">
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="`${treeItem.navigationPath}-${index}`"
:node="treeItem"
:is-selector-tree="isSelectorTree"
:selected-item="selectedItem"
:active-search="activeSearch"
:left-offset="!activeSearch ? treeItem.leftOffset : '0px'"
:is-new="treeItem.isNew"
:item-offset="itemOffset"
:item-index="index"
: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 v-if="isLoading" class="c-tree__item c-tree-and-search__loading loading">
<span class="c-tree__item__label">Loading...</span>
</div>
<!-- end loading -->
<div v-if="showNoItems" class="c-tree__item c-tree__item--empty">No items</div>
</div>
</div>
</div>
<!-- end main tree -->
</div>
</template>
<script>
import _ from 'lodash';
import treeItem from './tree-item.vue';
import search from '../components/search.vue';
import { markRaw, reactive } from 'vue';
const ITEM_BUFFER = 25;
const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded';
const SORT_MY_ITEMS_ALPH_ASC = true;
const TREE_ITEM_INDENT_PX = 18;
const LOCATOR_ITEM_COUNT_HEIGHT = 10; // how many tree items to make the locator selection box show
export default {
name: 'MctTree',
components: {
search,
treeItem
},
inject: ['openmct'],
props: {
isSelectorTree: {
type: Boolean,
required: false,
default() {
return false;
}
},
initialSelection: {
type: Object,
required: false,
default() {
return {};
}
},
syncTreeNavigation: {
type: Boolean,
required: false
},
resetTreeNavigation: {
type: Boolean,
required: false
}
},
data() {
return {
isLoading: false,
treeItemLoading: {},
mainTreeHeight: undefined,
searchLoading: false,
searchValue: '',
treeItems: [],
openTreeItems: [],
compositionCollections: {},
searchResultItems: [],
visibleItems: [],
updatingView: false,
itemHeight: 27,
itemOffset: 0,
activeSearch: false,
mainTreeTopMargin: undefined,
selectedItem: {},
targetedPath: ''
};
},
computed: {
childrenHeight() {
const childrenCount = this.focusedItems.length || 1;
return this.itemHeight * childrenCount - this.mainTreeTopMargin; // 5px margin
},
childrenHeightStyles() {
return { height: `${this.childrenHeight}px` };
},
focusedItems() {
return this.activeSearch ? this.searchResultItems : this.treeItems;
},
getAriaLabel() {
return this.isSelectorTree ? 'Create Modal Tree' : 'Main Tree';
},
pageThreshold() {
return Math.ceil(this.mainTreeHeight / this.itemHeight) + ITEM_BUFFER;
},
scrollableStyles() {
return { height: `${this.mainTreeHeight}px` };
},
showNoItems() {
return (
this.visibleItems.length === 0 &&
!this.activeSearch &&
this.searchValue === '' &&
!this.isLoading
);
},
showNoSearchResults() {
return this.searchValue && this.searchResultItems.length === 0 && !this.searchLoading;
},
treeHeight() {
if (!this.isSelectorTree) {
return {};
} else {
return { minHeight: `${this.itemHeight * LOCATOR_ITEM_COUNT_HEIGHT}px` };
}
}
},
watch: {
syncTreeNavigation() {
this.searchValue = '';
// if there is an abort controller, then a search is in progress and will need to be canceled
if (this.abortSearchController) {
this.abortSearchController.abort();
delete this.abortSearchController;
}
if (!this.openmct.router.path) {
return;
}
this.$nextTick(this.showCurrentPathInTree);
},
resetTreeNavigation() {
[...this.openTreeItems].reverse().map(this.closeTreeItemByPath);
},
searchValue() {
if (this.searchValue !== '' && !this.activeSearch) {
this.activeSearch = true;
this.$refs.scrollable.scrollTop = 0;
} else if (this.searchValue === '') {
this.activeSearch = false;
}
},
mainTreeHeight() {
this.updateVisibleItems();
},
focusedItems: {
handler(val, oldVal) {
this.updateVisibleItems();
},
deep: true
},
openTreeItems: {
handler(val, oldVal) {
this.setSavedOpenItems();
},
deep: true
}
},
async mounted() {
this.initialize();
await this.loadRoot();
this.isLoading = false;
if (!this.isSelectorTree) {
await this.syncTreeOpenItems();
} else {
if (this.initialSelection.identifier) {
const objectPath = await this.openmct.objects.getOriginalPath(
this.initialSelection.identifier
);
const navigationPath = this.buildNavigationPath(objectPath);
this.openAndScrollTo(navigationPath);
}
}
},
created() {
this.getSearchResults = _.debounce(this.getSearchResults, 400);
this.handleTreeResize = _.debounce(this.handleTreeResize, 300);
this.scrollEndEvent = _.debounce(this.scrollEndEvent, 100);
},
unmounted() {
if (this.treeResizeObserver) {
this.treeResizeObserver.disconnect();
}
this.destroyObservers();
this.destroyMutables();
},
methods: {
initialize() {
this.observers = {};
this.mutables = {};
this.isLoading = true;
this.getSavedOpenItems();
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
this.treeResizeObserver.observe(this.$el);
// need to wait for the first tick to get the height of the tree
this.$nextTick().then(this.calculateHeights);
return;
},
async loadRoot() {
this.treeItems = [];
const root = await this.openmct.objects.get('ROOT');
if (!root.identifier) {
return false;
}
// will need to listen for root composition changes as well
this.treeItems = await this.loadAndBuildTreeItemsFor(root.identifier, []);
},
treeItemAction(parentItem, type) {
if (type === 'close') {
this.closeTreeItem(parentItem);
} else {
this.openTreeItem(parentItem);
}
},
targetedPathAnimationEnd() {
this.targetedPath = null;
},
treeItemSelection(item) {
this.selectedItem = item;
this.$emit('tree-item-selection', item);
},
async openTreeItem(parentItem) {
const parentPath = parentItem.navigationPath;
const abortSignal = this.startItemLoad(parentPath);
// pass in abort signal when functional
const childrenItems = await this.loadAndBuildTreeItemsFor(
parentItem.object.identifier,
parentItem.objectPath,
abortSignal
);
const parentIndex = this.treeItems.findIndex((item) => item.navigationPath === parentPath);
// if it's not loading, it was aborted
if (!this.isItemLoading(parentPath) || parentIndex === -1) {
return;
}
this.endItemLoad(parentPath);
const newTreeItems = [...this.treeItems];
newTreeItems.splice(parentIndex + 1, 0, ...childrenItems);
this.treeItems = [...newTreeItems];
if (!this.isTreeItemOpen(parentItem)) {
this.openTreeItems.push(parentPath);
}
for (let item of childrenItems) {
if (this.isTreeItemOpen(item)) {
this.openTreeItem(item);
}
}
return;
},
closeTreeItemByPath(path) {
// if actively loading, abort
if (this.isItemLoading(path)) {
this.abortItemLoad(path);
}
const pathIndex = this.openTreeItems.indexOf(path);
if (pathIndex === -1) {
return;
}
const newTreeItems = this.treeItems.filter((item) => {
const otherPath = item.navigationPath;
if (otherPath !== path && this.isTreeItemAChildOf(otherPath, path)) {
this.destroyObserverByPath(otherPath);
this.destroyMutableByPath(otherPath);
return false;
}
return true;
});
this.treeItems = [...newTreeItems];
const newOpenTreeItems = [...this.openTreeItems];
newOpenTreeItems.splice(pathIndex, 1);
this.openTreeItems = [...newOpenTreeItems];
this.removeCompositionListenerFor(path);
},
closeTreeItem(item) {
this.closeTreeItemByPath(item.navigationPath);
},
// returns an AbortController signal to be passed on to requests
startItemLoad(path) {
if (this.isItemLoading(path)) {
this.abortItemLoad(path);
}
this.treeItemLoading[path] = new AbortController();
return this.treeItemLoading[path].signal;
},
endItemLoad(path) {
this.treeItemLoading[path] = undefined;
delete this.treeItemLoading[path];
},
abortItemLoad(path) {
if (this.treeItemLoading[path]) {
this.treeItemLoading[path].abort();
this.endItemLoad(path);
}
},
isItemLoading(path) {
return this.treeItemLoading[path] instanceof AbortController;
},
showCurrentPathInTree() {
const currentPath = this.buildNavigationPath(this.openmct.router.path);
if (this.getTreeItemByPath(currentPath)) {
this.scrollTo(currentPath);
} else {
this.openAndScrollTo(currentPath);
}
},
async syncTreeOpenItems() {
const items = [...this.treeItems];
for (let item of items) {
if (this.isTreeItemOpen(item)) {
await this.openTreeItem(item);
}
}
},
openAndScrollTo(navigationPath) {
if (navigationPath.includes('/ROOT')) {
navigationPath = navigationPath.split('/ROOT').join('');
}
let idArray = navigationPath.split('/');
let fullPathArray = [];
let pathsToOpen;
this.scrollToPath = navigationPath;
// skip root
idArray.splice(0, 2);
idArray[0] = 'browse/' + idArray[0];
idArray.reduce((parentPath, childPath) => {
let fullPath = [parentPath, childPath].join('/');
fullPathArray.push(fullPath);
return fullPath;
}, '');
pathsToOpen = fullPathArray.filter(
(fullPath) => !this.isTreeItemPathOpen(fullPath) && fullPath !== navigationPath
);
pathsToOpen
.reduce(async (parentLoaded, childPath) => {
await parentLoaded;
return this.openTreeItem(this.getTreeItemByPath(childPath));
}, Promise.resolve())
.then(() => {
if (this.isSelectorTree) {
let item = this.getTreeItemByPath(navigationPath);
// If item is missing due to error in object creation,
// walk up the navigationPath until we find an item
while (!item && navigationPath !== '') {
const startIndex = 0;
const endIndex = navigationPath.lastIndexOf('/');
navigationPath = navigationPath.substring(startIndex, endIndex);
item = this.getTreeItemByPath(navigationPath);
}
this.treeItemSelection(item);
}
this.scrollToCheck(navigationPath);
this.scrollToPath = null;
});
},
scrollToCheck(navigationPath) {
if (this.scrollToPath && this.scrollToPath === navigationPath) {
this.scrollTo(navigationPath);
}
},
scrollTo(navigationPath) {
if (!this.$refs.scrollable || this.isItemInView(navigationPath)) {
return;
}
const indexOfScroll = this.treeItems.findIndex(
(item) => item.navigationPath === navigationPath
);
if (indexOfScroll !== -1) {
const scrollTopAmount = indexOfScroll * this.itemHeight;
this.$refs.scrollable.scrollTo({
top: scrollTopAmount,
behavior: 'smooth'
});
} else if (this.scrollToPath) {
this.scrollToPath = null;
}
},
scrollEndEvent() {
if (!this.$refs.scrollable) {
return;
}
this.$nextTick(() => {
if (this.scrollToPath) {
if (!this.isItemInView(this.scrollToPath)) {
this.scrollTo(this.scrollToPath);
} else {
this.scrollToPath = null;
}
}
});
},
setTargetedItem(navigationPath) {
this.targetedItem = navigationPath;
},
isItemInView(navigationPath) {
const indexOfScroll = this.treeItems.findIndex(
(item) => item.navigationPath === navigationPath
);
const scrollTopAmount = indexOfScroll * this.itemHeight;
const treeStart = this.$refs.scrollable.scrollTop;
const treeEnd = treeStart + this.mainTreeHeight;
return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd;
},
getLowercaseObjectName(domainObject) {
let objectName;
if (!domainObject) {
return objectName;
}
if (domainObject.name) {
objectName = domainObject.name.toLowerCase();
}
if (domainObject.object && domainObject.object.name) {
objectName = domainObject.object.name.toLowerCase();
}
return objectName;
},
sortNameAscending(a, b) {
// sorting tree children items
let objectAName = this.getLowercaseObjectName(a);
let objectBName = this.getLowercaseObjectName(b);
if (!objectAName || !objectBName) {
return 0;
}
// sorting composition items
if (objectAName > objectBName) {
return 1;
}
if (objectBName > objectAName) {
return -1;
}
return 0;
},
isSortable(parentObjectPath) {
// determine if any part of the parent's path includes a key value of mine; aka My Items
return Boolean(parentObjectPath.find((path) => path.identifier.key === 'mine'));
},
async loadAndBuildTreeItemsFor(identifier, parentObjectPath, abortSignal) {
const domainObject = await this.openmct.objects.get(identifier);
const collection = this.openmct.composition.get(domainObject);
let composition = await collection.load(abortSignal);
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) {
const sortedComposition = composition.slice().sort(this.sortNameAscending);
composition = sortedComposition;
}
if (parentObjectPath.length && !this.isSelectorTree) {
let navigationPath = this.buildNavigationPath(parentObjectPath);
if (this.compositionCollections[navigationPath]) {
this.removeCompositionListenerFor(navigationPath);
}
this.compositionCollections[navigationPath] = {};
this.compositionCollections[navigationPath].collection = markRaw(collection);
this.compositionCollections[navigationPath].addHandler =
this.compositionAddHandler(navigationPath);
this.compositionCollections[navigationPath].removeHandler =
this.compositionRemoveHandler(navigationPath);
this.compositionCollections[navigationPath].collection.on(
'add',
this.compositionCollections[navigationPath].addHandler
);
this.compositionCollections[navigationPath].collection.on(
'remove',
this.compositionCollections[navigationPath].removeHandler
);
}
return composition.map((object) => {
// Only add observers and mutables if this is NOT a selector tree
if (!this.isSelectorTree) {
if (this.openmct.objects.supportsMutation(object.identifier)) {
object = this.openmct.objects.toMutable(object);
this.addMutable(object, parentObjectPath);
}
this.addTreeItemObserver(object, parentObjectPath);
}
return this.buildTreeItem(object, parentObjectPath);
});
},
buildTreeItem(domainObject, parentObjectPath, isNew = false) {
let objectPath = [domainObject].concat(parentObjectPath);
let navigationPath = this.buildNavigationPath(objectPath);
// Ensure that we create reactive objects for the tree
return reactive({
id: this.openmct.objects.makeKeyString(domainObject.identifier),
object: domainObject,
leftOffset: (objectPath.length - 1) * TREE_ITEM_INDENT_PX + 'px',
isNew,
objectPath,
navigationPath
});
},
addMutable(mutableDomainObject, parentObjectPath) {
const objectPath = [mutableDomainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
// If the mutable already exists, destroy it.
this.destroyMutableByPath(navigationPath);
this.mutables[navigationPath] = () =>
this.openmct.objects.destroyMutable(mutableDomainObject);
},
addTreeItemObserver(domainObject, parentObjectPath) {
const objectPath = [domainObject].concat(parentObjectPath);
const navigationPath = this.buildNavigationPath(objectPath);
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
}
this.observers[navigationPath] = this.openmct.objects.observe(
domainObject,
'name',
this.sortTreeItems.bind(this, parentObjectPath)
);
},
sortTreeItems(parentObjectPath) {
const navigationPath = this.buildNavigationPath(parentObjectPath);
const parentItem = this.getTreeItemByPath(navigationPath);
// If the parent is not sortable, skip sorting
if (!this.isSortable(parentObjectPath)) {
return;
}
// Sort the renamed object and its siblings (direct descendants of the parent)
const directDescendants = this.getChildrenInTreeFor(parentItem, false);
directDescendants.sort(this.sortNameAscending);
// Take a copy of the sorted descendants array
const sortedTreeItems = directDescendants.slice();
directDescendants.forEach((descendant) => {
const parent = this.getTreeItemByPath(descendant.navigationPath);
// If descendant is not open, skip
if (!this.isTreeItemOpen(parent)) {
return;
}
// If descendant is open but has no children, skip
const children = this.getChildrenInTreeFor(parent, true);
if (children.length === 0) {
return;
}
// Splice in the children of the descendant
const parentIndex = sortedTreeItems
.map((item) => item.navigationPath)
.indexOf(parent.navigationPath);
sortedTreeItems.splice(parentIndex + 1, 0, ...children);
});
// Splice in all of the sorted descendants
const newTreeItems = [...this.treeItems];
newTreeItems.splice(
newTreeItems.indexOf(parentItem) + 1,
sortedTreeItems.length,
...sortedTreeItems
);
this.treeItems = [...newTreeItems];
},
buildNavigationPath(objectPath) {
return (
'/browse/' +
[...objectPath]
.reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
.join('/')
);
},
compositionAddHandler(navigationPath) {
return (domainObject) => {
const parentItem = this.getTreeItemByPath(navigationPath);
const newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true);
const descendants = this.getChildrenInTreeFor(parentItem, true);
const directDescendants = this.getChildrenInTreeFor(parentItem);
if (domainObject.isMutable) {
this.addMutable(domainObject, parentItem.objectPath);
}
this.addTreeItemObserver(domainObject, parentItem.objectPath);
if (directDescendants.length === 0) {
this.addItemToTreeAfter(newItem, parentItem);
return;
}
if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentItem.objectPath)) {
const newItemIndex = directDescendants.findIndex(
(descendant) => this.sortNameAscending(descendant, newItem) > 0
);
const shouldInsertFirst = newItemIndex === 0;
const shouldInsertLast = newItemIndex === -1;
if (shouldInsertFirst) {
this.addItemToTreeAfter(newItem, parentItem);
} else if (shouldInsertLast) {
this.addItemToTreeAfter(newItem, descendants.pop());
} else {
this.addItemToTreeBefore(newItem, directDescendants[newItemIndex]);
}
return;
}
this.addItemToTreeAfter(newItem, descendants.pop());
};
},
compositionRemoveHandler(navigationPath) {
return (identifier) => {
const removeKeyString = this.openmct.objects.makeKeyString(identifier);
const parentItem = this.getTreeItemByPath(navigationPath);
const directDescendants = this.getChildrenInTreeFor(parentItem);
const removeItem = directDescendants.find((item) => item.id === removeKeyString);
// Remove the item from the tree, unobserve it, and clean up any mutables
this.removeItemFromTree(removeItem);
this.destroyObserverByPath(removeItem.navigationPath);
this.destroyMutableByPath(removeItem.navigationPath);
};
},
removeCompositionListenerFor(navigationPath) {
if (this.compositionCollections[navigationPath]) {
this.compositionCollections[navigationPath].collection.off(
'add',
this.compositionCollections[navigationPath].addHandler
);
this.compositionCollections[navigationPath].collection.off(
'remove',
this.compositionCollections[navigationPath].removeHandler
);
this.compositionCollections[navigationPath] = undefined;
delete this.compositionCollections[navigationPath];
}
},
removeItemFromTree(item) {
if (this.isTreeItemOpen(item)) {
this.closeTreeItem(item);
}
const removeIndex = this.getTreeItemIndex(item.navigationPath);
const newTreeItems = [...this.treeItems];
newTreeItems.splice(removeIndex, 1);
this.treeItems = [...newTreeItems];
},
addItemToTreeBefore(addItem, beforeItem) {
const addIndex = this.getTreeItemIndex(beforeItem.navigationPath);
this.addItemToTree(addItem, addIndex);
},
addItemToTreeAfter(addItem, afterItem) {
const addIndex = this.getTreeItemIndex(afterItem.navigationPath);
this.addItemToTree(addItem, addIndex + 1);
},
addItemToTree(addItem, index) {
const newTreeItems = [...this.treeItems];
newTreeItems.splice(index, 0, addItem);
this.treeItems = [...newTreeItems];
if (this.isTreeItemOpen(addItem)) {
this.openTreeItem(addItem);
}
},
searchTree(value) {
// if an abort controller exists, regardless of the value passed in,
// there is an active search that should be canceled
if (this.abortSearchController) {
this.abortSearchController.abort();
delete this.abortSearchController;
}
this.searchValue = value;
this.searchLoading = true;
if (this.searchValue !== '') {
// clear any previous search results
this.searchResultItems = [];
this.getSearchResults();
} else {
this.searchLoading = false;
}
},
getSearchResults() {
// an abort controller will be passed in that will be used
// to cancel an active searches if necessary
this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal;
const searchPromises = this.openmct.objects.search(this.searchValue, abortSignal);
searchPromises.map((promise) =>
promise.then((results) => {
this.aggregateSearchResults(results, abortSignal);
})
);
Promise.all(searchPromises)
.catch((reason) => {
// search aborted
})
.finally(() => {
this.searchLoading = false;
if (this.abortSearchController) {
delete this.abortSearchController;
}
});
},
aggregateSearchResults(results, abortSignal) {
let resultPromises = [];
for (const result of results) {
if (!abortSignal.aborted) {
// Don't show deleted objects in search results
if (result.location === null) {
continue;
}
resultPromises.push(
this.openmct.objects.getOriginalPath(result.identifier).then((objectPath) => {
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// if root, remove, we're not using in object path for tree
const lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
this.searchResultItems.push(this.buildTreeItem(result, objectPath));
})
);
}
}
return resultPromises;
},
updateVisibleItems() {
this.scrollEndEvent();
if (this.updatingView) {
return;
}
this.updatingView = true;
requestAnimationFrame(() => {
let start = 0;
let end = this.pageThreshold;
let allItemsCount = this.focusedItems.length;
if (allItemsCount < this.pageThreshold) {
end = allItemsCount;
} else {
let firstVisible = this.calculateFirstVisibleItem();
let lastVisible = this.calculateLastVisibleItem();
let totalVisible = lastVisible - firstVisible;
let numberOffscreen = this.pageThreshold - totalVisible;
start = firstVisible - Math.floor(numberOffscreen / 2);
end = lastVisible + Math.ceil(numberOffscreen / 2);
if (start < 0) {
start = 0;
end = Math.min(this.pageThreshold, allItemsCount);
} else if (end >= allItemsCount) {
end = allItemsCount;
start = end - this.pageThreshold + 1;
}
}
this.itemOffset = start;
this.visibleItems = this.focusedItems.slice(start, end);
this.updatingView = false;
});
},
calculateFirstVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollTop = this.$refs.scrollable.scrollTop;
return Math.floor(scrollTop / this.itemHeight);
},
calculateLastVisibleItem() {
if (!this.$refs.scrollable) {
return;
}
let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight;
return Math.ceil(scrollBottom / this.itemHeight);
},
calculateHeights() {
const RECHECK = 100;
return new Promise((resolve, reject) => {
let checkHeights = () => {
let treeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
let paddingOffset = 0;
if (
this.$el &&
this.$refs.search &&
this.$refs.mainTree &&
this.$refs.treeContainer &&
this.$refs.dummyItem &&
this.$el.offsetHeight !== 0 &&
treeTopMargin > 0
) {
if (this.isSelectorTree) {
paddingOffset = this.getElementStyleValue(this.$refs.treeContainer, 'padding');
}
this.mainTreeTopMargin = treeTopMargin;
this.mainTreeHeight =
this.$el.offsetHeight -
this.$refs.search.offsetHeight -
this.mainTreeTopMargin -
paddingOffset * 2;
this.itemHeight = this.getElementStyleValue(this.$refs.dummyItem, 'height');
resolve();
} else {
setTimeout(checkHeights, RECHECK);
}
};
checkHeights();
});
},
getTreeItemByPath(path) {
return this.treeItems.find((item) => item.navigationPath === path);
},
getTreeItemIndex(indexItem) {
let path = typeof indexItem === 'string' ? indexItem : indexItem.navigationPath;
return this.treeItems.findIndex((item) => item.navigationPath === path);
},
getChildrenInTreeFor(parent, allDescendants = false) {
const parentPath = typeof parent === 'string' ? parent : parent.navigationPath;
const parentDepth = parentPath.split('/').length;
return this.treeItems.filter((childItem) => {
const childDepth = childItem.navigationPath.split('/').length;
if (!allDescendants && childDepth > parentDepth + 1) {
return false;
}
return (
childItem.navigationPath !== parentPath && childItem.navigationPath.includes(parentPath)
);
});
},
isTreeItemOpen(item) {
return this.isTreeItemPathOpen(item.navigationPath);
},
isTreeItemPathOpen(path) {
return this.openTreeItems.includes(path);
},
isTreeItemAChildOf(childNavigationPath, parentNavigationPath) {
const childPathKeys = childNavigationPath.split('/');
const parentPathKeys = parentNavigationPath.split('/');
// If child path is shorter than or same length as
// the parent path, then it's not a child.
if (childPathKeys.length <= parentPathKeys.length) {
return false;
}
for (let i = 0; i < parentPathKeys.length; i++) {
if (childPathKeys[i] !== parentPathKeys[i]) {
return false;
}
}
return true;
},
getElementStyleValue(el, style) {
if (!el) {
return;
}
let styleString = window.getComputedStyle(el)[style];
let index = styleString.indexOf('px');
return Number(styleString.slice(0, index));
},
getSavedOpenItems() {
if (this.isSelectorTree) {
return;
}
let openItems = localStorage.getItem(LOCAL_STORAGE_KEY__TREE_EXPANDED);
this.openTreeItems = openItems ? JSON.parse(openItems) : [];
},
setSavedOpenItems() {
if (this.isSelectorTree) {
return;
}
localStorage.setItem(LOCAL_STORAGE_KEY__TREE_EXPANDED, JSON.stringify(this.openTreeItems));
},
handleTreeResize() {
this.calculateHeights();
},
/**
* Destroy an observer for the given navigationPath.
*/
destroyObserverByPath(navigationPath) {
if (this.observers[navigationPath]) {
this.observers[navigationPath]();
delete this.observers[navigationPath];
}
},
/**
* Destroy all observers.
*/
destroyObservers() {
Object.entries(this.observers).forEach(([key, unobserve]) => {
if (unobserve) {
unobserve();
}
delete this.observers[key];
});
},
/**
* Destroy a mutable for the given navigationPath.
*/
destroyMutableByPath(navigationPath) {
if (this.mutables[navigationPath]) {
this.mutables[navigationPath]();
delete this.mutables[navigationPath];
}
},
/**
* Destroy all mutables.
*/
destroyMutables() {
Object.entries(this.mutables).forEach(([key, destroyMutable]) => {
if (destroyMutable) {
destroyMutable();
}
delete this.mutables[key];
});
}
}
};
</script>