All properties added to the sheet now use the type/library/create UX

This commit is contained in:
Stefan Zermatten
2021-08-01 23:28:04 +02:00
parent 758cb2f8bc
commit 1e38295164
8 changed files with 397 additions and 321 deletions

View File

@@ -47,7 +47,7 @@ const PROPERTIES = Object.freeze({
name: 'Container',
helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack',
suggestedParents: [],
suggestedParents: ['folder'],
},
damage: {
icon: '$vuetify.icons.damage',

View File

@@ -31,10 +31,10 @@
:key="type"
color="primary"
:data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name"
:icon="properties[type].icon"
:label="type ? 'New ' + properties[type].name : 'New Property'"
:icon="type ? properties[type].icon : 'mdi-plus'"
:disabled="!editPermission"
@click="insertPropertyOfType(type)"
@click="addProperty(type)"
/>
<template v-if="tabNumber === 5">
<labeled-fab
@@ -42,7 +42,7 @@
color="primary"
data-id="add-creature-property-btn"
label="Add Property"
icon="mdi-pencil"
icon="mdi-plus"
:disabled="!editPermission"
@click="addProperty"
/>
@@ -53,10 +53,11 @@
<script lang="js">
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { getHighestOrder } from '/imports/api/parenting/order.js';
import insertProperty, { insertPropertyAsChildOfTag } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function getParentAndOrderFromSelectedTreeNode(creatureId){
// find the parent based on the currently selected property
@@ -140,120 +141,49 @@
'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'],
'character': ['note'],
'tree': [],
'tree': [null],
};},
properties(){
return PROPERTIES;
},
},
methods: {
insertPropertyOfType(type){
addProperty(forcedType){
let creatureId = this.creatureId;
let fab = hideFab();
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-type-' + type,
data: {
forcedType: type,
},
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Insert the property
creatureProperty.order = getHighestOrder({
collection: CreatureProperties,
ancestorId: creatureId
}) + 1;
let tagDetails;
switch (type){
case 'item':
tagDetails = {tag: 'carried', name: 'Carried'};
break;
case 'container':
tagDetails = {tag: 'inventory', name: 'Inventory'};
break;
default:
tagDetails = {tag: `${type}s`};
break;
}
let id = insertPropertyAsChildOfTag.call({
creatureProperty,
creatureId,
tag: tagDetails.tag,
tagDefaultName: tagDetails.name,
});
return id;
}
});
},
insertTreeProperty(){
let creatureId = this.creatureId;
let fab = hideFab();
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-btn',
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Get order and parent
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
}
});
},
propertyFromLibrary(){
let creatureId = this.creatureId;
let fab = hideFab();
this.$store.commit('pushDialogStack', {
component: 'creature-property-from-library-dialog',
elementId: 'insert-creature-property-from-library-btn',
callback(libraryNode){
if (!libraryNode) return 'insert-creature-property-fab';
revealFab(fab);
let nodeId = libraryNode._id;
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let id = insertPropertyFromLibraryNode.call({nodeIds: [nodeId], parentRef, order});
return `tree-node-${id}`;
}
});
},
addProperty(){
let creatureId = this.creatureId;
let fab = hideFab();
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let parent;
try {
parent = fetchDocByRef(parentRef);
} catch (e) {
console.warn(e);
}
this.$store.commit('pushDialogStack', {
component: 'add-creature-property-dialog',
elementId: 'add-creature-property-btn',
elementId: 'insert-creature-property-type-' + forcedType,
data: {
parentDoc: forcedType ? undefined : parent,
forcedType,
},
callback(result){
revealFab(fab);
if (!result){
return 'insert-creature-property-fab';
}
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
if (Array.isArray(result)){
revealFab(fab);
let nodeIds = result;
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
return `tree-node-${id}`;
return forcedType ? id : `tree-node-${id}`;
} else {
revealFab(fab);
let creatureProperty = result;
// Get order and parent
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
return forcedType ? id : `tree-node-${id}`;
}
}
});

View File

@@ -1,50 +1,127 @@
<template lang="html">
<selectable-property-dialog
:value="type"
no-library-only-props
@input="e => type = e"
>
<dialog-base
:override-back-button="back"
<dialog-base>
<template slot="toolbar">
<v-toolbar-title class="mr-4">
<template v-if="tab === 2">
New
</template>{{ typeName }}
</v-toolbar-title>
<v-spacer />
<v-slide-x-reverse-transition hide-on-leave>
<v-switch
v-if="tab === 0"
:input-value="showPropertyHelp"
append-icon="mdi-help"
hide-details
flat
@change="propertyHelpChanged"
/>
<text-field
v-if="tab === 1"
prepend-inner-icon="mdi-magnify"
regular
hide-details
:value="searchValue"
:debounce="400"
@change="searchChanged"
/>
</v-slide-x-reverse-transition>
</template>
<v-tabs
slot="toolbar-extension"
v-model="tab"
>
<template slot="toolbar">
<v-toolbar-title class="mr-4">
<template v-if="customProperty">
New
</template>{{ typeName }}
</v-toolbar-title>
<v-slide-x-transition>
<text-field
v-if="!customProperty"
prepend-inner-icon="mdi-magnify"
regular
hide-details
:value="searchValue"
:debounce="400"
@change="searchChanged"
/>
</v-slide-x-transition>
<v-scale-transition>
<v-btn
v-if="!customProperty"
fab
small
elevation="0"
class="mr-2"
color="accent"
@click="customProperty = true"
<v-tab :disabled="!!forcedType">
{{ typeName || 'Type' }}
</v-tab>
<v-tab :disabled="!type">
Library
</v-tab>
<v-tab :disabled="!type">
Create
</v-tab>
</v-tabs>
<v-tabs-items
slot="unwrapped-content"
v-model="tab"
>
<v-tab-item :disabled="!!forcedType">
<property-selector
no-library-only-props
:parent-type="parentDoc && parentDoc.type"
@select="e => type = e"
/>
</v-tab-item>
<v-tab-item :disabled="!type">
<v-expansion-panels
multiple
inset
>
<v-expansion-panel
v-for="libraryNode in libraryNodes"
:key="libraryNode._id"
:model="libraryNode"
:data-id="libraryNode._id"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-scale-transition>
</template>
<v-slide-x-transition
class="unwrapped-content"
leave-absolute
>
<div
v-if="customProperty"
key="custom-property-form"
<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="!selectedNodeIds.includes(libraryNode._id) &&
selectedNodeIds.length >= 20"
@click.stop
/>
<v-layout column>
<tree-node-view :model="libraryNode" />
<div class="text-caption">
{{ libraryNames[libraryNode.ancestors[0].id ] }}
</div>
</v-layout>
<template v-if="open">
<v-spacer />
<v-btn
icon
class="flex-grow-0"
@click.stop="openPropertyDetails(libraryNode._id)"
>
<v-icon>mdi-window-restore</v-icon>
</v-btn>
</template>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<library-node-expansion-content :model="libraryNode" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-layout
justify-center
>
<v-fade-transition mode="out-in">
<div
v-if="currentLimit < countAll"
class="layout justify-center align-stretch"
>
<v-btn
v-if="currentLimit < countAll"
key="load-more-btn"
:loading="!$subReady.searchLibraryNodes"
color="accent"
class="ma-4"
@click="loadMore"
>
Load More
</v-btn>
</div>
</v-fade-transition>
</v-layout>
</v-tab-item>
<v-tab-item :disabled="!type">
<v-card-text
v-if="!$slots['unwrapped-content']"
>
<component
:is="type"
@@ -56,113 +133,43 @@
@push="push"
@pull="pull"
/>
</div>
<div
v-else
key="library-search"
>
<v-expansion-panels
multiple
inset
>
<v-expansion-panel
v-for="libraryNode in libraryNodes"
:key="libraryNode._id"
:model="libraryNode"
:data-id="libraryNode._id"
>
<v-expansion-panel-header>
<template #default="{ open }">
<v-checkbox
v-model="selectedNodeIds"
class="my-0 py-0 mr-2"
hide-details
:value="libraryNode._id"
:disabled="!selectedNodeIds.includes(libraryNode._id) &&
selectedNodeIds.length >= 20"
@click.stop
/>
<v-layout column>
<tree-node-view :model="libraryNode" />
<div class="text-caption">
{{ libraryNames[libraryNode.ancestors[0].id ] }}
</div>
</v-layout>
<v-spacer />
<v-btn
v-if="open"
icon
class="flex-grow-0"
@click.stop="openPropertyDetails(libraryNode._id)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<library-node-expansion-content :model="libraryNode" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-layout
justify-center
>
<v-fade-transition mode="out-in">
<div
v-if="currentLimit < countAll"
class="layout justify-center align-stretch"
>
<v-btn
v-if="currentLimit < countAll"
key="load-more-btn"
:loading="!$subReady.searchLibraryNodes"
color="accent"
class="ma-4"
@click="loadMore"
>
Load More
</v-btn>
</div>
</v-fade-transition>
</v-layout>
</div>
</v-slide-x-transition>
<template slot="actions">
<v-btn
text
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
<v-spacer />
<v-btn
v-if="customProperty"
text
color="primary"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', model)"
>
create
</v-btn>
<v-btn
v-else
text
color="primary"
:disabled="!selectedNodeIds.length"
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
>
<template v-if="selectedNodeIds.length >= 15">
{{ selectedNodeIds.length }}/20
</template>
Insert
</v-btn>
</template>
</dialog-base>
</selectable-property-dialog>
</v-card-text>
</v-tab-item>
</v-tabs-items>
<template slot="actions">
<v-btn
text
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
<v-spacer />
<v-btn
v-if="tab === 2"
text
color="primary"
:disabled="!valid"
@click="$store.dispatch('popDialogStack', model)"
>
create
</v-btn>
<v-btn
v-else-if="tab === 1"
text
color="primary"
:disabled="!selectedNodeIds.length"
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
>
<template v-if="selectedNodeIds.length >= 15">
{{ selectedNodeIds.length }}/20
</template>
Insert
</v-btn>
</template>
</dialog-base>
</template>
<script lang="js">
import SelectablePropertyDialog from '/imports/ui/properties/shared/SelectablePropertyDialog.vue';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
@@ -172,14 +179,19 @@
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import Libraries from '/imports/api/library/Libraries.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
const SKIP_LIBRARY_PROP_TYPES = ['note', 'damage', 'adjustment']
export default {
components: {
...propertyFormIndex,
SelectablePropertyDialog,
PropertySelector,
DialogBase,
TreeNodeView,
LibraryNodeExpansionContent,
...propertyFormIndex,
},
mixins: [schemaFormMixin],
props: {
@@ -187,6 +199,10 @@
type: String,
default: undefined,
},
suggestedTypes: {
type: Array,
default: undefined,
},
suggestedType: {
type: String,
default: undefined,
@@ -207,13 +223,16 @@
type: this.type,
},
searchValue: undefined,
customProperty: false,
debounceTime: 0,
tab: 0,
};},
computed: {
typeName(){
return getPropertyName(this.type) || 'Property';
},
toolbarColor(){
return getThemeColor('secondary');
}
},
watch: {
type(newType){
@@ -224,14 +243,18 @@
this.changeType(this.type);
},
methods: {
back(){
if (this.customProperty){
this.customProperty = false;
} else if (this.forcedType){
this.$store.dispatch('popDialogStack');
} else {
this.type = undefined;
}
propertyHelpChanged(value){
Meteor.users.setPreference.call({
preference: 'hidePropertySelectDialogHelp',
value: !value
}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
searchChanged(val, ack){
this._subs.searchLibraryNodes.setData('searchTerm', val);
@@ -251,6 +274,11 @@
changeType(type){
this._subs.searchLibraryNodes.setData('type', type);
if (!type) return;
if (SKIP_LIBRARY_PROP_TYPES.includes(type)){
this.tab = 2;
} else {
this.tab = 1;
}
this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
@@ -265,12 +293,16 @@
_id: id,
},
});
}
},
},
meteor: {
'$subscribe':{
'searchLibraryNodes': [],
},
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
},
currentLimit(){
return this._subs.searchLibraryNodes.data('limit') || 32;
},

View File

@@ -98,6 +98,7 @@ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import { getHighestOrder } from '/imports/api/parenting/order.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -234,8 +235,7 @@ export default {
},
});
},
selectSubProperty(doc){
let _id = doc && doc._id
selectSubProperty(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `tree-node-${_id}`,
@@ -244,28 +244,37 @@ export default {
},
addProperty(){
let parentPropertyId = this.model._id;
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
component: 'add-creature-property-dialog',
elementId: 'insert-creature-property-btn',
callback(creatureProperty){
if (!creatureProperty) return;
// Get order and parent
data: {
parentDoc: this.model,
},
callback(result){
if (!result) return;
let parentRef = {
id: parentPropertyId,
collection: 'creatureProperties',
};
creatureProperty.order = getHighestOrder({
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 0.5;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
if (Array.isArray(result)){
let nodeIds = result;
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
return `tree-node-${id}`;
} else {
let creatureProperty = result;
// Get order and parent
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
}
}
});
}
},
}
};
</script>

