Started re-implementing slot fill dialog with different design pattern

This commit is contained in:
Stefan Zermatten
2021-08-10 10:32:56 +02:00
parent 9f62a78eb0
commit 8ecefb03ad
10 changed files with 674 additions and 201 deletions

View File

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

View File

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

View File

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

View File

@@ -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},
});

View File

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

View File

@@ -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];
});
});
});

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

View File

@@ -18,134 +18,97 @@
@keyup.enter="insert"
/>
</template>
<div
class="library-nodes"
<property-description
:string="model.description"
/>
<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"
<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: libraryNode._disabled}"
>
<v-expansion-panel-header>
<template #default="{ open }">
<v-checkbox
v-model="selectedNodeIds"
class="my-0 py-0 mr-2 flex-grow-0"
hide-details
:value="libraryNode._id"
:disabled="libraryNode._disabled"
@click.stop
/>
<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>
</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>,
<v-layout column>
<v-layout>
<tree-node-view :model="libraryNode" />
<div
v-if="libraryNode._disabledBySlotFillerCondition"
class="error--text"
>
{{ libraryNode.slotFillerCondition }}
</div>
</v-layout>
<div class="text-caption">
{{ libraryNames[libraryNode.ancestors[0].id ] }}
</div>
</v-layout>
<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>
<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>
</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>
<template v-if="!showDisabled && disabledNodeCount">
<v-layout
column
align-center
justify-center
>
<div class="mt-4 ma-2">
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
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="selectedNodeIds.length >= 15">
{{ selectedNodeIds.length }}/20
</template>
Insert
</v-btn>
</template>
@@ -153,21 +116,32 @@
</template>
<script lang="js">
/**
* TODO
* Show the tags that are being searched for/against
* Show the quantity to fill with this dialog
* Show the quantity filled by the selection
* 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
*/
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';
export default {
components: {
DialogBase,
TreeNodeView,
PropertyDescription,
LibraryNodeExpansionContent,
},
props:{
slotId: {
@@ -180,29 +154,16 @@ 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;
},
methods: {
searchChanged(val, ack){
this._subs['slotFillers'].setData('searchTerm', val);
this._subs['slotFillers'].setData('limit', undefined);
@@ -217,7 +178,16 @@ export default {
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,
},
});
},
},
meteor: {
$subscribe: {
@@ -237,47 +207,51 @@ export default {
countAll(){
return this._subs['slotFillers'].data('countAll');
},
totalQuantitySelected(){
return 0; //TODO
},
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;
// 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._disabled = true;
node._disabledBySlotFillerCondition = true;
}
}
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
if (
node.type === 'slotFiller' &&
this.model.spaceLeft > 0 &&
node.slotQuantityFilled > this.model.spaceLeft
quantityToFill > this.spaceLeft
){
return false;
node._disabled = true;
node._disabledByQuantityFilled = true;
}
if (node._disabledBySlotFillerCondition){
disabledNodeCount += 1;
}
return true;
});
this.numFiltered = totalNodes - nodes.length;
if (nodes.length === 1) this.selectedNode = nodes[0];
this.disabledNodeCount = disabledNodeCount;
return nodes;
},
}
@@ -285,17 +259,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>

View File

@@ -6,6 +6,7 @@
class="property-viewer"
/>
<tree-node-list
v-if="$subReady.descendantLibraryNodes"
group="library-node-expansion"
:children="propertyChildren"
@selected="clickChild"

View File

@@ -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.slotType"
:error-messages="errors.slotType"
@change="change('slotType', ...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>