diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 3af576c8..a507d01f 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -21,7 +21,11 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; const insertPropertyFromLibraryNode = new ValidatedMethod({ name: 'creatureProperties.insertPropertyFromLibraryNode', validate: new SimpleSchema({ - nodeId: { + nodeIds: { + type: Array, + max: 20, + }, + 'nodeIds.$': { type: String, regEx: SimpleSchema.RegEx.Id, }, @@ -38,7 +42,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({nodeId, parentRef, order}) { + run({nodeIds, parentRef, order}) { // get the new ancestry for the properties let {parentDoc, ancestors} = getAncestry({parentRef}); @@ -53,54 +57,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ } assertEditPermission(rootCreature, this.userId); - // 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}, - }); - if (!node) throw `Node not found for nodeId: ${nodeId}`; - let oldParent = node.parent; - let nodes = LibraryNodes.find({ - 'ancestors.id': nodeId, - removed: {$ne: true}, - }).fetch(); + // {libraryId: hasViewPermission} + //let libraryPermissionMemoir = {}; + let node; + nodeIds.forEach(nodeId => { + // TODO: Check library view permission for each node before starting + node = insertPropertyFromNode(nodeId, ancestors, order); + }); - // 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]; - - // re-map all the ancestors - setLineageOfDocs({ - docArray: nodes, - newAncestry: ancestors, - oldParent, - }); - - // Give the docs new IDs without breaking internal references - renewDocIds({ - docArray: nodes, - collectionMap: {'libraryNodes': 'creatureProperties'} - }); - - // Order the root node - if (order === undefined){ - setDocToLastOrder({ - collection: CreatureProperties, - doc: node, - }); - } else { - node.order = order; - } - - // Insert the creature properties - CreatureProperties.batchInsert(nodes); - - // get the root inserted doc + // get one of the root inserted docs let rootId = node._id; // Tree structure changed by inserts, reorder the tree @@ -110,7 +75,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ }); // The library properties need to denormalise which of them are inactive - recomputeInactiveProperties(rootId); + recomputeInactiveProperties(rootCreature._id); // Some of the library properties may be items or containers recomputeInventory(rootCreature._id); // Inserting a creature property invalidates dependencies: full recompute @@ -120,6 +85,56 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ }, }); +function insertPropertyFromNode(nodeId, ancestors, order){ + // 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}, + }); + if (!node) throw `Node not found for nodeId: ${nodeId}`; + let oldParent = node.parent; + let nodes = LibraryNodes.find({ + 'ancestors.id': nodeId, + 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]; + + // re-map all the ancestors + setLineageOfDocs({ + docArray: nodes, + newAncestry: ancestors, + oldParent, + }); + + // Give the docs new IDs without breaking internal references + renewDocIds({ + docArray: nodes, + collectionMap: {'libraryNodes': 'creatureProperties'} + }); + + // Order the root node + if (order === undefined){ + setDocToLastOrder({ + collection: CreatureProperties, + doc: node, + }); + } else { + node.order = order; + } + + // Insert the creature properties + CreatureProperties.batchInsert(nodes); + return node; +} + // Covert node references into actual nodes // TODO: check permissions for each library a reference node references function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ @@ -194,7 +209,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ // TODO: Force the referencedNode to take the old id of the reference // such that the reference's children can be kept - + // Give the new referenced sub-tree new ids renewDocIds({ docArray: addedNodes, diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js index 41a8c83f..79086eb3 100644 --- a/app/imports/api/creature/creatures/methods/insertCreature.js +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -57,7 +57,7 @@ const insertCreature = new ValidatedMethod({ if (Meteor.isServer){ // Insert the 5e ruleset as the default base insertPropertyFromLibraryNode.call({ - nodeId: 'iHbhfcg3AL5isSWbw', + nodeIds: ['iHbhfcg3AL5isSWbw'], parentRef: {id: baseId, collection: 'creatureProperties'}, order: 0.5, }); diff --git a/app/imports/server/publications/index.js b/app/imports/server/publications/index.js index adff4e49..c58f6188 100644 --- a/app/imports/server/publications/index.js +++ b/app/imports/server/publications/index.js @@ -9,3 +9,4 @@ import '/imports/server/publications/tabletops.js'; import '/imports/server/publications/slotFillers.js'; import '/imports/server/publications/ownedDocuments.js'; import '/imports/server/publications/archivedCreatures.js'; +import '/imports/server/publications/searchLibraryNodes.js'; diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js index 0a25ff59..4f94b72b 100644 --- a/app/imports/server/publications/library.js +++ b/app/imports/server/publications/library.js @@ -2,13 +2,6 @@ import SimpleSchema from 'simpl-schema'; import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js'; -const standardLibraryIds = [ - 'SRDLibraryGA3XWsd', -]; - -Meteor.publish('standardLibraries', function(){ - return Libraries.find({_id: {$in: standardLibraryIds}}); -}); Meteor.publish('libraries', function(){ this.autorun(function (){ @@ -75,3 +68,24 @@ Meteor.publish('libraryNodes', function(libraryId){ ]; }); }); + +Meteor.publish('descendantLibraryNodes', function(nodeId){ + let node = LibraryNodes.findOne(nodeId); + let libraryId = node?.ancestors[0]?.id; + if (!libraryId) return []; + this.autorun(function (){ + let userId = this.userId; + let library = Libraries.findOne(libraryId); + try { assertViewPermission(library, userId) } + catch(e){ + return this.error(e); + } + return [ + LibraryNodes.find({ + 'ancestors.id': nodeId, + }, { + sort: {order: 1}, + }), + ]; + }); +}); diff --git a/app/imports/server/publications/searchLibraryNodes.js b/app/imports/server/publications/searchLibraryNodes.js new file mode 100644 index 00000000..79f1912e --- /dev/null +++ b/app/imports/server/publications/searchLibraryNodes.js @@ -0,0 +1,116 @@ +import { check } from 'meteor/check'; +import Libraries from '/imports/api/library/Libraries.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; + +Meteor.publish('searchLibraryNodes', function(){ + let self = this; + this.autorun(function (){ + let type = self.data('type'); + if (!type) return []; + + let userId = this.userId; + if (!userId) { + return []; + } + + // Get all the ids of libraries the user can access + const user = Meteor.users.findOne(userId, { + fields: {subscribedLibraries: 1} + }); + if (!user) return []; + + const subs = user.subscribedLibraries || []; + let libraries = Libraries.find({ + $or: [ + {owner: this.userId}, + {writers: this.userId}, + {readers: this.userId}, + {_id: {$in: subs}}, + ] + }, { + fields: {_id: 1, name: 1}, + }); + let libraryIds = libraries.map(lib => lib._id); + + // Build a filter for nodes in those libraries that match the type + let filter = { + 'ancestors.id': {$in: libraryIds}, + removed: {$ne: true}, + tags: {$ne: []}, // Only tagged library nodes are considered + }; + if (type){ + filter.$or = [{ + type, + },{ + type: 'slotFiller', + slotFillerType: type, + }]; + } + + this.autorun(function(){ + // Get the limit of the documents the user can fetch + var limit = self.data('limit') || 32; + check(limit, Number); + + // Get the search term + let searchTerm = self.data('searchTerm') || ''; + check(searchTerm, String); + + let options = undefined; + if (searchTerm){ + filter.$text = {$search: searchTerm}; + options = { + // relevant documents have a higher score. + fields: { + score: { $meta: 'textScore' } + }, + sort: { + // `score` property specified in the projection fields above. + score: { $meta: 'textScore' }, + 'ancestors.0.id': 1, + name: 1, + order: 1, + } + } + } else { + delete filter.$text + options = {sort: { + 'ancestors.0.id': 1, + name: 1, + order: 1, + }}; + } + options.limit = limit; + + this.autorun(function () { + self.setData('countAll', LibraryNodes.find(filter).count()); + }); + + let cursor = LibraryNodes.find(filter, options); + + Mongo.Collection._publishCursor(libraries, self, 'libraries'); + + let observeHandle = cursor.observeChanges({ + added: function (id, fields) { + fields._searchResult = true; + self.added('libraryNodes', id, fields); + }, + changed: function (id, fields) { + self.changed('libraryNodes', id, fields); + }, + removed: function (id) { + self.removed('libraryNodes', id); + } + }, + // Publications don't mutate the documents + { nonMutatingCallbacks: true } + ); + + // register stop callback (expects lambda w/ no args). + this.onStop(function () { + observeHandle.stop(); + }); + // this.ready(); + }); + }); +}); diff --git a/app/imports/ui/creature/character/CharacterSheetFab.vue b/app/imports/ui/creature/character/CharacterSheetFab.vue index bd8de830..ae1ec668 100644 --- a/app/imports/ui/creature/character/CharacterSheetFab.vue +++ b/app/imports/ui/creature/character/CharacterSheetFab.vue @@ -38,22 +38,13 @@ /> @@ -234,11 +225,39 @@ let nodeId = libraryNode._id; let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId); - let id = insertPropertyFromLibraryNode.call({nodeId, parentRef, order}); + let id = insertPropertyFromLibraryNode.call({nodeIds: [nodeId], parentRef, order}); return `tree-node-${id}`; } }); }, + addProperty(){ + let creatureId = this.creatureId; + let fab = hideFab(); + + this.$store.commit('pushDialogStack', { + component: 'add-creature-property-dialog', + elementId: 'add-creature-property-btn', + callback(result){ + revealFab(fab); + if (!result){ + return 'insert-creature-property-fab'; + } + let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId); + if (Array.isArray(result)){ + let nodeIds = result; + let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order}); + return `tree-node-${id}`; + } else { + let creatureProperty = result; + // Get order and parent + creatureProperty.order = order; + // Insert the property + let id = insertProperty.call({creatureProperty, parentRef}); + return `tree-node-${id}`; + } + } + }); + }, } } diff --git a/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue b/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue new file mode 100644 index 00000000..f30741b3 --- /dev/null +++ b/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue index 96abc130..951bbc13 100644 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue +++ b/app/imports/ui/creature/creatureProperties/CreaturePropertyInsertForm.vue @@ -46,44 +46,44 @@ import ColorPicker from '/imports/ui/components/ColorPicker.vue'; import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js'; export default { - components: { - ...propertyFormIndex, - DialogBase, + components: { + ...propertyFormIndex, + DialogBase, ColorPicker, - }, - mixins: [schemaFormMixin], - props: { - propertyName: String, - type: String, - }, + }, + mixins: [schemaFormMixin], + props: { + propertyName: String, + type: String, + }, reactiveProvide: { name: 'context', include: ['debounceTime'], }, - data(){return { - model: { - type: this.type, - }, - schema: undefined, - validationContext: undefined, + data(){return { + model: { + type: this.type, + }, + schema: undefined, + validationContext: undefined, debounceTime: 0, - };}, - watch: { - type(newType){ + };}, + watch: { + type(newType){ this.changeType(newType); - }, - }, + }, + }, mounted(){ this.changeType(this.type); }, methods:{ changeType(type){ if (!type) return; - this.schema = propertySchemasIndex[type]; - this.validationContext = this.schema.newContext(); - let model = this.schema.clean({}); - model.type = type; - this.model = model; + this.schema = propertySchemasIndex[type]; + this.validationContext = this.schema.newContext(); + let model = this.schema.clean({}); + model.type = type; + this.model = model; } }, } diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 5af45199..660f83d5 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -9,7 +9,7 @@ import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; - import nodesToTree from '/imports/api/parenting/nodesToTree.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'; diff --git a/app/imports/ui/library/LibraryNodeExpansionContent.vue b/app/imports/ui/library/LibraryNodeExpansionContent.vue new file mode 100644 index 00000000..19b3199b --- /dev/null +++ b/app/imports/ui/library/LibraryNodeExpansionContent.vue @@ -0,0 +1,61 @@ + + + + +