View File

@@ -21,19 +21,25 @@
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<slot name="toolbar" />
<slot
slot="extension"
name="toolbar-extension"
/>
</v-toolbar>
</slot>
<div
v-if="$slots['unwrapped-content']"
id="base-dialog-body"
class="unwrapped-content"
@scroll.passive="onScroll"
>
<slot name="unwrapped-content" />
</div>
<v-card-text
v-if="!$slots['unwrapped-content']"
v-else
id="base-dialog-body"
v-scroll:#base-dialog-body="onScroll"
:class="{'dark-body': darkBody}"
@scroll.passive="onScroll"
>
<slot />
</v-card-text>
@@ -90,7 +96,7 @@
<style scoped>
.base-dialog-toolbar {
z-index: 1;
z-index: 2;
border-radius: 2px 2px 0 0;
}
#base-dialog-body, .unwrapped-content {

View File

@@ -0,0 +1,57 @@
<template lang="html">
<v-card
hover
style="height: 100%; overflow: hidden;"
@click="e => $emit('click', e)"
>
<v-card-title
class="subtitle pb-3"
style="text-align: center;"
>
<v-avatar tile>
<v-icon x-large>
{{ property.icon }}
</v-icon>
</v-avatar>
<span class="ml-3">
{{ property.name }}
</span>
</v-card-title>
<v-expand-transition>
<div
v-if="showPropertyHelp"
class="mx-4"
>
{{ property.helpText }}
<div style="height: 16px;" />
<div
v-if="property.examples"
class="text-caption"
>
{{ property.examples }}
<div style="height: 16px;" />
</div>
</div>
</v-expand-transition>
</v-card>
</template>
<script lang="js">
export default {
props: {
property: {
type: Object,
required: true,
}
},
meteor: {
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -2,77 +2,113 @@
<div class="card-raised-background">
<v-container
fluid
grid-list-lg
fill-height
>
<v-layout
<v-row
wrap
fill-height
dense
justify="center"
justify-sm="start"
>
<template v-for="(property, type) in PROPERTIES">
<v-flex
<template v-if="properties.suggested">
<v-col
cols="12"
>
<v-subheader>
Suggested
</v-subheader>
</v-col>
<template v-for="(property, type) in properties.suggested">
<v-col
v-if="!noLibraryOnlyProps || !property.libraryOnly"
:key="type"
md="4"
sm="6"
cols="10"
>
<property-select-card
:property="property"
@click="$emit('select', type)"
/>
</v-col>
</template>
</template>
<v-col
v-if="properties.suggested"
cols="12"
>
<v-subheader>
More
</v-subheader>
</v-col>
<template v-for="(property, type) in properties.more">
<v-col
v-if="!noLibraryOnlyProps || !property.libraryOnly"
:key="type"
md4
sm6
xs12
md="4"
sm="6"
cols="10"
>
<v-card
hover
style="height: 100%; overflow: hidden;"
<property-select-card
:property="property"
@click="$emit('select', type)"
>
<v-card-title
class="subtitle pb-3"
style="text-align: center;"
>
<v-avatar tile>
<v-icon x-large>
{{ property.icon }}
</v-icon>
</v-avatar>
<span class="ml-3">
{{ property.name }}
</span>
</v-card-title>
<v-expand-transition>
<div
v-if="showPropertyHelp"
class="mx-4"
>
{{ property.helpText }}
<div style="height: 16px;" />
<div
v-if="property.examples"
class="text-caption"
>
{{ property.examples }}
<div style="height: 16px;" />
</div>
</div>
</v-expand-transition>
</v-card>
</v-flex>
/>
</v-col>
</template>
</v-layout>
</v-row>
</v-container>
</div>
</template>
<script lang="js">
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import PropertySelectCard from '/imports/ui/properties/shared/PropertySelectCard.vue';
export default {
components: {
PropertySelectCard,
},
props: {
noLibraryOnlyProps: Boolean,
parentType: {
type: String,
default: undefined,
},
suggestedTypes: {
type: Array,
default: undefined,
},
},
data(){ return {
PROPERTIES,
};},
meteor: {
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
computed:{
properties(){
let suggested;
let more = {};
if (this.suggestedTypes){
for (const key in PROPERTIES){
let prop = PROPERTIES[key];
if (this.suggestedTypes.includes(prop.type)){
if (!suggested) suggested = {};
suggested[key] = prop;
} else {
more[key] = prop;
}
}
return {suggested, more};
} else if (this.parentType) {
for (const key in PROPERTIES){
let prop = PROPERTIES[key];
if (prop.suggestedParents.includes(this.parentType)){
if (!suggested) suggested = {};
suggested[key] = prop;
} else {
more[key] = prop;
}
}
return {suggested, more};
} else {
return {more: PROPERTIES};
}
},
},
}

View File

@@ -21,6 +21,7 @@
<property-selector
slot="unwrapped-content"
:no-library-only-props="noLibraryOnlyProps"
:parent-type="parentType"
@select="type => $emit('input', type)"
/>
</dialog-base>
@@ -49,7 +50,12 @@ export default {
noLibraryOnlyProps: Boolean,
value: {
type: String,
default: undefined,
},
parentType: {
type: String,
default: undefined,
},
},
meteor: {
showPropertyHelp(){