Merge branch 'version-2' into version-2-dev
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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){
|
||||
|
||||
@@ -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},
|
||||
});
|
||||
|
||||
5
app/imports/api/creature/creatures/getCreatureUrlName.js
Normal file
5
app/imports/api/creature/creatures/getCreatureUrlName.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import getSlug from 'speakingurl';
|
||||
|
||||
export default function getCreatureUrlName({name}){
|
||||
return getSlug(name, {maintainCase: true}) || '-';
|
||||
}
|
||||
@@ -90,11 +90,6 @@ const insertExperience = new ValidatedMethod({
|
||||
throw new Meteor.Error('Experiences.methods.insert.denied',
|
||||
'You need to be logged in to insert an experience');
|
||||
}
|
||||
let tier = getUserTier(this.userId);
|
||||
if (!tier.paidBenefits){
|
||||
throw new Meteor.Error('Experiences.methods.insert.denied',
|
||||
`The ${tier.name} tier does not allow you to grant experience`);
|
||||
}
|
||||
let insertedIds = [];
|
||||
creatureIds.forEach(creatureId => {
|
||||
let id = insertExperienceForCreature({experience, creatureId, userId});
|
||||
@@ -123,11 +118,6 @@ const removeExperience = new ValidatedMethod({
|
||||
throw new Meteor.Error('Experiences.methods.remove.denied',
|
||||
'You need to be logged in to remove an experience');
|
||||
}
|
||||
let tier = getUserTier(this.userId);
|
||||
if (!tier.paidBenefits){
|
||||
throw new Meteor.Error('Experiences.methods.remove.denied',
|
||||
`The ${tier.name} tier does not allow you to remove an experience`);
|
||||
}
|
||||
let experience = Experiences.findOne(experienceId);
|
||||
if (!experience) return;
|
||||
let creatureId = experience.creatureId
|
||||
@@ -168,11 +158,6 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
throw new Meteor.Error('Experiences.methods.recompute.denied',
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
}
|
||||
let tier = getUserTier(this.userId);
|
||||
if (!tier.paidBenefits){
|
||||
throw new Meteor.Error('Experiences.methods.recompute.denied',
|
||||
`The ${tier.name} tier does not allow you to recompute a creature's experiences`);
|
||||
}
|
||||
assertEditPermission(creatureId, userId);
|
||||
|
||||
let xp = 0;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js'
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
/*
|
||||
* Actions are things a character can do
|
||||
* Any rolls that are children of actions will be rolled when taking the action
|
||||
@@ -12,14 +14,17 @@ let ActionSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
summary: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.summary,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
// What time-resource is used to take the action in combat
|
||||
// long actions take longer than 1 round to cast
|
||||
@@ -41,9 +46,11 @@ let ActionSchema = new SimpleSchema({
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
// Duplicate the ResourceSchema here so we can extend it elegantly.
|
||||
resources: {
|
||||
@@ -53,6 +60,7 @@ let ActionSchema = new SimpleSchema({
|
||||
'resources.itemsConsumed': {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.resourcesCount,
|
||||
},
|
||||
'resources.itemsConsumed.$': {
|
||||
type: Object,
|
||||
@@ -79,6 +87,7 @@ let ActionSchema = new SimpleSchema({
|
||||
'resources.attributesConsumed': {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.resourcesCount,
|
||||
},
|
||||
'resources.attributesConsumed.$': {
|
||||
type: Object,
|
||||
@@ -102,6 +111,7 @@ let ActionSchema = new SimpleSchema({
|
||||
uses: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
// Integer of how many times it has already been used
|
||||
usesUsed: {
|
||||
@@ -120,14 +130,14 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
summaryCalculations: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: 32,
|
||||
maxCount: STORAGE_LIMITS.inlineCalculationCount,
|
||||
},
|
||||
'summaryCalculations.$': InlineComputationSchema,
|
||||
|
||||
descriptionCalculations: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: 32,
|
||||
maxCount: STORAGE_LIMITS.inlineCalculationCount,
|
||||
},
|
||||
'descriptionCalculations.$': InlineComputationSchema,
|
||||
|
||||
@@ -138,6 +148,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
usesErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
},
|
||||
'usesErrors.$':{
|
||||
type: ErrorSchema,
|
||||
@@ -158,6 +169,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
},
|
||||
'resources.itemsConsumed.$.itemName': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
optional: true,
|
||||
},
|
||||
'resources.itemsConsumed.$.itemIcon': {
|
||||
@@ -167,6 +179,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
'resources.itemsConsumed.$.itemColor': {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.color,
|
||||
},
|
||||
'resources.attributesConsumed': Array,
|
||||
'resources.attributesConsumed.$': Object,
|
||||
@@ -182,6 +195,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
|
||||
'resources.attributesConsumed.$.statName': {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// True if the uses left is zero, or any item or attribute consumed is
|
||||
// insufficient
|
||||
|
||||
@@ -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({
|
||||
|
||||
14
app/imports/constants/STORAGE_LIMITS.js
Normal file
14
app/imports/constants/STORAGE_LIMITS.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const STORAGE_LIMITS = Object.freeze({
|
||||
name: 140,
|
||||
color: 10000,
|
||||
summary: 10000,
|
||||
description: 49473, //the length of the Bee Movie script
|
||||
inlineCalculationCount: 32,
|
||||
errorCount: 32,
|
||||
tagCount: 64,
|
||||
tagLength: 140,
|
||||
resourcesCount: 32,
|
||||
calculation: 280,
|
||||
});
|
||||
|
||||
export default STORAGE_LIMITS;
|
||||
@@ -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,28 +30,16 @@ 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') || 20;
|
||||
var limit = self.data('limit') || 50;
|
||||
check(limit, Number);
|
||||
|
||||
// Get the search term
|
||||
@@ -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];
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
310
app/imports/ui/creature/slots/OldSlotFillDialog.vue
Normal file
310
app/imports/ui/creature/slots/OldSlotFillDialog.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template lang="html">
|
||||
<dialog-base
|
||||
:color="model.color"
|
||||
dark-body
|
||||
>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
{{ model.name }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<text-field
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
hide-details
|
||||
:value="searchValue"
|
||||
:debounce="300"
|
||||
@change="searchChanged"
|
||||
@keyup.enter="insert"
|
||||
/>
|
||||
</template>
|
||||
<p
|
||||
v-if="model.description"
|
||||
class="description"
|
||||
>
|
||||
{{ model.description }}
|
||||
</p>
|
||||
<div
|
||||
class="library-nodes"
|
||||
>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div v-if="libraryNodes && libraryNodes.length">
|
||||
<v-container fluid>
|
||||
<v-row
|
||||
dense
|
||||
align="center"
|
||||
align-md="start"
|
||||
>
|
||||
<v-col
|
||||
v-for="node in libraryNodes"
|
||||
:key="node._id"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<v-card
|
||||
hover
|
||||
ripple
|
||||
class="slot-card layout column justify-end"
|
||||
:class="{'selected': node._id === (selectedNode && selectedNode._id)}"
|
||||
:dark="node._id === (selectedNode && selectedNode._id)"
|
||||
@click="selectedNode = node"
|
||||
>
|
||||
<v-img
|
||||
v-if="node.picture"
|
||||
:src="node.picture"
|
||||
:height="200"
|
||||
contain
|
||||
class="slot-card-image"
|
||||
/>
|
||||
<v-card-title primary-title>
|
||||
<tree-node-view
|
||||
class="mr-2 text-h6 mb-0"
|
||||
:class="{'theme--dark': node._id === (selectedNode && selectedNode._id)}"
|
||||
:hide-icon="node.picture"
|
||||
:model="node"
|
||||
:color="node.color"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
v-if="node.description"
|
||||
class="pt-0"
|
||||
>
|
||||
<property-description
|
||||
class="slot-card-text line-clamp"
|
||||
:string="node.description"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="countAll"
|
||||
class="ma-4"
|
||||
>
|
||||
<h4 v-if="numFiltered">
|
||||
Requirements of {{ numFiltered }} library properties were not met.
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
Nothing suitable was found in your libraries.
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$subReady.slotFillers"
|
||||
class="ma-4"
|
||||
>
|
||||
<h4>
|
||||
Nothing suitable was found in your libraries
|
||||
<span v-if="searchValue">
|
||||
matching "{{ searchValue }}"
|
||||
</span>
|
||||
</h4>
|
||||
<p>
|
||||
This slot requires a {{ slotPropertyTypeName }}
|
||||
<template v-if="model.slotTags.length == 1">
|
||||
with the tag <code>{{ model.slotTags[0] }}</code>,
|
||||
</template>
|
||||
<template v-else-if="model.slotTags.length > 1">
|
||||
with the following tags:
|
||||
<span
|
||||
v-for="(tag, index) in model.slotTags"
|
||||
:key="index"
|
||||
>
|
||||
<code>{{ tag }}</code>,
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="model.spaceLeft">
|
||||
that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div
|
||||
v-if="!$subReady.slotFillers"
|
||||
key="character-loading"
|
||||
class="fill-height layout justify-center align-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div
|
||||
v-if="currentLimit < countAll"
|
||||
class="layout justify-center align-stretch"
|
||||
>
|
||||
<v-btn
|
||||
:loading="!$subReady.slotFillers"
|
||||
class="primary"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
<template slot="actions">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
:disabled="!selectedNode"
|
||||
@click="insert"
|
||||
>
|
||||
Insert
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
|
||||
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
PropertyDescription,
|
||||
},
|
||||
props:{
|
||||
slotId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
selectedNode: undefined,
|
||||
searchValue: undefined,
|
||||
numFiltered: 0,
|
||||
}},
|
||||
computed: {
|
||||
slotPropertyTypeName(){
|
||||
if (!this.model) return;
|
||||
if (!this.model.slotType) return 'property';
|
||||
let propName = getPropertyName(this.model.slotType);
|
||||
return propName && propName.toLowerCase();
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
methods:{
|
||||
getTitle(model){
|
||||
if (!model) return;
|
||||
if (model.name) return model.name;
|
||||
let prop = PROPERTIES[model.type]
|
||||
return prop && prop.name;
|
||||
},
|
||||
searchChanged(val, ack){
|
||||
this._subs['slotFillers'].setData('searchTerm', val);
|
||||
this._subs['slotFillers'].setData('limit', undefined);
|
||||
this.selectedNode = undefined;
|
||||
this.searchValue = val;
|
||||
setTimeout(ack, 200);
|
||||
},
|
||||
loadMore(){
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs['slotFillers'].setData('limit', this.currentLimit + 50);
|
||||
},
|
||||
insert(){
|
||||
if (!this.selectedNode) return;
|
||||
this.$store.dispatch('popDialogStack', this.selectedNode);
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'slotFillers'(){
|
||||
return [this.slotId]
|
||||
},
|
||||
},
|
||||
model(){
|
||||
return CreatureProperties.findOne(this.slotId);
|
||||
},
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
currentLimit(){
|
||||
return this._subs['slotFillers'].data('limit') || 50;
|
||||
},
|
||||
countAll(){
|
||||
return this._subs['slotFillers'].data('countAll');
|
||||
},
|
||||
libraryNodes(){
|
||||
let filter = {
|
||||
removed: {$ne: true},
|
||||
};
|
||||
if (this.model.slotTags && this.model.slotTags.length){
|
||||
filter.tags = {$all: this.model.slotTags};
|
||||
}
|
||||
if (this.model.slotType){
|
||||
filter.$or = [{
|
||||
type: this.model.slotType
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
slotFillerType: this.model.slotType,
|
||||
}];
|
||||
}
|
||||
let nodes = LibraryNodes.find(filter, {
|
||||
sort: {name: 1, order: 1}
|
||||
}).fetch();
|
||||
let totalNodes = nodes.length;
|
||||
// Filter out slotFillers whose condition isn't met or are too big to fit
|
||||
// the quantity to fill
|
||||
nodes = nodes.filter(node => {
|
||||
if (node.slotFillerCondition){
|
||||
let {result} = evaluateString({
|
||||
string: node.slotFillerCondition,
|
||||
scope: this.creature.variables,
|
||||
fn: 'reduce',
|
||||
});
|
||||
if (!result.value) return false;
|
||||
}
|
||||
if (
|
||||
node.type === 'slotFiller' &&
|
||||
this.model.spaceLeft > 0 &&
|
||||
node.slotQuantityFilled > this.model.spaceLeft
|
||||
){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.numFiltered = totalNodes - nodes.length;
|
||||
if (nodes.length === 1) this.selectedNode = nodes[0];
|
||||
return nodes;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.slot-card-text.line-clamp {
|
||||
-webkit-line-clamp: 5;
|
||||
}
|
||||
.slot-card.selected {
|
||||
background: #8E1B1B;
|
||||
}
|
||||
</style>
|
||||
@@ -18,134 +18,154 @@
|
||||
@keyup.enter="insert"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="library-nodes"
|
||||
<property-description
|
||||
:string="model.description"
|
||||
/>
|
||||
<p>
|
||||
{{ slotPropertyTypeName }} with tags:
|
||||
<template v-for="(tags, index) in tagsSearched.or">
|
||||
<property-tags
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
:prefix="index ? 'OR' : undefined"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="(tags, index) in tagsSearched.not">
|
||||
<property-tags
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
prefix="NOT"
|
||||
/>
|
||||
</template>
|
||||
</p>
|
||||
<v-expansion-panels
|
||||
multiple
|
||||
inset
|
||||
>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div v-if="libraryNodes && libraryNodes.length">
|
||||
<section
|
||||
class="layout wrap justify-between"
|
||||
>
|
||||
<v-card
|
||||
v-for="node in libraryNodes"
|
||||
:key="node._id"
|
||||
hover
|
||||
ripple
|
||||
class="slot-card layout column justify-end"
|
||||
:class="{'selected': node._id === (selectedNode && selectedNode._id)}"
|
||||
:dark="node._id === (selectedNode && selectedNode._id)"
|
||||
@click="selectedNode = node"
|
||||
>
|
||||
<v-img
|
||||
v-if="node.picture"
|
||||
:src="node.picture"
|
||||
:height="200"
|
||||
contain
|
||||
class="slot-card-image"
|
||||
/>
|
||||
<v-card-title primary-title>
|
||||
<tree-node-view
|
||||
class="mr-2 text-h6 mb-0"
|
||||
:class="{'theme--dark': node._id === (selectedNode && selectedNode._id)}"
|
||||
:hide-icon="node.picture"
|
||||
:model="node"
|
||||
:color="node.color"
|
||||
/>
|
||||
</v-card-title>
|
||||
<v-card-text
|
||||
v-if="node.description"
|
||||
class="pt-0"
|
||||
<template v-for="libraryNode in libraryNodes">
|
||||
<v-expansion-panel
|
||||
v-if="showDisabled || !libraryNode._disabledBySlotFillerCondition"
|
||||
:key="libraryNode._id"
|
||||
:model="libraryNode"
|
||||
:data-id="libraryNode._id"
|
||||
:class="{disabled: isDisabled(libraryNode)}"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<template #default="{ open }">
|
||||
<v-layout
|
||||
align-center
|
||||
class="flex-grow-0 mr-2"
|
||||
>
|
||||
<property-description
|
||||
class="slot-card-text line-clamp"
|
||||
:string="node.description"
|
||||
<v-checkbox
|
||||
v-if="libraryNode._disabledByAlreadyAdded"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:value="true"
|
||||
:disabled="true"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="countAll"
|
||||
class="ma-4"
|
||||
>
|
||||
<h4 v-if="numFiltered">
|
||||
Requirements of {{ numFiltered }} library properties were not met.
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
Nothing suitable was found in your libraries.
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$subReady.slotFillers"
|
||||
class="ma-4"
|
||||
>
|
||||
<h4>
|
||||
Nothing suitable was found in your libraries
|
||||
<span v-if="searchValue">
|
||||
matching "{{ searchValue }}"
|
||||
</span>
|
||||
</h4>
|
||||
<p>
|
||||
This slot requires a {{ slotPropertyTypeName }}
|
||||
<template v-if="model.slotTags.length == 1">
|
||||
with the tag <code>{{ model.slotTags[0] }}</code>,
|
||||
</template>
|
||||
<template v-else-if="model.slotTags.length > 1">
|
||||
with the following tags:
|
||||
<span
|
||||
v-for="(tag, index) in model.slotTags"
|
||||
:key="index"
|
||||
<v-checkbox
|
||||
v-else
|
||||
v-model="selectedNodeIds"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:disabled="isDisabled(libraryNode)"
|
||||
:value="libraryNode._id"
|
||||
@click.stop
|
||||
/>
|
||||
</v-layout>
|
||||
<v-layout column>
|
||||
<v-layout align-center>
|
||||
<tree-node-view :model="libraryNode" />
|
||||
<div
|
||||
v-if="libraryNode._disabledBySlotFillerCondition"
|
||||
class="error--text text-no-wrap text-truncate"
|
||||
>
|
||||
{{ libraryNode.slotFillerCondition }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div class="text-caption text-no-wrap text-truncate">
|
||||
{{ libraryNames[libraryNode.ancestors[0].id ] }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div
|
||||
v-if="libraryNode.slotQuantityFilled && libraryNode.slotQuantityFilled !== 1"
|
||||
class="text-overline flex-grow-0 text-no-wrap"
|
||||
:class="{
|
||||
'error--text': isDisabled(libraryNode) &&
|
||||
libraryNode._disabledByQuantityFilled
|
||||
}"
|
||||
>
|
||||
<code>{{ tag }}</code>,
|
||||
</span>
|
||||
{{ libraryNode.slotQuantityFilled }} slots
|
||||
</div>
|
||||
<template v-if="open">
|
||||
<v-btn
|
||||
icon
|
||||
class="flex-grow-0"
|
||||
@click.stop="openPropertyDetails(libraryNode._id)"
|
||||
>
|
||||
<v-icon>mdi-window-restore</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</template>
|
||||
<span v-if="model.spaceLeft">
|
||||
that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }}
|
||||
</span>
|
||||
</p>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<library-node-expansion-content :model="libraryNode" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
</v-expansion-panels>
|
||||
<v-layout
|
||||
v-if="!$subReady.slotFillers || currentLimit < countAll"
|
||||
column
|
||||
align-center
|
||||
justify-center
|
||||
class="ma-3"
|
||||
>
|
||||
<v-btn
|
||||
:loading="!$subReady.slotFillers"
|
||||
color="accent"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
<template v-if="!showDisabled && disabledNodeCount">
|
||||
<v-layout
|
||||
column
|
||||
align-center
|
||||
justify-center
|
||||
class="ma-3"
|
||||
>
|
||||
<div>
|
||||
Requirements of {{ disabledNodeCount }} properties were not met
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div
|
||||
v-if="!$subReady.slotFillers"
|
||||
key="character-loading"
|
||||
class="fill-height layout justify-center align-center"
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
elevation="0"
|
||||
color="accent"
|
||||
@click="showDisabled = true"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div
|
||||
v-if="currentLimit < countAll"
|
||||
class="layout justify-center align-stretch"
|
||||
>
|
||||
<v-btn
|
||||
:loading="!$subReady.slotFillers"
|
||||
class="primary"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
Show All
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
</template>
|
||||
<template slot="actions">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
:disabled="!selectedNode"
|
||||
@click="insert"
|
||||
color="primary"
|
||||
:disabled="!selectedNodeIds.length"
|
||||
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
|
||||
>
|
||||
<template v-if="model.spaceLeft">
|
||||
{{ totalQuantitySelected }} / {{ model.spaceLeft }}
|
||||
</template>
|
||||
Insert
|
||||
</v-btn>
|
||||
</template>
|
||||
@@ -153,21 +173,33 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
/**
|
||||
* TODO
|
||||
* Enforce unique in slot/unique in character selection rules
|
||||
* Fix the dialog callback for multiple property inserting
|
||||
* Show the dialog in library view to test slots
|
||||
* Delete the old slot fill dialog
|
||||
*/
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
|
||||
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
||||
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
PropertyDescription,
|
||||
LibraryNodeExpansionContent,
|
||||
PropertyTags,
|
||||
},
|
||||
props:{
|
||||
slotId: {
|
||||
@@ -180,29 +212,41 @@ export default {
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
selectedNode: undefined,
|
||||
selectedNodeIds: [],
|
||||
searchValue: undefined,
|
||||
numFiltered: 0,
|
||||
showDisabled: false,
|
||||
disabledNodeCount: undefined,
|
||||
}},
|
||||
computed: {
|
||||
slotPropertyTypeName(){
|
||||
if (!this.model) return;
|
||||
if (!this.model.slotType) return 'property';
|
||||
let propName = getPropertyName(this.model.slotType);
|
||||
return propName && propName.toLowerCase();
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
methods:{
|
||||
getTitle(model){
|
||||
if (!model) return;
|
||||
if (model.name) return model.name;
|
||||
let prop = PROPERTIES[model.type]
|
||||
return prop && prop.name;
|
||||
computed: {
|
||||
tagsSearched(){
|
||||
let or = [];
|
||||
let not = [];
|
||||
if (this.model.slotTags && this.model.slotTags.length){
|
||||
or.push(this.model.slotTags);
|
||||
}
|
||||
this.model.extraTags?.forEach(extras => {
|
||||
if (extras.tags?.length){
|
||||
if(extras.operation === 'OR'){
|
||||
or.push(extras.tags);
|
||||
} else if (extras.operation === 'NOT'){
|
||||
not.push(extras.tags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {or, not};
|
||||
},
|
||||
slotPropertyTypeName(){
|
||||
if (!this.model) return;
|
||||
if (!this.model.slotType) return 'Property';
|
||||
let propName = getPropertyName(this.model.slotType);
|
||||
return propName;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
searchChanged(val, ack){
|
||||
this._subs['slotFillers'].setData('searchTerm', val);
|
||||
this._subs['slotFillers'].setData('limit', undefined);
|
||||
@@ -212,11 +256,28 @@ export default {
|
||||
},
|
||||
loadMore(){
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs['slotFillers'].setData('limit', this.currentLimit + 20);
|
||||
this._subs['slotFillers'].setData('limit', this.currentLimit + 50);
|
||||
},
|
||||
insert(){
|
||||
if (!this.selectedNode) return;
|
||||
this.$store.dispatch('popDialogStack', this.selectedNode);
|
||||
},
|
||||
openPropertyDetails(id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
isDisabled(node){
|
||||
return node._disabledBySlotFillerCondition ||
|
||||
node._disabledByAlreadyAdded ||
|
||||
(
|
||||
node._disabledByQuantityFilled &&
|
||||
!this.selectedNodeIds.includes(node._id)
|
||||
)
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
@@ -232,52 +293,96 @@ export default {
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
currentLimit(){
|
||||
return this._subs['slotFillers'].data('limit') || 20;
|
||||
return this._subs['slotFillers'].data('limit') || 50;
|
||||
},
|
||||
countAll(){
|
||||
return this._subs['slotFillers'].data('countAll');
|
||||
},
|
||||
alreadyAdded(){
|
||||
let added = new Set();
|
||||
if (this.model.unique) return added;
|
||||
let ancestorId;
|
||||
if (this.model.unique === 'uniqueInSlot'){
|
||||
ancestorId = this.model._id;
|
||||
} else if (this.model.unique === 'uniqueInCreature'){
|
||||
ancestorId = this.creatureId;
|
||||
}
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': ancestorId,
|
||||
libraryNodeId: {$exists: true},
|
||||
}, {
|
||||
fields: {libraryNodeId: 1},
|
||||
}).forEach(prop => {
|
||||
added.add(prop.libraryNodeId);
|
||||
});
|
||||
return added;
|
||||
},
|
||||
totalQuantitySelected(){
|
||||
let quantitySelected = 0;
|
||||
LibraryNodes.find({
|
||||
_id: {$in: this.selectedNodeIds}
|
||||
}, {
|
||||
fields: {slotQuantityFilled: 1},
|
||||
}).forEach(node => {
|
||||
if (Number.isFinite(node.slotQuantityFilled)){
|
||||
quantitySelected += node.slotQuantityFilled;
|
||||
} else {
|
||||
quantitySelected += 1;
|
||||
}
|
||||
});
|
||||
return quantitySelected;
|
||||
},
|
||||
spaceLeft(){
|
||||
if (this.model.quantityExpectedResult === 0) return undefined;
|
||||
return this.model.spaceLeft - this.totalQuantitySelected;
|
||||
},
|
||||
libraryNames(){
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
},
|
||||
libraryNodes(){
|
||||
let filter = {
|
||||
removed: {$ne: true},
|
||||
};
|
||||
if (this.model.slotTags && this.model.slotTags.length){
|
||||
filter.tags = {$all: this.model.slotTags};
|
||||
}
|
||||
if (this.model.slotType){
|
||||
filter.$or = [{
|
||||
type: this.model.slotType
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
slotFillerType: this.model.slotType,
|
||||
}];
|
||||
}
|
||||
let filter = getSlotFillFilter({slot: this.model});
|
||||
let nodes = LibraryNodes.find(filter, {
|
||||
sort: {name: 1, order: 1}
|
||||
}).fetch();
|
||||
let totalNodes = nodes.length;
|
||||
// Filter out slotFillers whose condition isn't met or are too big to fit
|
||||
let disabledNodeCount = 0;
|
||||
let activeNodeCount = 0;
|
||||
let lastActiveNodeId = undefined;
|
||||
// Mark slotFillers whose condition isn't met or are too big to fit
|
||||
// the quantity to fill
|
||||
nodes = nodes.filter(node => {
|
||||
nodes.forEach(node => {
|
||||
if (node.slotFillerCondition){
|
||||
let {result} = evaluateString({
|
||||
string: node.slotFillerCondition,
|
||||
scope: this.creature.variables,
|
||||
fn: 'reduce',
|
||||
});
|
||||
if (!result.value) return false;
|
||||
if (!result.value){
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
}
|
||||
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
|
||||
if (
|
||||
quantityToFill > this.spaceLeft
|
||||
){
|
||||
node._disabledByQuantityFilled = true;
|
||||
}
|
||||
if (this.alreadyAdded.has(node._id)){
|
||||
node._disabledByAlreadyAdded = true;
|
||||
}
|
||||
if (
|
||||
node.type === 'slotFiller' &&
|
||||
this.model.spaceLeft > 0 &&
|
||||
node.slotQuantityFilled > this.model.spaceLeft
|
||||
!node._disabledBySlotFillerCondition &&
|
||||
!node._disabledByQuantityFilled &&
|
||||
!node._disabledByAlreadyAdded
|
||||
){
|
||||
return false;
|
||||
activeNodeCount += 1;
|
||||
lastActiveNodeId = node._id;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.numFiltered = totalNodes - nodes.length;
|
||||
if (nodes.length === 1) this.selectedNode = nodes[0];
|
||||
this.disabledNodeCount = disabledNodeCount;
|
||||
if (activeNodeCount === 1) this.selectedNodeIds = [lastActiveNodeId];
|
||||
return nodes;
|
||||
},
|
||||
}
|
||||
@@ -285,17 +390,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.slot-card {
|
||||
max-width: 500px;
|
||||
width: 300px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 4px;
|
||||
}
|
||||
.slot-card-text.line-clamp {
|
||||
-webkit-line-clamp: 5;
|
||||
}
|
||||
.slot-card.selected {
|
||||
background: #8E1B1B;
|
||||
.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,9 +66,10 @@
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
|
||||
|
||||
const characterTransform = function(char){
|
||||
char.url = `/character/${char._id}/${char.urlName || '-'}`;
|
||||
char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
|
||||
char.initial = char.name && char.name[0] || '?';
|
||||
return char;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
class="property-viewer"
|
||||
/>
|
||||
<tree-node-list
|
||||
v-if="$subReady.descendantLibraryNodes"
|
||||
group="library-node-expansion"
|
||||
:children="propertyChildren"
|
||||
@selected="clickChild"
|
||||
|
||||
@@ -82,9 +82,10 @@
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
|
||||
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
|
||||
|
||||
const characterTransform = function(char){
|
||||
char.url = `/character/${char._id}/${char.urlName || '-'}`;
|
||||
char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
|
||||
char.initial = char.name && char.name[0] || '?';
|
||||
return char;
|
||||
};
|
||||
|
||||
@@ -80,12 +80,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.skill-list-tile >>> .v-list__tile {
|
||||
height: 34px;
|
||||
}
|
||||
.skill-list-tile{
|
||||
background: inherit;
|
||||
}
|
||||
.prof-icon {
|
||||
min-width: 30px;
|
||||
}
|
||||
@@ -95,7 +89,3 @@ export default {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
$list-item-min-height: 32px;
|
||||
</style>
|
||||
|
||||
@@ -17,16 +17,63 @@
|
||||
:error-messages="errors.slotType"
|
||||
@change="change('slotType', ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Tags Required"
|
||||
hint="The slot must be filled with a property which has all the listed tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:value="model.slotTags"
|
||||
:error-messages="errors.slotTags"
|
||||
@change="change('slotTags', ...arguments)"
|
||||
/>
|
||||
<v-layout align-center>
|
||||
<v-btn
|
||||
icon
|
||||
style="margin-top: -30px;"
|
||||
class="mr-2"
|
||||
:loading="addExtraTagsLoading"
|
||||
:disabled="extraTagsFull"
|
||||
@click="addExtraTags"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<smart-combobox
|
||||
label="Tags Required"
|
||||
hint="The slot must be filled with a property which has all the listed tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:value="model.slotTags"
|
||||
:error-messages="errors.slotTags"
|
||||
@change="change('slotTags', ...arguments)"
|
||||
/>
|
||||
</v-layout>
|
||||
<v-slide-x-transition group>
|
||||
<div
|
||||
v-for="(extras, i) in model.extraTags"
|
||||
:key="extras._id"
|
||||
class="extra-tags layout align-center justify-space-between"
|
||||
>
|
||||
<smart-select
|
||||
label="Operation"
|
||||
style="width: 90px; flex-grow: 0;"
|
||||
:items="extraTagOperations"
|
||||
:value="extras.operation"
|
||||
:error-messages="errors.extraTags && errors.extraTags[i]"
|
||||
@change="change(['extraTags', i, 'operation'], ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
:hint="extras.operation === 'OR' ? 'The slot can be filled with a property that has all of these tags instead' : 'The slot cannot be filled with a property that has any of these tags'"
|
||||
class="mx-2"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
:value="extras.tags"
|
||||
@change="change(['extraTags', i, 'tags'], ...arguments)"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
style="margin-top: -30px;"
|
||||
@click="$emit('pull', {path: ['extraTags', i]})"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
<text-field
|
||||
label="Quantity"
|
||||
hint="How many matching properties must be used to fill this slot, 0 is unlimited"
|
||||
@@ -75,8 +122,19 @@
|
||||
@change="change('ignored', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
<smart-select
|
||||
label="Unique"
|
||||
style="flex-basis: 300px;"
|
||||
clearable
|
||||
hint="Do the properties that fill this slot need to be unique?"
|
||||
:items="uniqueOptions"
|
||||
:value="model.unique"
|
||||
:error-messages="errors.unique"
|
||||
@change="change('unique', ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
hint="This slot's own tags which will be used to fill other slots"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
@@ -92,6 +150,7 @@
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import { SlotSchema } from '/imports/api/properties/Slots.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -104,7 +163,42 @@
|
||||
for (let key in PROPERTIES){
|
||||
slotTypes.push({text: PROPERTIES[key].name, value: key});
|
||||
}
|
||||
return {slotTypes};
|
||||
return {
|
||||
slotTypes,
|
||||
addExtraTagsLoading: false,
|
||||
extraTagOperations: ['OR', 'NOT'],
|
||||
uniqueOptions: [{
|
||||
text: 'Each property inside this slot should be unique',
|
||||
value: 'uniqueInSlot',
|
||||
}, {
|
||||
text: 'Properties in this slot should be unique accross the whole character',
|
||||
value: 'uniqueInCreature',
|
||||
}],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
extraTagsFull(){
|
||||
if (!this.model.extraTags) return false;
|
||||
let maxCount = SlotSchema.get('extraTags', 'maxCount');
|
||||
return this.model.extraTags.length >= 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,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
v-if="tagString"
|
||||
v-if="tags.length"
|
||||
class="tags"
|
||||
:class="{'ma-3': !noMargin}"
|
||||
:class="{'ma-2': !noMargin}"
|
||||
>
|
||||
{{ tagString }}
|
||||
<span
|
||||
v-if="prefix"
|
||||
class="mx-1 text-overline"
|
||||
>
|
||||
{{ prefix }}
|
||||
</span>
|
||||
<v-chip
|
||||
v-for="(tag, i) in tags"
|
||||
:key="tag + i"
|
||||
class="mx-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,17 +28,13 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
noMargin: Boolean,
|
||||
prefix: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
tagString(){
|
||||
return this.tags.join(', ');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tags {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -130,18 +130,9 @@ RouterFactory.configure(factory => {
|
||||
meta: {
|
||||
title: 'Library',
|
||||
},
|
||||
},{
|
||||
path: '/character/:id/:urlName',
|
||||
components: {
|
||||
default: CharacterSheetPage,
|
||||
toolbar: CharacterSheetToolbar,
|
||||
rightDrawer: CharacterSheetRightDrawer,
|
||||
},
|
||||
meta: {
|
||||
title: 'Character Sheet',
|
||||
},
|
||||
},{
|
||||
path: '/character/:id',
|
||||
alias: '/character/:id/:urlName',
|
||||
components: {
|
||||
default: CharacterSheetPage,
|
||||
toolbar: CharacterSheetToolbar,
|
||||
|
||||
23
app/package-lock.json
generated
23
app/package-lock.json
generated
@@ -2330,6 +2330,11 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"mongo-object": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-0.1.4.tgz",
|
||||
@@ -2805,6 +2810,11 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -2956,9 +2966,9 @@
|
||||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
|
||||
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz",
|
||||
"integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==",
|
||||
"requires": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
@@ -2966,13 +2976,6 @@
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"request": "^2.88.2",
|
||||
"simpl-schema": "^1.12.0",
|
||||
"source-map-support": "^0.5.16",
|
||||
"speakingurl": "^14.0.1",
|
||||
"styles": "^0.2.1",
|
||||
"underscore": "^1.13.1",
|
||||
"vue": "2.6.10",
|
||||
|
||||
Reference in New Issue
Block a user