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 @@
+
+
+ {{ model.description }}
+
+ This slot requires a {{ slotPropertyTypeName }}
+
+ with the tag
+ Requirements of {{ numFiltered }} library properties were not met.
+
+
+ Nothing suitable was found in your libraries.
+
+
+ Nothing suitable was found in your libraries
+
+ matching "{{ searchValue }}"
+
+
+ {{ model.slotTags[0] }},
+
+
+ with the following tags:
+
+ {{ tag }},
+
+
+
+ that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }}
+
+
- This slot requires a {{ slotPropertyTypeName }}
-
- with the tag {{ model.slotTags[0] }},
+ {{ tag }},
-
-
-
- that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }}
-
-