Added attribute insertion UI and API
This commit is contained in:
@@ -4,6 +4,7 @@ import schema from '/imports/api/schema.js';
|
|||||||
import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js";
|
import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js";
|
||||||
import { canEditCreature } from '/imports/api/creature/creaturePermission.js';
|
import { canEditCreature } from '/imports/api/creature/creaturePermission.js';
|
||||||
import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js'
|
import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js'
|
||||||
|
import { getHighestOrder } from '/imports/api/order.js';
|
||||||
import pickKeysAsOptional from '/imports/api/pickKeysAsOptional.js';
|
import pickKeysAsOptional from '/imports/api/pickKeysAsOptional.js';
|
||||||
|
|
||||||
let Attributes = new Mongo.Collection("attributes");
|
let Attributes = new Mongo.Collection("attributes");
|
||||||
@@ -11,7 +12,7 @@ let Attributes = new Mongo.Collection("attributes");
|
|||||||
/*
|
/*
|
||||||
* Attributes are numbered stats of a character
|
* Attributes are numbered stats of a character
|
||||||
*/
|
*/
|
||||||
attributeSchema = schema({
|
let attributeSchema = schema({
|
||||||
charId: {
|
charId: {
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
@@ -24,6 +25,8 @@ attributeSchema = schema({
|
|||||||
// The technical, lowercase, single-word name used in formulae
|
// The technical, lowercase, single-word name used in formulae
|
||||||
variableName: {
|
variableName: {
|
||||||
type: String,
|
type: String,
|
||||||
|
// Must contain a letter, and be made of word characters only
|
||||||
|
regEx: /^\w*[a-z]\w*$/i,
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
// Attributes need to store their order to keep the sheet consistent
|
// Attributes need to store their order to keep the sheet consistent
|
||||||
@@ -99,6 +102,35 @@ let updateAttributeSchema = pickKeysAsOptional(attributeSchema, [
|
|||||||
'color',
|
'color',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const insertAttribute = new ValidatedMethod({
|
||||||
|
|
||||||
|
name: "Attributes.methods.insert",
|
||||||
|
|
||||||
|
validate: schema({
|
||||||
|
attribute: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
|
||||||
|
run({attribute}) {
|
||||||
|
const charId = attribute.charId;
|
||||||
|
if (canEditCreature(charId, this.userId)){
|
||||||
|
attribute.order = getHighestOrder({
|
||||||
|
collection: Attributes,
|
||||||
|
charId,
|
||||||
|
}) + 1;
|
||||||
|
attribute.parent = {
|
||||||
|
id: charId,
|
||||||
|
collection: 'Creatures',
|
||||||
|
};
|
||||||
|
let attId = Attributes.insert(attribute);
|
||||||
|
recomputeCreatureById(charId);
|
||||||
|
return attId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const updateAttribute = new ValidatedMethod({
|
const updateAttribute = new ValidatedMethod({
|
||||||
|
|
||||||
name: "Attributes.methods.update",
|
name: "Attributes.methods.update",
|
||||||
@@ -191,4 +223,4 @@ const adjustAttribute = new ValidatedMethod({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default Attributes;
|
export default Attributes;
|
||||||
export { updateAttribute, adjustAttribute };
|
export { insertAttribute, updateAttribute, adjustAttribute };
|
||||||
|
|||||||
71
app/imports/api/order.js
Normal file
71
app/imports/api/order.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const getHighestOrder = function({collection, charId}){
|
||||||
|
const highestOrderedDoc = collection.findOne({
|
||||||
|
charId
|
||||||
|
}, {
|
||||||
|
fields: {order: 1},
|
||||||
|
sort: {order: -1},
|
||||||
|
});
|
||||||
|
return (highestOrderedDoc && highestOrderedDoc.order) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveDocToOrder = function({collection, doc, order}){
|
||||||
|
const currentOrder = doc.order;
|
||||||
|
if (currentOrder === order){
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Move the document to its new order
|
||||||
|
collection.update(doc._id, {$set: {order}});
|
||||||
|
let inBetweenSelector, increment;
|
||||||
|
if (order > currentOrder){
|
||||||
|
// Move in-between docs backward
|
||||||
|
inBetweenSelector = [
|
||||||
|
{$gt: currentOrder},
|
||||||
|
{$lte: order},
|
||||||
|
];
|
||||||
|
increment = -1;
|
||||||
|
} else if (order < currentOrder){
|
||||||
|
// Move in-between docs forward
|
||||||
|
inBetweenSelector = [
|
||||||
|
{$lt: currentOrder},
|
||||||
|
{$gte: order},
|
||||||
|
];
|
||||||
|
increment = 1;
|
||||||
|
}
|
||||||
|
collection.update({
|
||||||
|
order: {$and: inBetweenSelector},
|
||||||
|
charId: doc.charId,
|
||||||
|
}, {
|
||||||
|
$inc: {order: increment},
|
||||||
|
}, {
|
||||||
|
multi: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderDocs = function({collection, charId}){
|
||||||
|
let bulkWrite = [];
|
||||||
|
collection.find({
|
||||||
|
charId
|
||||||
|
}, {
|
||||||
|
fields: {order: 1},
|
||||||
|
sort: {order: 1}
|
||||||
|
}).forEach((doc, index) => {
|
||||||
|
if (doc.order !== index){
|
||||||
|
bulkwrite.push({
|
||||||
|
updateOne : {
|
||||||
|
filter: {_id: doc._id},
|
||||||
|
update: {$set: {order: index}},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (Meteor.isServer){
|
||||||
|
collection.rawCollection().bulkWrite(bulkWrite);
|
||||||
|
} else {
|
||||||
|
bulkWrite.forEach(op => {
|
||||||
|
collection.update(op.filter, op.update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getHighestOrder, moveDocToOrder, reorderDocs };
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="saving-throws">
|
<div class="saving-throws" v-show="true">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-subheader>Saving Throws</v-subheader>
|
<v-subheader>Saving Throws</v-subheader>
|
||||||
@@ -89,6 +89,16 @@
|
|||||||
|
|
||||||
</column-layout>
|
</column-layout>
|
||||||
|
|
||||||
|
<v-btn absolute fab bottom right
|
||||||
|
color="primary"
|
||||||
|
@click="insertAttribute"
|
||||||
|
data-id="insert-attribute-fab"
|
||||||
|
>
|
||||||
|
<v-icon>
|
||||||
|
add
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -97,12 +107,12 @@
|
|||||||
import Skills from '/imports/api/creature/properties/Skills.js';
|
import Skills from '/imports/api/creature/properties/Skills.js';
|
||||||
import AttributeCard from '/imports/ui/components/AttributeCard.vue';
|
import AttributeCard from '/imports/ui/components/AttributeCard.vue';
|
||||||
import AbilityListTile from '/imports/ui/components/AbilityListTile.vue';
|
import AbilityListTile from '/imports/ui/components/AbilityListTile.vue';
|
||||||
import ColumnLayout from "/imports/ui/components/ColumnLayout.vue";
|
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||||
import HealthBarCardContainer from '/imports/ui/components/HealthBarCardContainer.vue';
|
import HealthBarCardContainer from '/imports/ui/components/HealthBarCardContainer.vue';
|
||||||
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.vue';
|
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.vue';
|
||||||
import SkillListTile from '/imports/ui/components/SkillListTile.vue';
|
import SkillListTile from '/imports/ui/components/SkillListTile.vue';
|
||||||
|
|
||||||
import { adjustAttribute } from '/imports/api/creature/properties/Attributes.js';
|
import { adjustAttribute, insertAttribute } from '/imports/api/creature/properties/Attributes.js';
|
||||||
|
|
||||||
const getAttributeOfType = function(charId, type){
|
const getAttributeOfType = function(charId, type){
|
||||||
return Attributes.find({charId, type}, {sort: {order: 1}});
|
return Attributes.find({charId, type}, {sort: {order: 1}});
|
||||||
@@ -142,7 +152,7 @@
|
|||||||
let dice = diceMatch && +diceMatch[1];
|
let dice = diceMatch && +diceMatch[1];
|
||||||
let con = Attributes.findOne({
|
let con = Attributes.findOne({
|
||||||
charId: this.charId,
|
charId: this.charId,
|
||||||
variableName: "constitution"
|
variableName: 'constitution'
|
||||||
});
|
});
|
||||||
let conMod = con && con.mod;
|
let conMod = con && con.mod;
|
||||||
return {
|
return {
|
||||||
@@ -174,15 +184,15 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickAttribute({_id}){
|
clickAttribute({_id}){
|
||||||
this.$store.commit("pushDialogStack", {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: "attribute-dialog-container",
|
component: 'attribute-dialog-container',
|
||||||
elementId: _id,
|
elementId: _id,
|
||||||
data: {_id},
|
data: {_id},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clickSkill({_id}){
|
clickSkill({_id}){
|
||||||
this.$store.commit("pushDialogStack", {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: "skill-dialog-container",
|
component: 'skill-dialog-container',
|
||||||
elementId: _id,
|
elementId: _id,
|
||||||
data: {_id},
|
data: {_id},
|
||||||
});
|
});
|
||||||
@@ -192,6 +202,19 @@
|
|||||||
adjustAttribute.call({_id, increment: value});
|
adjustAttribute.call({_id, increment: value});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
insertAttribute(){
|
||||||
|
const charId = this.charId;
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'attribute-creation-dialog',
|
||||||
|
elementId: 'insert-attribute-fab',
|
||||||
|
callback(attribute){
|
||||||
|
if (!attribute) return;
|
||||||
|
attribute.charId = charId;
|
||||||
|
let attId = insertAttribute.call({attribute});
|
||||||
|
return attId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
86
app/imports/ui/components/AttributeCreationDialog.vue
Normal file
86
app/imports/ui/components/AttributeCreationDialog.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<dialog-base>
|
||||||
|
<div slot="toolbar">
|
||||||
|
New Attribute
|
||||||
|
</div>
|
||||||
|
<attribute-edit
|
||||||
|
:attribute="attribute"
|
||||||
|
:errors="errors"
|
||||||
|
@change="change"
|
||||||
|
:debounce-time="0"
|
||||||
|
/>
|
||||||
|
<div slot="actions">
|
||||||
|
<v-spacer/>
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
:disabled="!valid"
|
||||||
|
@click="$store.dispatch('popDialogStack', attribute)"
|
||||||
|
>
|
||||||
|
Insert Attribute
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</dialog-base>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AttributeEdit from '/imports/ui/components/AttributeEdit.vue';
|
||||||
|
import Attributes from '/imports/api/creature/properties/Attributes.js';
|
||||||
|
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||||
|
import { Tracker } from 'meteor/tracker';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AttributeEdit,
|
||||||
|
DialogBase,
|
||||||
|
},
|
||||||
|
data(){ return {
|
||||||
|
attribute: {
|
||||||
|
name: 'New Attribute',
|
||||||
|
variableName: 'newAttribute',
|
||||||
|
type: 'stat',
|
||||||
|
baseValue: null,
|
||||||
|
adjustment: null,
|
||||||
|
decimal: null,
|
||||||
|
reset: null,
|
||||||
|
color: '#9E9E9E',
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
}},
|
||||||
|
methods: {
|
||||||
|
change(update, ack){
|
||||||
|
for (key in update){
|
||||||
|
this.attribute[key] = update[key];
|
||||||
|
if (key === 'name' && update[key]){
|
||||||
|
const name = update[key];
|
||||||
|
this.attribute.variableName = name.toLowerCase().replace(
|
||||||
|
/\W+(\w?)/g, (match, p1) => p1.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ack) ack();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created(){
|
||||||
|
this.validationContext = Attributes.simpleSchema().newContext();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
errors(){
|
||||||
|
this.valid = true;
|
||||||
|
let cleanAtt = this.validationContext.clean(this.attribute)
|
||||||
|
this.validationContext.validate(cleanAtt, {keys: [
|
||||||
|
'name', 'variableName', 'type', 'baseValue', 'adjustment', 'decimal',
|
||||||
|
'reset', 'color'
|
||||||
|
]});
|
||||||
|
let errors = {};
|
||||||
|
this.validationContext.validationErrors().forEach(error => {
|
||||||
|
if (this.valid) this.valid = false;
|
||||||
|
errors[error.name] = Attributes.simpleSchema().messageForError(error);
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
</style>
|
||||||
@@ -38,9 +38,14 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedEffect(e){
|
clickedEffect(e){
|
||||||
console.log(e);
|
console.log({TODO: e});
|
||||||
},
|
},
|
||||||
change(update, ack){
|
change(update, ack){
|
||||||
|
if(update.name){
|
||||||
|
update.variableName = update.name.toLowerCase().replace(
|
||||||
|
/\W+(\w?)/g, (match, p1) => p1.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
updateAttribute.call({_id: this._id, update}, error => {
|
updateAttribute.call({_id: this._id, update}, error => {
|
||||||
ack(error);
|
ack(error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
label="Name"
|
label="Name"
|
||||||
:value="attribute.name"
|
:value="attribute.name"
|
||||||
@change="(name, ack) => $emit('change', {name}, ack)"
|
@change="(name, ack) => $emit('change', {name}, ack)"
|
||||||
|
:error-messages="errors.name"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Variable name"
|
label="Variable name"
|
||||||
:value="attribute.variableName"
|
:value="attribute.variableName"
|
||||||
@change="(variableName, ack) => $emit('change', {variableName}, ack)"
|
@change="(variableName, ack) => $emit('change', {variableName}, ack)"
|
||||||
hint="Use this name in formulae to reference this attribute"
|
hint="Use this name in formulae to reference this attribute"
|
||||||
|
:error-messages="errors.variableName"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Base Value"
|
label="Base Value"
|
||||||
@@ -17,23 +21,30 @@
|
|||||||
:value="attribute.baseValue"
|
:value="attribute.baseValue"
|
||||||
@change="(baseValue, ack) => $emit('change', {baseValue: +baseValue}, ack)"
|
@change="(baseValue, ack) => $emit('change', {baseValue: +baseValue}, ack)"
|
||||||
hint="This is the value of the attribute before effects are applied"
|
hint="This is the value of the attribute before effects are applied"
|
||||||
|
:error-messages="errors.baseValue"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Damage"
|
label="Damage"
|
||||||
type="number"
|
type="number"
|
||||||
:value="-attribute.adjustment"
|
:value="-attribute.adjustment"
|
||||||
@change="(damage, ack) => $emit('change', {adjustment: -damage || null}, ack)"
|
@change="(damage, ack) => $emit('change', {adjustment: -damage || null}, ack)"
|
||||||
|
:error-messages="errors.adjustment"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<smart-select
|
<smart-select
|
||||||
label="Type"
|
label="Type"
|
||||||
:items="attributeTypes"
|
:items="attributeTypes"
|
||||||
:value="attribute.type"
|
:value="attribute.type"
|
||||||
|
:error-messages="errors.type"
|
||||||
:menu-props="{auto: true, lazy: true}"
|
:menu-props="{auto: true, lazy: true}"
|
||||||
@change="(type, ack) => $emit('change', {type}, ack)"
|
@change="(type, ack) => $emit('change', {type}, ack)"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Allow decimal values"
|
label="Allow decimal values"
|
||||||
:value="attribute.decimal"
|
:value="attribute.decimal"
|
||||||
|
:error-messages="errors.decimal"
|
||||||
@change="e => $emit('change', {decimal: !!e})"
|
@change="e => $emit('change', {decimal: !!e})"
|
||||||
/>
|
/>
|
||||||
<smart-select
|
<smart-select
|
||||||
@@ -42,15 +53,19 @@
|
|||||||
clearable
|
clearable
|
||||||
:items="resetOptions"
|
:items="resetOptions"
|
||||||
:value="attribute.reset"
|
:value="attribute.reset"
|
||||||
|
:error-messages="errors.reset"
|
||||||
:menu-props="{auto: true, lazy: true}"
|
:menu-props="{auto: true, lazy: true}"
|
||||||
@change="(reset, ack) => $emit('change', {reset}, ack)"
|
@change="(reset, ack) => $emit('change', {reset}, ack)"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Reset Multiplier"
|
label="Reset Multiplier"
|
||||||
type="number"
|
type="number"
|
||||||
:value="attribute.resetMultiplier"
|
:value="attribute.resetMultiplier"
|
||||||
|
:error-messages="errors.resetMultiplier"
|
||||||
@change="(resetMultiplier, ack) => $emit('change', {resetMultiplier: +resetMultiplier}, ack)"
|
@change="(resetMultiplier, ack) => $emit('change', {resetMultiplier: +resetMultiplier}, ack)"
|
||||||
hint="Some attributes, like hit dice, only reset by half their total on a long rest"
|
hint="Some attributes, like hit dice, only reset by half their total on a long rest"
|
||||||
|
:debounce-time="debounceTime"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -60,8 +75,13 @@
|
|||||||
props: {
|
props: {
|
||||||
attribute: {
|
attribute: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: {},
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
errors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
debounceTime: Number,
|
||||||
},
|
},
|
||||||
data(){ return{
|
data(){ return{
|
||||||
attributeTypes: [
|
attributeTypes: [
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import AttributeDialog from '/imports/ui/components/AttributeDialog.vue';
|
import AttributeDialog from '/imports/ui/components/AttributeDialog.vue';
|
||||||
import AttributeDialogContainer from '/imports/ui/components/AttributeDialogContainer.vue';
|
import AttributeDialogContainer from '/imports/ui/components/AttributeDialogContainer.vue';
|
||||||
|
import AttributeCreationDialog from '/imports/ui/components/AttributeCreationDialog.vue';
|
||||||
import SkillDialogContainer from '/imports/ui/components/SkillDialogContainer.vue';
|
import SkillDialogContainer from '/imports/ui/components/SkillDialogContainer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
AttributeDialog,
|
AttributeDialog,
|
||||||
AttributeDialogContainer,
|
AttributeDialogContainer,
|
||||||
|
AttributeCreationDialog,
|
||||||
SkillDialogContainer,
|
SkillDialogContainer,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user