Drastically improved tree tab search UX for locating parts of the sheet
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
115
app/imports/api/parenting/nodesToTree.js
Normal file
115
app/imports/api/parenting/nodesToTree.js
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template lang="html">
|
||||
<v-sheet
|
||||
class="tree-node"
|
||||
:class="!hasChildren ? 'empty' : null"
|
||||
:class="{
|
||||
'empty': !hasChildren,
|
||||
'found': node._matchedDocumentFilter,
|
||||
}"
|
||||
:data-id="`tree-node-${node._id}`"
|
||||
>
|
||||
<div
|
||||
@@ -52,7 +55,7 @@
|
||||
:children="computedChildren"
|
||||
:group="group"
|
||||
:organize="organize"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@reordered="e => $emit('reordered', e)"
|
||||
@reorganized="e => $emit('reorganized', e)"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@@ -80,6 +83,7 @@
|
||||
import { canBeParent } from '/imports/api/parenting/parenting.js';
|
||||
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import { some } from 'lodash';
|
||||
|
||||
export default {
|
||||
name: 'TreeNode',
|
||||
@@ -87,16 +91,33 @@
|
||||
TreeNodeView,
|
||||
},
|
||||
props: {
|
||||
node: Object,
|
||||
group: String,
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
organize: Boolean,
|
||||
children: Array,
|
||||
getChildren: Function,
|
||||
selectedNodeId: String,
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
getChildren: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
selected: Boolean,
|
||||
},
|
||||
data(){ return {
|
||||
expanded: false,
|
||||
data(){return {
|
||||
expanded: this.node._ancestorOfMatchedDocument ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
|
||||
false,
|
||||
}},
|
||||
computed: {
|
||||
hasChildren(){
|
||||
@@ -119,6 +140,15 @@
|
||||
return canBeParent(this.node.type);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node._ancestorOfMatchedDocument'(value){
|
||||
this.expanded = !!value ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
|
||||
},
|
||||
'selectedNode.ancestors'(value){
|
||||
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default
|
||||
},
|
||||
@@ -148,9 +178,12 @@
|
||||
.empty .v-btn {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.found {
|
||||
background: rgba(200, 0, 0, 0.1);
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #fbc8c8;
|
||||
background: rgba(251, 0, 0, 0.3);
|
||||
}
|
||||
.v-icon.v-icon--disabled {
|
||||
opacity: 0;
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
:node="child.node"
|
||||
:children="child.children"
|
||||
:group="group"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected="selectedNodeId === child.node._id"
|
||||
:selected-node="selectedNode"
|
||||
:selected="selectedNode && selectedNode._id === child.node._id"
|
||||
:ancestors-of-selected-node="ancestorsOfSelectedNode"
|
||||
:organize="organize"
|
||||
:lazy="lazy"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@@ -49,7 +50,14 @@
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedNodeId: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
ancestorsOfSelectedNode: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
expanded: false,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
style="overflow-y: auto;"
|
||||
:root="{collection: 'creatures', id: creatureId}"
|
||||
:organize="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
:filter="filter"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
@@ -51,9 +51,9 @@
|
||||
<template slot="detail">
|
||||
<creature-property-dialog
|
||||
embedded
|
||||
:_id="selected"
|
||||
@removed="selected = undefined"
|
||||
@duplicated="id => selected = id"
|
||||
:_id="selectedNodeId"
|
||||
@removed="selectedNodeId = undefined"
|
||||
@duplicated="id => selectedNodeId = id"
|
||||
/>
|
||||
</template>
|
||||
</tree-detail-layout>
|
||||
@@ -87,7 +87,7 @@
|
||||
data(){ return {
|
||||
organize: false,
|
||||
organizeDisabled: false,
|
||||
selected: undefined,
|
||||
selectedNodeId: undefined,
|
||||
fab: false,
|
||||
filterString: '',
|
||||
filterOptions: [
|
||||
@@ -144,14 +144,14 @@
|
||||
},
|
||||
'$vuetify.breakpoint.mdAndUp'(mdAndUp){
|
||||
if (!mdAndUp){
|
||||
this.selected = undefined;
|
||||
this.selectedNodeId = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickNode(id){
|
||||
if (this.$vuetify.breakpoint.mdAndUp){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
} else {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
@@ -167,7 +167,7 @@
|
||||
component: 'creature-property-dialog',
|
||||
elementId: 'selected-node-card',
|
||||
data: {
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
startInEditTab: true,
|
||||
},
|
||||
});
|
||||
@@ -175,9 +175,9 @@
|
||||
getPropertyName,
|
||||
},
|
||||
meteor: {
|
||||
selectedProperty(){
|
||||
selectedNode(){
|
||||
return CreatureProperties.findOne({
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
removed: {$ne: true}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:children="children"
|
||||
:group="group"
|
||||
:organize="organize"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="reordered"
|
||||
@reorganized="reorganized"
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js'
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
props: {
|
||||
root: Object,
|
||||
organize: Boolean,
|
||||
selectedNodeId: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
filter: Object,
|
||||
group: {
|
||||
type: String,
|
||||
@@ -37,6 +40,8 @@
|
||||
collection: CreatureProperties,
|
||||
ancestorId: this.root.id,
|
||||
filter: this.filter,
|
||||
includeFilteredDocAncestors: true,
|
||||
includeFilteredDocDescendants: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -234,7 +234,8 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
selectSubProperty(_id){
|
||||
selectSubProperty(doc){
|
||||
let _id = doc && doc._id
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
style="bottom: -32px"
|
||||
fab
|
||||
:library-id="libraryId"
|
||||
:selected-node-id="selected"
|
||||
@selected="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}"
|
||||
:selected-node-id="selectedNodeId"
|
||||
@selected="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<div
|
||||
@@ -41,7 +41,7 @@
|
||||
<library-contents-container
|
||||
:library-id="libraryId"
|
||||
:organize-mode="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
should-subscribe
|
||||
@selected="clickNode"
|
||||
/>
|
||||
@@ -50,7 +50,7 @@
|
||||
v-else
|
||||
edit-mode
|
||||
:organize-mode="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
style="overflow-y: auto; padding: 12px;"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
@@ -61,10 +61,10 @@
|
||||
style="overflow: hidden;"
|
||||
>
|
||||
<library-node-dialog
|
||||
:_id="selected"
|
||||
:_id="selectedNodeId"
|
||||
embedded
|
||||
@removed="selected = undefined"
|
||||
@duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}"
|
||||
@removed="selectedNodeId = undefined"
|
||||
@duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
|
||||
/>
|
||||
</div>
|
||||
</tree-detail-layout>
|
||||
@@ -100,7 +100,7 @@ export default {
|
||||
},
|
||||
data(){ return {
|
||||
organize: false,
|
||||
selected: undefined,
|
||||
selectedNodeId: undefined,
|
||||
};},
|
||||
computed: {
|
||||
isToolbarDark(){
|
||||
@@ -120,12 +120,12 @@ export default {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-edit-dialog',
|
||||
elementId: 'selected-node-card',
|
||||
data: {_id: this.selected},
|
||||
data: {_id: this.selectedNodeId},
|
||||
});
|
||||
},
|
||||
clickNode(id){
|
||||
if (this.$vuetify.breakpoint.mdAndUp){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
} else {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
@@ -136,7 +136,7 @@ export default {
|
||||
},
|
||||
callback: result => {
|
||||
if (result){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -175,7 +175,7 @@ export default {
|
||||
},
|
||||
selectedNode(){
|
||||
return LibraryNodes.findOne({
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
removed: {$ne: true}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<insert-library-node-button
|
||||
v-if="editPermission(library)"
|
||||
:library-id="library._id"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node-id="selectedNode && selectedNode._id"
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
<v-btn
|
||||
@@ -46,7 +46,7 @@
|
||||
:library-id="library._id"
|
||||
:organize-mode="organizeMode && editPermission(library)"
|
||||
:edit-mode="editMode"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
should-subscribe
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
@@ -83,8 +83,8 @@ export default {
|
||||
props: {
|
||||
organizeMode: Boolean,
|
||||
editMode: Boolean,
|
||||
selectedNodeId: {
|
||||
type: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
group="library"
|
||||
:children="libraryChildren"
|
||||
:organize="organizeMode"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="reordered"
|
||||
@reorganized="reorganized"
|
||||
@@ -29,7 +29,7 @@
|
||||
<script lang="js">
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js'
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
props: {
|
||||
libraryId: String,
|
||||
organizeMode: Boolean,
|
||||
selectedNodeId: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
shouldSubscribe: Boolean,
|
||||
},
|
||||
data(){return {
|
||||
|
||||
Reference in New Issue
Block a user