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 @@