From 02434de34c3e60367ef50e99eb0d5a162850655b Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sat, 31 Jul 2021 15:19:54 +0200 Subject: [PATCH] Drastically improved tree tab search UX for locating parts of the sheet --- app/imports/api/creature/actions/doAction.js | 2 +- .../denormalise/recomputeInventory.js | 2 +- .../getDescendantsInDepthFirstOrder.js | 2 +- app/imports/api/parenting/nodesToTree.js | 115 ++++++++++++++++++ app/imports/api/parenting/parenting.js | 40 +----- app/imports/ui/components/tree/TreeNode.vue | 53 ++++++-- .../ui/components/tree/TreeNodeList.vue | 14 ++- .../character/characterSheetTabs/TreeTab.vue | 20 +-- .../CreaturePropertiesTree.vue | 11 +- .../CreaturePropertyDialog.vue | 3 +- app/imports/ui/library/LibraryAndNode.vue | 24 ++-- app/imports/ui/library/LibraryBrowser.vue | 8 +- .../ui/library/LibraryContentsContainer.vue | 9 +- 13 files changed, 215 insertions(+), 88 deletions(-) create mode 100644 app/imports/api/parenting/nodesToTree.js diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js index 27d560db..8cbe6690 100644 --- a/app/imports/api/creature/actions/doAction.js +++ b/app/imports/api/creature/actions/doAction.js @@ -7,7 +7,7 @@ import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import { nodesToTree } from '/imports/api/parenting/parenting.js'; +import nodesToTree from '/imports/api/parenting/nodesToTree.js'; import applyProperties from '/imports/api/creature/actions/applyProperties.js'; import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; diff --git a/app/imports/api/creature/denormalise/recomputeInventory.js b/app/imports/api/creature/denormalise/recomputeInventory.js index ae3871ec..3bf7d1a2 100644 --- a/app/imports/api/creature/denormalise/recomputeInventory.js +++ b/app/imports/api/creature/denormalise/recomputeInventory.js @@ -1,6 +1,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { nodesToTree } from '/imports/api/parenting/parenting.js'; +import nodesToTree from '/imports/api/parenting/nodesToTree.js'; export default function recomputeInventory(creatureId){ let inventoryForest = nodesToTree({ diff --git a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js b/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js index cc308f73..9010d51b 100644 --- a/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js +++ b/app/imports/api/parenting/getDescendantsInDepthFirstOrder.js @@ -1,4 +1,4 @@ -import { nodesToTree } from '/imports/api/parenting/parenting.js'; +import nodesToTree from '/imports/api/parenting/nodesToTree.js'; export default function getDescendantsInDepthFirstOrder({ collection, diff --git a/app/imports/api/parenting/nodesToTree.js b/app/imports/api/parenting/nodesToTree.js new file mode 100644 index 00000000..b8589967 --- /dev/null +++ b/app/imports/api/parenting/nodesToTree.js @@ -0,0 +1,115 @@ +import { union, difference, sortBy, findLast } from 'lodash'; + +export function nodeArrayToTree(nodes){ + // Store a dict and list of all the nodes + let nodeIndex = {}; + let nodeList = []; + nodes.forEach( node => { + let treeNode = { + node: node, + children: [], + }; + nodeIndex[node._id] = treeNode; + nodeList.push(treeNode); + }); + // Create a forest of trees + let forest = []; + // Either the node is a child of its nearest found ancestor, or in the forest as a root + nodeList.forEach(treeNode => { + let ancestorInForest = findLast( + treeNode.node.ancestors, + ancestor => !!nodeIndex[ancestor.id] + ); + if (ancestorInForest){ + nodeIndex[ancestorInForest.id].children.push(treeNode); + } else { + forest.push(treeNode); + } + }); + return forest; +} + +// Fetch the documents from a collection, and return the tree of those documents +export default function nodesToTree({ + collection, ancestorId, filter, options = {}, + includeFilteredDocAncestors = false, includeFilteredDocDescendants = false +}){ + // Setup the filter + let collectionFilter = { + 'ancestors.id': ancestorId, + 'removed': {$ne: true}, + }; + if (filter){ + collectionFilter = { + ...collectionFilter, + ...filter, + } + } + // Set up the options + let collectionSort = { + order: 1 + }; + if (options && options.sort){ + collectionSort = { + ...collectionSort, + ...options.sort, + } + } + let collectionOptions = { + sort: collectionSort, + } + if (options){ + collectionOptions = { + ...collectionOptions, + ...options, + } + } + // Find all the nodes that match the filter + let docs = collection.find(collectionFilter, collectionOptions).map(doc => { + if (!filter) return doc; + // Mark the nodes that were found by the custom filter + doc._matchedDocumentFilter = true; + return doc; + }); + let ancestors = []; + let ancestorIds = []; + let docIds = []; + if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){ + docIds = docs.map(doc => doc._id) + } + if (filter && includeFilteredDocAncestors){ + // Add all ancestor ids to an array + docs.forEach(doc => { + ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id)); + }); + // Remove the IDs of docs we have already found + ancestorIds = difference(ancestorIds, docIds); + // Get the docs from the collection, don't worry about `removed` docs, + // if their descendant was not removed, neither are they + ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => { + // Mark that the nodes are ancestors of the found nodes + doc._ancestorOfMatchedDocument = true; + return doc; + }); + } + let descendants = []; + if (filter && includeFilteredDocDescendants){ + let exludeIds = union(ancestorIds, docIds); + descendants = collection.find({ + '_id': {$nin: exludeIds}, + 'ancestors.id': {$in: docIds}, + 'removed': {$ne: true}, + }).map(doc => { + // Mark that the nodes are descendants of the found nodes + doc._descendantOfMatchedDocument = true; + return doc; + }); + } + let nodes = sortBy([ + ...ancestors, + ...docs, + ...descendants + ], 'order'); + // Find all the nodes + return nodeArrayToTree(nodes); +} diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js index b7135356..70e7747d 100644 --- a/app/imports/api/parenting/parenting.js +++ b/app/imports/api/parenting/parenting.js @@ -1,6 +1,6 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { flatten, findLast } from 'lodash'; +import { flatten } from 'lodash'; const generalParents = [ 'attribute', @@ -217,41 +217,3 @@ export function getName(doc){ if (doc.ancestors[i].name) return doc.ancestors[i].name; } } - -export function nodeArrayToTree(nodes){ - // Store a dict and list of all the nodes - let nodeIndex = {}; - let nodeList = []; - nodes.forEach( node => { - let treeNode = { - node: node, - children: [], - }; - nodeIndex[node._id] = treeNode; - nodeList.push(treeNode); - }); - // Create a forest of trees - let forest = []; - // Either the node is a child of its nearest found ancestor, or in the forest as a root - nodeList.forEach(treeNode => { - let ancestorInForest = findLast( - treeNode.node.ancestors, - ancestor => !!nodeIndex[ancestor.id] - ); - if (ancestorInForest){ - nodeIndex[ancestorInForest.id].children.push(treeNode); - } else { - forest.push(treeNode); - } - }); - return forest; -} - -export function nodesToTree({collection, ancestorId, filter = {}, options = {}}){ - if (!('ancestors.id' in filter)) filter['ancestors.id'] = ancestorId; - if (!('removed' in filter)) filter['removed'] = {$ne: true}; - if (!options.sort) options.sort = {order: 1}; - if (!('order' in options.sort)) options.sort.order = 1; - let nodes = collection.find(filter, options); - return nodeArrayToTree(nodes); -} diff --git a/app/imports/ui/components/tree/TreeNode.vue b/app/imports/ui/components/tree/TreeNode.vue index c535eb3e..dad46c9c 100644 --- a/app/imports/ui/components/tree/TreeNode.vue +++ b/app/imports/ui/components/tree/TreeNode.vue @@ -1,7 +1,10 @@