Progress on forms overhaul: insert lib node broken

This commit is contained in:
Stefan Zermatten
2023-04-17 21:43:46 +02:00
parent d643886a7f
commit cf09abaa57
18 changed files with 274 additions and 96 deletions

View File

@@ -72,14 +72,12 @@ let LibraryNodeSchema = new SimpleSchema({
optional: true,
max: STORAGE_LIMITS.variableName,
},
/* TODO: Disabled for now until image upload is working
// Image to display when filling the slot
slotFillImage: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
*/
// Fill more than one quantity in a slot, like feats and ability score
// improvements, filtered out of UI if there isn't space in quantityExpected
slotQuantityFilled: {

View File

@@ -45,7 +45,10 @@
<v-tab :disabled="!type">
Create
</v-tab>
<v-tab :disabled="!type">
<v-tab
v-if="!hideLibraryTab"
:disabled="!type"
>
Library
</v-tab>
</v-tabs>
@@ -77,7 +80,10 @@
/>
</v-card-text>
</v-tab-item>
<v-tab-item :disabled="!type">
<v-tab-item
v-if="!hideLibraryTab"
:disabled="!type"
>
<v-expansion-panels
multiple
inset
@@ -222,6 +228,7 @@ export default {
type: Object,
default: undefined,
},
hideLibraryTab: Boolean,
},
reactiveProvide: {
name: 'context',
@@ -258,7 +265,6 @@ export default {
this.changeType(this.type);
},
methods: {
propertyHelpChanged(value){
Meteor.users.setPreference.call({
preference: 'hidePropertySelectDialogHelp',
@@ -291,10 +297,6 @@ export default {
if (this.currentLimit >= this.countAll) return;
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
},
insert(){
if (!this.selectedNodeIds.length) return;
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
},
changeType(type){
this._subs.searchLibraryNodes.setData('type', type);
if (!type) return;

View File

@@ -208,7 +208,9 @@ export default {
}
});
},
change({path, value, ack}){
change(arg) {
const { path, value, ack } = arg;
console.log('creaturePropDialogChangeHandler', arg);
if (path && path[0] === 'equipped'){
equipItem.call({_id: this.currentId, equipped: value}, ack);
return;

View File

@@ -28,15 +28,16 @@
size="64"
/>
</div>
<component
:is="model.type + 'Form'"
<property-form
v-else-if="model && editing"
:key="_id"
class="library-node-form"
:model="model"
:embedded="embedded"
@change="change"
@push="push"
@pull="pull"
@add-child="addLibraryNode"
/>
<component
:is="model.type + 'Viewer'"
@@ -87,6 +88,7 @@
pullFromLibraryNode,
softRemoveLibraryNode,
restoreLibraryNode,
insertNode,
} from '/imports/api/library/LibraryNodes.js';
import duplicateLibraryNode from '/imports/api/library/methods/duplicateLibraryNode.js';
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
@@ -103,15 +105,12 @@
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle.js';
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
let formIndex = {};
for (let key in propertyFormIndex){
formIndex[key + 'Form'] = propertyFormIndex[key];
}
import { getHighestOrder } from '/imports/api/parenting/order.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import PropertyForm from '/imports/client/ui/properties/PropertyForm.vue';
let viewerIndex = {};
for (let key in propertyViewerIndex){
formIndex[key + 'Viewer'] = propertyViewerIndex[key];
viewerIndex[key + 'Viewer'] = propertyViewerIndex[key];
}
export default {
@@ -119,7 +118,7 @@
PropertyToolbar,
PropertyIcon,
DialogBase,
...formIndex,
PropertyForm,
...viewerIndex,
},
props: {
@@ -272,6 +271,44 @@
}
});
},
addLibraryNode() {
// Check tier has paid benefits
let tier = getUserTier(Meteor.userId());
if (!(tier && tier.paidBenefits)){
this.$store.commit('pushDialogStack', {
component: 'tier-too-low-dialog',
elementId: 'insert-library-node-button',
});
return;
}
let parentPropertyId = this.model._id;
this.$store.commit('pushDialogStack', {
component: 'add-creature-property-dialog',
elementId: 'insert-creature-property-btn',
data: {
parentDoc: this.model,
creatureId: this.creatureId,
hideLibraryTab: true,
},
callback(result){
if (!result) return;
let parentRef = {
id: parentPropertyId,
collection: 'libraryNodes',
};
let order = getHighestOrder({
collection: LibraryNodes,
ancestorId: parentRef.id,
}) + 0.5;
let creatureProperty = result;
// Get order and parent
creatureProperty.order = order;
// Insert the property
let id = insertNode.call({creatureProperty, parentRef});
return `tree-node-${id}`;
}
});
},
remove(){
let _id = this.currentId;
softRemoveLibraryNode.call({_id});

View File

@@ -11,27 +11,105 @@
label="Name"
style="flex-basis: 320px;"
:value="model.name"
@change="change('name', ...arguments)"
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
/>
<icon-color-menu
:model="model"
@change="$emit('change', $event)"
@change="e => $emit('change', e)"
/>
</v-col>
</v-row>
<component
:is="model.type"
class="creature-property-form"
class="creature-property-form mb-4"
:model="model"
@change="$emit('change', ...arguments)"
@push="$emit('push', ...arguments)"
@pull="$emit('pull', ...arguments)"
@change="e => $emit('change', e)"
@push="e => $emit('push', e)"
@pull="e => $emit('pull', e)"
>
<form-section
v-if="context.isLibraryForm"
name="Library Options"
>
<v-row
v-if="context.isLibraryForm"
>
<v-col
cols="12"
md="6"
>
<smart-select
label="Slot Fill Type"
style="flex-basis: 300px;"
clearable
hint="The property type that this slot filler pretends to be when being searched for by a slot"
:items="slotTypes"
:value="model.slotFillerType"
@change="(value, ack) => $emit('change', {path: ['slotFillerType'], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
label="Slot Fill Quantity"
type="number"
min="0"
hint="How many properties this counts as when filling a slot"
:value="model.slotQuantityFilled"
@change="(value, ack) => $emit('change', {path: ['slotQuantityFilled'], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
v-if="context.isLibraryForm"
label="Condition"
hint="A caclulation to determine if this property can be added to a character"
placeholder="Always active"
:value="model.slotFillerCondition"
@change="(value, ack) => $emit('change', {path: ['slotFillerCondition'], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
v-if="context.isLibraryForm"
label="Condition Error Text"
hint="Text to display if the condition isn't met"
placeholder="Always active"
:value="model.slotFillerConditionNote"
@change="(value, ack) => $emit('change', {path: ['slotFillerConditionNote'], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<smart-combobox
label="Library Tags"
multiple
chips
deletable-chips
hint="Used to let slots find this property in a library"
:value="model.tags"
@change="(value, ack) => $emit('change', {path: ['libraryTags'], value, ack})"
/>
</v-col>
</v-row>
</form-section>
</component>
<v-divider
class="mb-4"
/>
<v-divider inset />
<v-row>
<v-col
cols="12"
md="6"
>
<smart-combobox
label="Tags"
@@ -40,7 +118,7 @@
deletable-chips
hint="Tags let other properties target this property with interactions"
:value="model.tags"
@change="change('tags', ...arguments)"
@change="(value, ack) => $emit('change', {path: ['tags'], value, ack})"
/>
</v-col>
</v-row>
@@ -71,7 +149,7 @@
tile
color="accent"
data-id="insert-creature-property-btn"
@click="$emit('add-child')"
@click="$event=>$emit('add-child')"
>
<v-icon left>
mdi-plus
@@ -83,9 +161,15 @@
tile
color="accent"
data-id="insert-creature-property-btn"
@click="$emit('add-child')"
@click="$event=>$emit('add-child')"
>
...Other
<v-icon
v-if="!suggestedChildren.length"
left
>
mdi-plus
</v-icon>
{{ suggestedChildren.length ? '...Other' : 'Child' }}
</v-btn>
</outlined-input>
</v-col>
@@ -106,7 +190,13 @@ import IconColorMenu from '/imports/client/ui/properties/forms/shared/IconColorM
import CreaturePropertiesTree from '/imports/client/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue';
import { getSuggestedChildren } from '/imports/constants/PROPERTIES.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
const slotTypes = [];
for (let key in PROPERTIES) {
slotTypes.push({ text: PROPERTIES[key].name, value: key });
}
export default {
components: {
ComputedField,
@@ -118,6 +208,9 @@ export default {
OutlinedInput,
...propertyFormIndex,
},
inject: {
context: { default: {} }
},
props: {
model: {
type: [Object, Array],
@@ -125,19 +218,28 @@ export default {
},
embedded: Boolean, // This dialog is embedded in a page
},
data() {
return {
slotTypes,
};
},
computed: {
suggestedChildren() {
if (!this.model?.type) return;
return getSuggestedChildren(this.model.type);
},
},
methods: {
change(path, value, ack){
if (!Array.isArray(path)){
path = [path];
mounted() {
// Don't autofocus on mobile, it brings up the on-screen keyboard
if (this.$vuetify.breakpoint.smAndDown) return;
setTimeout(() => {
if (this.$refs.focusFirst && this.$refs.focusFirst.focus) {
this.$refs.focusFirst.focus()
}
this.$emit('change', {path, value, ack});
},
}, 300);
},
methods: {
selectSubProperty(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',

View File

@@ -172,6 +172,7 @@
@change="change('reset', ...arguments)"
/>
</form-section>
<slot />
</form-sections>
</div>
</template>

View File

@@ -65,13 +65,15 @@
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section
v-if="$slots.children"
name="Children"
standalone
>
<slot name="children" />
</form-section>
<form-sections>
<form-section
v-if="$slots.children"
name="Children"
>
<slot name="children" />
</form-section>
<slot />
</form-sections>
</div>
</template>

View File

@@ -259,6 +259,7 @@
/>
</div>
</form-section>
<slot />
</form-sections>
</div>
</template>

View File

@@ -44,12 +44,14 @@
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section
name="Children"
standalone
>
<slot name="children" />
</form-section>
<form-sections>
<form-section
name="Children"
>
<slot name="children" />
</form-section>
<slot />
</form-sections>
</div>
</template>

View File

@@ -93,6 +93,7 @@
@change="change('tags', ...arguments)"
/>
</form-section>
<slot />
</form-sections>
</div>
</template>

View File

@@ -1,31 +1,8 @@
<template lang="html">
<div class="folder-form">
<div class="layout wrap">
<text-field
ref="focusFirst"
label="Name"
style="flex-basis: 300px;"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<form-sections>
<form-section
v-if="$slots.children"
name="Children"
>
<slot name="children" />
</form-section>
<form-section name="Advanced">
<smart-combobox
label="Tags"
multiple
chips
deletable-chips
:value="model.tags"
@change="change('tags', ...arguments)"
/>
<smart-switch
label="Group children on a card"
:value="model.groupStats"
@@ -69,6 +46,7 @@
</div>
</v-expand-transition>
</form-section>
<slot />
</form-sections>
</div>
</div>

View File

@@ -19,22 +19,12 @@ export default {
default: () => ({}),
},
},
mounted(){
// Don't autofocus on mobile, it brings up the on-screen keyboard
if (this.$vuetify.breakpoint.smAndDown) return;
setTimeout(() => {
if (this.$refs.focusFirst && this.$refs.focusFirst.focus){
this.$refs.focusFirst.focus()
}
}, 300);
},
methods: {
change(path, value, ack){
if (!Array.isArray(path)){
change(path, value, ack) {
if (!Array.isArray(path)) {
path = [path];
}
this.$emit('change', {path, value, ack});
this.$emit('change', { path, value, ack });
}
},
}

View File

@@ -1,5 +1,17 @@
<template lang="html">
<div class="folder-viewer" />
<div class="folder-viewer">
<v-row
dense
align="stretch"
justify="center"
justify-sm="start"
>
<property-field
name="Name"
:value="model.name"
/>
</v-row>
</div>
</template>
<script lang="js">

View File

@@ -377,7 +377,11 @@ RouterFactory.configure(router => {
function redirectIfMaintenance(to, from, next) {
if (!MAINTENANCE_MODE) return next();
if (to?.path === '/admin' || to?.path === '/maintenance' || to?.path === '/sign-in') return next();
if (
to?.path === '/admin' ||
to?.path === '/maintenance' ||
to?.path === '/sign-in'
) return next();
Tracker.autorun((computation) => {
if (userSubscription.ready()) {
computation.stop();

View File

@@ -170,6 +170,9 @@ const PROPERTIES = Object.freeze({
helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot',
suggestedParents: [],
},
/* Deprecated
* Slot fillers are no longer suggested as possible property types,
* but existing slot fillers are still supported for backwards compatibility
slotFiller: {
icon: 'mdi-power-plug-outline',
name: 'Slot filler',
@@ -177,6 +180,7 @@ const PROPERTIES = Object.freeze({
helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
suggestedParents: ['propertySlot'],
},
*/
spellList: {
icon: '$vuetify.icons.spell_list',
name: 'Spell list',

View File

@@ -25,15 +25,22 @@ Migrations.add({
});
export function migratePropUp(bulk, prop) {
let update;
// If the prop is a slot filler with an image, move it
if (prop.type === 'slotFiller' && typeof prop.picture === 'string') {
update = { $set: {} };
update.$set.slotFillImage = prop.picture;
update.$unset = { picture: 1 };
}
// If there are tags, copy them to libraryTags and set findable flags
if (Array.isArray(prop.tags) && prop.tags.length) {
bulk.find({ _id: prop._id }).updateOne({
$set: {
libraryTags: prop.tags,
fillSlots: true,
searchable: true,
},
});
update = update || { $set: {} };
update.$set.libraryTags = prop.tags;
update.$set.fillSlots = true;
update.$set.searchable = true;
}
if (update) {
bulk.find({ _id: prop._id }).updateOne(update);
}
}

View File

@@ -58,6 +58,33 @@ const emptyFolderExample = {
}],
};
const exampleSlotFiller = {
_id: 'DXPYsHKF6888h3hZs',
type: 'slotFiller',
name: 'Slot Filler Example',
'picture': 'https://url.to.pic',
'tags': ['slot', 'tags'],
'parent': {
'collection': 'creatures',
'id': 'm9sdCvs6iDf7qRaGv',
},
'ancestors': [{
'collection': 'creatures',
'id': 'm9sdCvs6iDf7qRaGv',
}],
};
const expectedSlotFillerUpdate = {
$set: {
'libraryTags': ['slot', 'tags'],
'fillSlots': true,
'searchable': true,
'slotFillImage': 'https://url.to.pic',
},
$unset: {
picture: 1,
},
};
const DownMergeExample = {
_id: 'DXPYsHKF6W8Hh3hZs',
type: 'feature',
@@ -104,6 +131,13 @@ describe('dbv2 Migrate library nodes', function () {
assert.isUndefined(update, 'There should be no update on a prop with no tags');
assert.equal(timesUpdate, 0, 'Update should be called zero times on a prop with no tags');
});
it('Migrates slot fillers up', function () {
const bulk = stubBulk();
migratePropUp(bulk, exampleSlotFiller);
const { query, update } = bulk.result();
assert.deepEqual(query, { _id: 'DXPYsHKF6888h3hZs' }, 'The query should match the id of the given prop');
assert.deepEqual(update, expectedSlotFillerUpdate, 'The update should match the expected update');
});
it('Merges tags when down migrating', function () {
const bulk = stubBulk();
migratePropDown(bulk, DownMergeExample);

View File

@@ -1 +1,2 @@
import './dbv1/dbv1.js';
import './dbv2/dbv2.js';