diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index 87b5f072..e8522fae 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -28,6 +28,12 @@ let CreaturePropertySchema = new SimpleSchema({ type: storedIconsSchema, optional: true, }, + // Reference to the library node that this property was copied from + libraryNodeId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, // Denormalised flag if this property is inactive on the sheet for any reason // Including being disabled, or a decendent of a disabled property inactive: { diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js new file mode 100644 index 00000000..1bba9421 --- /dev/null +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -0,0 +1,44 @@ +export default function getSlotFillFilter({slot, libraryIds}){ + let filter = { + removed: {$ne: true}, + $and: [] + }; + if (libraryIds){ + filter['ancestors.id'] = {$in: libraryIds}; + } + if (slot.slotType){ + filter.$and.push({ + $or: [{ + type: slot.slotType + },{ + type: 'slotFiller', + slotFillerType: slot.slotType, + }] + }); + } + let tagsOr = []; + let tagsNor = []; + if (slot.slotTags && slot.slotTags.length){ + tagsOr.push({tags: {$all: slot.slotTags}}); + } + if (slot.extraTags && slot.extraTags.length){ + slot.extraTags.forEach(extra => { + if (!extra.tags || !extra.tags.length) return; + if (extra.operation === 'OR'){ + tagsOr.push({tags: {$all: extra.tags}}); + } else if (extra.operation === 'NOT'){ + tagsNor.push({tags: {$all: extra.tags}}); + } + }); + } + if (tagsOr.length){ + filter.$and.push({$or: tagsOr}); + } + if (tagsNor.length){ + filter.$and.push({$nor: tagsNor}); + } + if (!filter.$and.length){ + delete filter.$and; + } + return filter; +} diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index a507d01f..0d05f29d 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -107,6 +107,9 @@ function insertPropertyFromNode(nodeId, ancestors, order){ // It must get the first generated ID to prevent flickering nodes = [node, ...nodes]; + // set libraryNodeIds + storeLibraryNodeReferences(nodes, nodeId); + // re-map all the ancestors setLineageOfDocs({ docArray: nodes, @@ -135,6 +138,13 @@ function insertPropertyFromNode(nodeId, ancestors, order){ return node; } + +function storeLibraryNodeReferences(nodes){ + nodes.forEach(node => { + node.libraryNodeId = node._id; + }); +} + // Covert node references into actual nodes // TODO: check permissions for each library a reference node references function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js index 4228eca0..c1054490 100644 --- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js @@ -4,6 +4,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { get } from 'lodash'; const pushToProperty = new ValidatedMethod({ name: 'creatureProperties.push', @@ -19,9 +20,26 @@ const pushToProperty = new ValidatedMethod({ let rootCreature = getRootCreatureAncestor(property); assertEditPermission(rootCreature, this.userId); + let joinedPath = path.join('.'); + + // Respect maxCount + let schema = CreatureProperties.simpleSchema(property); + let maxCount = schema.get(joinedPath, 'maxCount'); + + if (Number.isFinite(maxCount)){ + let array = get(property, path); + let currentCount = array ? array.length : 0; + if (currentCount >= maxCount){ + throw new Meteor.Error( + 'Array is full', + `Cannot have more than ${maxCount} values` + ); + } + } + // Do work CreatureProperties.update(_id, { - $push: {[path.join('.')]: value}, + $push: {[joinedPath]: value}, }, { selector: {type: property.type}, }); diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 872d6171..dd25fd41 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -21,6 +21,31 @@ let SlotSchema = new SimpleSchema({ 'slotTags.$': { type: String, }, + extraTags: { + type: Array, + defaultValue: [], + maxCount: 5, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + }, + 'extraTags.$.tags': { + type: Array, + }, + 'extraTags.$.tags.$': { + type: String, + }, quantityExpected: { type: String, optional: true, @@ -37,7 +62,19 @@ let SlotSchema = new SimpleSchema({ hideWhenFull: { type: Boolean, optional: true, - } + defaultValue: true, + }, + unique: { + type: String, + allowedValues: [ + // Can't choose the same slot filler twice in this slot + 'uniqueInSlot', + // Can't choose the same slot filler twice accross the whole creature + 'uniqueInCreature' + ], + optional: true, + defaultValue: 'uniqueInSlot', + }, }); const ComputedOnlySlotSchema = new SimpleSchema({ diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js index a22b8a65..94dbf7fc 100644 --- a/app/imports/server/publications/slotFillers.js +++ b/app/imports/server/publications/slotFillers.js @@ -2,6 +2,7 @@ import { check } from 'meteor/check'; import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js' Meteor.publish('slotFillers', function(slotId){ let self = this; @@ -21,7 +22,7 @@ Meteor.publish('slotFillers', function(slotId){ fields: {subscribedLibraries: 1} }); const subs = user && user.subscribedLibraries || []; - let libraryIds = Libraries.find({ + let libraries = Libraries.find({ $or: [ {owner: this.userId}, {writers: this.userId}, @@ -29,25 +30,13 @@ Meteor.publish('slotFillers', function(slotId){ {_id: {$in: subs}}, ] }, { - fields: {_id: 1}, - }).map(lib => lib._id); + fields: {_id: 1, name: 1}, + }); + let libraryIds = libraries.map(lib => lib._id); // Build a filter for nodes in those libraries that match the slot - let filter = { - 'ancestors.id': {$in: libraryIds}, - removed: {$ne: true}, - }; - if (slot.slotTags && slot.slotTags.length){ - filter.tags = {$all: slot.slotTags}; - } - if (slot.slotType){ - filter.$or = [{ - type: slot.slotType - },{ - type: 'slotFiller', - slotFillerType: slot.slotType, - }]; - } + let filter = getSlotFillFilter({slot, libraryIds}); + this.autorun(function(){ // Get the limit of the documents the user can fetch var limit = self.data('limit') || 50; @@ -85,7 +74,7 @@ Meteor.publish('slotFillers', function(slotId){ self.setData('countAll', LibraryNodes.find(filter).count()); }); self.autorun(function () { - return LibraryNodes.find(filter, options); + return [LibraryNodes.find(filter, options), libraries]; }); }); }); diff --git a/app/imports/ui/creature/slots/OldSlotFillDialog.vue b/app/imports/ui/creature/slots/OldSlotFillDialog.vue new file mode 100644 index 00000000..949460ab --- /dev/null +++ b/app/imports/ui/creature/slots/OldSlotFillDialog.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 86ce48a5..31d3571d 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -18,134 +18,97 @@ @keyup.enter="insert" /> -
+ - -
-
- - + + + + + @@ -153,21 +116,32 @@ diff --git a/app/imports/ui/library/LibraryNodeExpansionContent.vue b/app/imports/ui/library/LibraryNodeExpansionContent.vue index 19b3199b..aac0f104 100644 --- a/app/imports/ui/library/LibraryNodeExpansionContent.vue +++ b/app/imports/ui/library/LibraryNodeExpansionContent.vue @@ -6,6 +6,7 @@ class="property-viewer" /> - + + + + mdi-plus + + + + + +
+ + + + mdi-delete + +
+
+ = maxCount; + } + }, + methods: { + acknowledgeAddResult(){ + this.addExtraTagsLoading = false; + }, + addExtraTags(){ + this.addExtraTagsLoading = true; + this.$emit('push', { + path: ['extraTags'], + value: { + _id: Random.id(), + operation: 'OR', + tags: [], + }, + ack: this.acknowledgeAddResult, + }); + }, + }, };