From 9825872576088c7fed6af008a4acf842a031be22 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 29 Apr 2021 15:52:24 +0200 Subject: [PATCH] Implemented Reference properties --- .../methods/insertPropertyFromLibraryNode.js | 96 +++++++++++++++++++ app/imports/api/library/LibraryNodes.js | 15 ++- app/imports/api/library/methods/index.js | 1 + .../library/methods/updateReferenceNode.js | 67 +++++++++++++ app/imports/api/properties/References.js | 47 +++++++++ .../computedOnlyPropertySchemasIndex.js | 2 + .../computedPropertySchemasIndex.js | 2 + .../api/properties/propertySchemasIndex.js | 2 + app/imports/api/sharing/sharingPermissions.js | 2 +- app/imports/constants/PROPERTIES.js | 5 + .../CreaturePropertyCreationDialog.vue | 1 + .../ui/dialogStack/DialogComponentIndex.js | 4 +- .../ui/library/SelectLibraryNodeDialog.vue | 47 +++++++++ .../ui/properties/forms/ReferenceForm.vue | 69 +++++++++++++ .../forms/shared/propertyFormIndex.js | 2 + .../ui/properties/shared/PropertySelector.vue | 58 ++++++----- .../shared/SelectablePropertyDialog.vue | 2 + .../treeNodeViews/ReferenceTreeNode.vue | 22 +++++ .../treeNodeViews/treeNodeViewIndex.js | 2 + .../ui/properties/viewers/ReferenceViewer.vue | 30 ++++++ .../viewers/shared/propertyViewerIndex.js | 2 + 21 files changed, 448 insertions(+), 30 deletions(-) create mode 100644 app/imports/api/library/methods/updateReferenceNode.js create mode 100644 app/imports/api/properties/References.js create mode 100644 app/imports/ui/library/SelectLibraryNodeDialog.vue create mode 100644 app/imports/ui/properties/forms/ReferenceForm.vue create mode 100644 app/imports/ui/properties/treeNodeViews/ReferenceTreeNode.vue create mode 100644 app/imports/ui/properties/viewers/ReferenceViewer.vue diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 3c1e3b89..52a858c1 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -16,6 +16,7 @@ import { import { reorderDocs } from '/imports/api/parenting/order.js'; import { setDocToLastOrder } from '/imports/api/parenting/order.js'; import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; const insertPropertyFromLibraryNode = new ValidatedMethod({ name: 'creatureProperties.insertPropertyFromLibraryNode', @@ -54,6 +55,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ // Fetch the library node and its decendents, provided they have not been // removed + // TODO: Check permission to read the library this node is in let node = LibraryNodes.findOne({ _id: nodeId, removed: {$ne: true}, @@ -65,6 +67,9 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ removed: {$ne: true}, }).fetch(); + // Convert all references into actual nodes + nodes = reifyNodeReferences(nodes); + // The root node is first in the array of nodes // It must get the first generated ID to prevent flickering nodes = [node, ...nodes]; @@ -115,4 +120,95 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ }, }); +// Covert node references into actual nodes +// TODO: check permissions for each library a reference node references +function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ + depth += 1; + // New nodes added this function + let newNodes = []; + + // Filter out the reference nodes we replace + let resultingNodes = nodes.filter(node => { + + // We have already visited this ref and replaced it + if (visitedRefs.has(node._id)) return false; + + // Already replaced an ancestor node + for (let i; i < node.ancestors.length; i++){ + if (visitedRefs.has(node.ancestors[i].id)) return false; + } + + // This isn't a reference node, continue as normal + if (node.type !== 'reference') return true; + + // We have gone too deep, keep the reference node as an error + if (depth > 10){ + if (Meteor.isClient) console.warn('Reference depth limit exceeded'); + node.cache = {error: 'Reference depth limit exceeded'}; + return true; + } + + let referencedNode + try { + referencedNode = fetchDocByRef(node.ref); + referencedNode.order = node.order; + // We are definitely replacing this node, so add it to the list + visitedRefs.add(node._id); + } catch (e){ + node.cache = {error: e.reason || e.message || e.toString()}; + return true; + } + + // Get all the descendants of the referenced node + let descendents = LibraryNodes.find({ + 'ancestors.id': referencedNode._id, + removed: {$ne: true}, + }, { + sort: {order: 1}, + }).fetch(); + + // We are adding the referenced node and its descendants + let addedNodes = [referencedNode, ...descendents]; + + // re-map all the ancestors to parent the new sub-tree into our existing + // node tree + setLineageOfDocs({ + docArray: addedNodes, + newAncestry: node.ancestors, + oldParent: referencedNode.parent, + }); + + // Remove all the looped references and descendents from the new nodes + // We can't rely on the reify recursion to do this, since the IDs are + // getting renewed before it is called + addedNodes = addedNodes.filter(node => { + // Exclude removed referenced + if (visitedRefs.has(node._id)) return false; + + // Exclude descendants of removed references + for (let i; i < node.ancestors.length; i++){ + if (visitedRefs.has(node.ancestors[i].id)) return false; + } + return true; + }); + + // Give the new referenced sub-tree new ids + renewDocIds({ + docArray: addedNodes, + }); + + // Reify the subtree as well with recursion + addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth); + + // Store the new nodes from this inner loop without altering the array + // we are looping over + newNodes.push(...addedNodes); + }); + + // We are done filtering the array, we can add the new nodes to it + resultingNodes.push(...newNodes); + + return resultingNodes; +} + export default insertPropertyFromLibraryNode; diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index eb9692fd..2d6e0562 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -12,6 +12,7 @@ import { softRemove } from '/imports/api/parenting/softRemove.js'; import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import '/imports/api/library/methods/index.js'; +import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js'; let LibraryNodes = new Mongo.Collection('libraryNodes'); @@ -76,7 +77,12 @@ const insertNode = new ValidatedMethod({ run(libraryNode) { delete libraryNode._id; assertNodeEditPermission(libraryNode, this.userId); - return LibraryNodes.insert(libraryNode); + let nodeId = LibraryNodes.insert(libraryNode); + if (libraryNode.type == 'reference'){ + libraryNode._id = nodeId; + updateReferenceNodeWork(libraryNode, this.userId); + } + return nodeId; }, }); @@ -109,9 +115,14 @@ const updateLibraryNode = new ValidatedMethod({ } else { modifier = {$set: {[pathString]: value}}; } - return LibraryNodes.update(_id, modifier, { + let numUpdated = LibraryNodes.update(_id, modifier, { selector: {type: node.type}, }); + if (node.type == 'reference'){ + node = LibraryNodes.findOne(_id); + updateReferenceNodeWork(node, this.userId); + } + return numUpdated; }, }); diff --git a/app/imports/api/library/methods/index.js b/app/imports/api/library/methods/index.js index 771eba44..1b566bc5 100644 --- a/app/imports/api/library/methods/index.js +++ b/app/imports/api/library/methods/index.js @@ -1 +1,2 @@ import '/imports/api/library/methods/duplicateLibraryNode.js'; +import '/imports/api/library/methods/updateReferenceNode.js'; diff --git a/app/imports/api/library/methods/updateReferenceNode.js b/app/imports/api/library/methods/updateReferenceNode.js new file mode 100644 index 00000000..d74f93fc --- /dev/null +++ b/app/imports/api/library/methods/updateReferenceNode.js @@ -0,0 +1,67 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import SimpleSchema from 'simpl-schema'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { + assertDocEditPermission, + assertViewPermission, +} from '/imports/api/sharing/sharingPermissions.js'; +import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; + +const updateReferenceNode = new ValidatedMethod({ + name: 'libraryNodes.updateReferenceNode', + validate: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + } + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id}) { + let userId = this.userId; + let node = LibraryNodes.findOne(_id); + assertDocEditPermission(node, userId); + updateReferenceNodeWork(node, userId); + }, +}); + +function writeCache(_id, cache){ + LibraryNodes.update(_id, {$set: {cache}}, { + selector: {type: 'reference'}, + }); +} + +function updateReferenceNodeWork(node, userId){ + let cache = {} + if (!node.ref){ + writeCache(node._id, cache); + return; + } + let doc, library; + try { + doc = fetchDocByRef(node.ref); + if (doc.removed) throw 'Property has been deleted'; + if (doc.ancestors[0].id !== node.ancestors[0].id){ + library = fetchDocByRef(doc.ancestors[0]); + assertViewPermission(library, userId) + } + } catch(e){ + cache = {error: e.reason || e.message || e.toString()} + writeCache(node._id, cache); + return; + } + cache = { + node: {name: doc.name, type: doc.type}, + }; + if (library){ + cache.library = {name: library.name}; + } + writeCache(node._id, cache); +} + +export default updateReferenceNode; +export { updateReferenceNodeWork } diff --git a/app/imports/api/properties/References.js b/app/imports/api/properties/References.js new file mode 100644 index 00000000..3367ef3c --- /dev/null +++ b/app/imports/api/properties/References.js @@ -0,0 +1,47 @@ +import SimpleSchema from 'simpl-schema'; + +let ReferenceSchema = new SimpleSchema({ + ref: { + type: Object, + defaultValue: {}, + }, + 'ref.id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + 'ref.collection': { + type: String, + optional: true, + }, + // Denormalised store of referenced property's details + cache: { + type: Object, + defaultValue: {}, + }, + 'cache.error': { + type: String, + optional: true, + }, + 'cache.node': { + type: Object, + optional: true, + }, + 'cache.node.name': { + type: String, + optional: true, + }, + 'cache.node.type': { + type: String, + }, + 'cache.library': { + type: Object, + optional: true, + }, + 'cache.library.name': { + type: String, + optional: true, + }, +}); + +export { ReferenceSchema }; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 0cf8afcc..09ce6265 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; +import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js'; @@ -42,6 +43,7 @@ const propertySchemasIndex = { note: ComputedOnlyNoteSchema, proficiency: ProficiencySchema, propertySlot: ComputedOnlySlotSchema, + reference: ReferenceSchema, roll: ComputedOnlyRollSchema, savingThrow: ComputedOnlySavingThrowSchema, skill: ComputedOnlySkillSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 5bb0e9d0..02366c05 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedItemSchema } from '/imports/api/properties/Items.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; +import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedSkillSchema } from '/imports/api/properties/Skills.js'; @@ -40,6 +41,7 @@ const propertySchemasIndex = { note: ComputedNoteSchema, proficiency: ProficiencySchema, propertySlot: ComputedSlotSchema, + reference: ReferenceSchema, roll: ComputedRollSchema, savingThrow: ComputedSavingThrowSchema, skill: ComputedSkillSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 9a3596ab..3ad0c4d9 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -13,6 +13,7 @@ import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FolderSchema } from '/imports/api/properties/Folders.js'; import { NoteSchema } from '/imports/api/properties/Notes.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; +import { ReferenceSchema } from '/imports/api/properties/References.js'; import { RollSchema } from '/imports/api/properties/Rolls.js'; import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { SkillSchema } from '/imports/api/properties/Skills.js'; @@ -40,6 +41,7 @@ const propertySchemasIndex = { note: NoteSchema, proficiency: ProficiencySchema, propertySlot: SlotSchema, + reference: ReferenceSchema, roll: RollSchema, savingThrow: SavingThrowSchema, skill: SkillSchema, diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index bf37c57c..94b42355 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -12,7 +12,7 @@ function assertIdValid(userId){ function assertdocExists(doc){ if (!doc){ throw new Meteor.Error('Permission denied', - 'No such document exists'); + 'Permission denied: No such document exists'); } } diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index ab8e5b0b..bb60d32e 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -67,6 +67,11 @@ const PROPERTIES = Object.freeze({ icon: '$vuetify.icons.roll', name: 'Roll' }, + reference: { + icon: 'link', + name: 'Reference', + libraryOnly: true, + }, savingThrow: { icon: '$vuetify.icons.saving_throw', name: 'Saving throw' diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue index 35932cc9..e24407fe 100644 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue +++ b/app/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue @@ -1,6 +1,7 @@ + + + + diff --git a/app/imports/ui/properties/forms/ReferenceForm.vue b/app/imports/ui/properties/forms/ReferenceForm.vue new file mode 100644 index 00000000..27df3b78 --- /dev/null +++ b/app/imports/ui/properties/forms/ReferenceForm.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js index 7f2c9a8c..2bc5ac35 100644 --- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js +++ b/app/imports/ui/properties/forms/shared/propertyFormIndex.js @@ -14,6 +14,7 @@ import FolderForm from '/imports/ui/properties/forms/FolderForm.vue'; import ItemForm from '/imports/ui/properties/forms/ItemForm.vue'; import NoteForm from '/imports/ui/properties/forms/NoteForm.vue'; import ProficiencyForm from '/imports/ui/properties/forms/ProficiencyForm.vue'; +import ReferenceForm from '/imports/ui/properties/forms/ReferenceForm.vue'; import RollForm from '/imports/ui/properties/forms/RollForm.vue'; import SavingThrowForm from '/imports/ui/properties/forms/SavingThrowForm.vue'; import SkillForm from '/imports/ui/properties/forms/SkillForm.vue'; @@ -41,6 +42,7 @@ export default { note: NoteForm, proficiency: ProficiencyForm, propertySlot: SlotForm, + reference: ReferenceForm, roll: RollForm, savingThrow: SavingThrowForm, skill: SkillForm, diff --git a/app/imports/ui/properties/shared/PropertySelector.vue b/app/imports/ui/properties/shared/PropertySelector.vue index a4ed79ea..f7005695 100644 --- a/app/imports/ui/properties/shared/PropertySelector.vue +++ b/app/imports/ui/properties/shared/PropertySelector.vue @@ -9,33 +9,35 @@ wrap fill-height > - - + -
- - {{ property.icon }} - -
-

- {{ property.name }} -

-
-
+
+ + {{ property.icon }} + +
+

+ {{ property.name }} +

+ + + @@ -43,8 +45,12 @@ diff --git a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js index ac417af7..86fe7968 100644 --- a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js +++ b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js @@ -4,6 +4,7 @@ import ItemTreeNode from '/imports/ui/properties/treeNodeViews/ItemTreeNode.vue' import DamageTreeNode from '/imports/ui/properties/treeNodeViews/DamageTreeNode.vue'; import EffectTreeNode from '/imports/ui/properties/treeNodeViews/EffectTreeNode.vue'; import ClassLevelTreeNode from '/imports/ui/properties/treeNodeViews/ClassLevelTreeNode.vue'; +import ReferenceTreeNode from '/imports/ui/properties/treeNodeViews/ReferenceTreeNode.vue'; export default { default: DefaultTreeNode, @@ -12,4 +13,5 @@ export default { damage: DamageTreeNode, effect: EffectTreeNode, item: ItemTreeNode, + reference: ReferenceTreeNode, } diff --git a/app/imports/ui/properties/viewers/ReferenceViewer.vue b/app/imports/ui/properties/viewers/ReferenceViewer.vue new file mode 100644 index 00000000..ddbccf23 --- /dev/null +++ b/app/imports/ui/properties/viewers/ReferenceViewer.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js index b7a5f138..32b260d4 100644 --- a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js +++ b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js @@ -14,6 +14,7 @@ import FolderViewer from '/imports/ui/properties/viewers/FolderViewer.vue'; import ItemViewer from '/imports/ui/properties/viewers/ItemViewer.vue'; import NoteViewer from '/imports/ui/properties/viewers/NoteViewer.vue'; import ProficiencyViewer from '/imports/ui/properties/viewers/ProficiencyViewer.vue'; +import ReferenceViewer from '/imports/ui/properties/viewers/ReferenceViewer.vue'; import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue'; import SkillViewer from '/imports/ui/properties/viewers/SkillViewer.vue'; import SavingThrowViewer from '/imports/ui/properties/viewers/SavingThrowViewer.vue'; @@ -42,6 +43,7 @@ export default { proficiency: ProficiencyViewer, propertySlot: SlotViewer, roll: RollViewer, + reference: ReferenceViewer, savingThrow: SavingThrowViewer, slotFiller: SlotFillerViewer, skill: SkillViewer,