diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index 9acb3d0bbe..11d3bc50d3 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor; $colorItemTreeEditingIcon: $editUIColor; $colorItemTreeVC: $colorDisclosureCtrl; $colorItemTreeVCHover: $colorDisclosureCtrlHov; +$colorItemTreeNewNode: rgba($colorBodyFg, 0.7); $shdwItemTreeIcon: none; // Layout frame controls diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index 0f154b49d2..144f84e6cc 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -386,6 +386,7 @@ $colorItemTreeEditingFg: $editUIColor; $colorItemTreeEditingIcon: $editUIColor; $colorItemTreeVC: $colorDisclosureCtrl; $colorItemTreeVCHover: $colorDisclosureCtrlHov; +$colorItemTreeNewNode: rgba($colorBodyFg, 0.7); $shdwItemTreeIcon: none; // Layout frame controls diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index 8e59e3444c..be07419374 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -382,6 +382,7 @@ $colorItemTreeEditingFg: $editUIColor; $colorItemTreeEditingIcon: $editUIColor; $colorItemTreeVC: $colorDisclosureCtrl; $colorItemTreeVCHover: $colorDisclosureCtrlHov; +$colorItemTreeNewNode: rgba($colorBodyFg, 0.5); $shdwItemTreeIcon: none; // Layout frame controls diff --git a/src/ui/layout/mct-tree.scss b/src/ui/layout/mct-tree.scss index 9912aebabc..5ab78caf40 100644 --- a/src/ui/layout/mct-tree.scss +++ b/src/ui/layout/mct-tree.scss @@ -105,7 +105,12 @@ color: $colorItemTreeSelectedFg; } } - + &.is-new { + animation-name: animTemporaryHighlight; + animation-timing-function: ease-out; + animation-duration: 3s; + animation-iteration-count: 1; + } &.is-context-clicked { box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px; } @@ -289,13 +294,19 @@ } @keyframes animSlideLeft { - 0% {opactiy: 0; transform: translateX(100%);} + 0% {opacity: 0; transform: translateX(100%);} 10% {opacity: 1;} 100% {transform: translateX(0);} } @keyframes animSlideRight { - 0% {opactiy: 0; transform: translateX(-100%);} + 0% {opacity: 0; transform: translateX(-100%);} 10% {opacity: 1;} 100% {transform: translateX(0);} } + +@keyframes animTemporaryHighlight { + from { background: transparent; } + 30% { background: $colorItemTreeNewNode; } + 100% { background: transparent; } +} diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue index 32337a8a1d..562582d508 100644 --- a/src/ui/layout/mct-tree.vue +++ b/src/ui/layout/mct-tree.vue @@ -74,6 +74,7 @@ :node="treeItem" :active-search="activeSearch" :left-offset="!activeSearch ? treeItem.leftOffset : '0px'" + :is-new="treeItem.isNew" :item-offset="itemOffset" :item-index="index" :item-height="itemHeight" @@ -112,7 +113,8 @@ import search from '../components/search.vue'; const ITEM_BUFFER = 25; const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded'; -const RETURN_ALL_DESCDNDANTS = true; +const RETURN_ALL_DESCENDANTS = true; +const SORT_MY_ITEMS_ALPH_ASC = true; const TREE_ITEM_INDENT_PX = 18; export default { @@ -430,9 +432,40 @@ export default { return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd; }, + sortNameDescending(a, b) { + // sorting tree children items + if (!(a.name && b.name)) { + if (a.object.name > b.object.name) { + return 1; + } + + if (b.object.name > a.object.name) { + return -1; + } + } + + // sorting compositon items + if (a.name > b.name) { + return 1; + } + + if (b.name > a.name) { + return -1; + } + + return 0; + }, + async loadAndBuildTreeItemsFor(domainObject, parentObjectPath, abortSignal) { let collection = this.openmct.composition.get(domainObject); let composition = await collection.load(abortSignal); + // determine if any part of the parent's path includes a key value of mine; aka My Items + const isNestedInMyItems = Boolean(parentObjectPath.find(path => path.identifier.key === 'mine')); + + if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) { + const sortedComposition = composition.sort(this.sortNameDescending); + composition = sortedComposition; + } if (parentObjectPath.length) { let navigationPath = this.buildNavigationPath(parentObjectPath); @@ -456,7 +489,7 @@ export default { return this.buildTreeItem(object, parentObjectPath); }); }, - buildTreeItem(domainObject, parentObjectPath) { + buildTreeItem(domainObject, parentObjectPath, isNew = false) { let objectPath = [domainObject].concat(parentObjectPath); let navigationPath = this.buildNavigationPath(objectPath); @@ -464,6 +497,7 @@ export default { id: this.openmct.objects.makeKeyString(domainObject.identifier), object: domainObject, leftOffset: ((objectPath.length - 1) * TREE_ITEM_INDENT_PX) + 'px', + isNew, objectPath, navigationPath }; @@ -476,11 +510,16 @@ export default { compositionAddHandler(navigationPath) { return (domainObject) => { let parentItem = this.getTreeItemByPath(navigationPath); - let newItem = this.buildTreeItem(domainObject, parentItem.objectPath); - let allDescendants = this.getChildrenInTreeFor(parentItem, RETURN_ALL_DESCDNDANTS); + let newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true); + let allDescendants = this.getChildrenInTreeFor(parentItem, RETURN_ALL_DESCENDANTS); let afterItem = allDescendants.length ? allDescendants.pop() : parentItem; this.addItemToTreeAfter(newItem, afterItem); + const isNestedInMyItems = Boolean(parentItem.objectPath && parentItem.objectPath.find(path => path.identifier.key === 'mine')); + + if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) { + this.sortTreeComposition(this.sortNameDescending, navigationPath); + } }; }, compositionRemoveHandler(navigationPath) { @@ -512,6 +551,14 @@ export default { const removeIndex = this.getTreeItemIndex(item.navigationPath); this.treeItems.splice(removeIndex, 1); }, + sortTreeComposition(algorithem, parentPath) { + const parentIndex = this.getTreeItemIndex(parentPath); + const parentItem = this.treeItems[parentIndex]; + + const allDescendants = this.getChildrenInTreeFor(parentItem); + const sortedChildren = allDescendants.sort(algorithem); + this.treeItems.splice(parentIndex + 1, allDescendants.length, ...sortedChildren); + }, addItemToTreeAfter(addItem, afterItem) { const addIndex = this.getTreeItemIndex(afterItem.navigationPath); this.treeItems.splice(addIndex + 1, 0, addItem); diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue index b4a06526be..ca94611a71 100644 --- a/src/ui/layout/tree-item.vue +++ b/src/ui/layout/tree-item.vue @@ -8,7 +8,8 @@ :class="{ 'is-alias': isAlias, 'is-navigated-object': navigated, - 'is-context-clicked': contextClickActive + 'is-context-clicked': contextClickActive, + 'is-new': isNewItem }" @click.capture="handleClick" @contextmenu.capture="handleContextMenu" @@ -59,6 +60,10 @@ export default { type: String, default: '0px' }, + isNew: { + type: Boolean, + default: false + }, itemIndex: { type: Number, required: false, @@ -104,6 +109,9 @@ export default { return parentKeyString !== this.node.object.location; }, + isNewItem() { + return this.isNew; + }, isLoading() { return Boolean(this.loadingItems[this.navigationPath]); },