diff --git a/app/imports/api/creature/properties/Attributes.js b/app/imports/api/creature/properties/Attributes.js index 6a16dbf8..6daa70c1 100644 --- a/app/imports/api/creature/properties/Attributes.js +++ b/app/imports/api/creature/properties/Attributes.js @@ -4,6 +4,7 @@ import schema from '/imports/api/schema.js'; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; import { canEditCreature } from '/imports/api/creature/creaturePermission.js'; import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js' +import { getHighestOrder } from '/imports/api/order.js'; import pickKeysAsOptional from '/imports/api/pickKeysAsOptional.js'; let Attributes = new Mongo.Collection("attributes"); @@ -11,7 +12,7 @@ let Attributes = new Mongo.Collection("attributes"); /* * Attributes are numbered stats of a character */ -attributeSchema = schema({ +let attributeSchema = schema({ charId: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -24,6 +25,8 @@ attributeSchema = schema({ // The technical, lowercase, single-word name used in formulae variableName: { type: String, + // Must contain a letter, and be made of word characters only + regEx: /^\w*[a-z]\w*$/i, index: 1, }, // Attributes need to store their order to keep the sheet consistent @@ -99,6 +102,35 @@ let updateAttributeSchema = pickKeysAsOptional(attributeSchema, [ '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({ name: "Attributes.methods.update", @@ -191,4 +223,4 @@ const adjustAttribute = new ValidatedMethod({ }); export default Attributes; -export { updateAttribute, adjustAttribute }; +export { insertAttribute, updateAttribute, adjustAttribute }; diff --git a/app/imports/api/order.js b/app/imports/api/order.js new file mode 100644 index 00000000..6b566bd5 --- /dev/null +++ b/app/imports/api/order.js @@ -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 }; diff --git a/app/imports/ui/character/StatsTab.vue b/app/imports/ui/character/StatsTab.vue index 4db53b02..0c58779c 100644 --- a/app/imports/ui/character/StatsTab.vue +++ b/app/imports/ui/character/StatsTab.vue @@ -57,7 +57,7 @@ -
+
Saving Throws @@ -89,6 +89,16 @@ + + + add + + +
@@ -97,12 +107,12 @@ import Skills from '/imports/api/creature/properties/Skills.js'; import AttributeCard from '/imports/ui/components/AttributeCard.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 HitDiceListTile from '/imports/ui/components/HitDiceListTile.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){ return Attributes.find({charId, type}, {sort: {order: 1}}); @@ -142,7 +152,7 @@ let dice = diceMatch && +diceMatch[1]; let con = Attributes.findOne({ charId: this.charId, - variableName: "constitution" + variableName: 'constitution' }); let conMod = con && con.mod; return { @@ -174,15 +184,15 @@ }, methods: { clickAttribute({_id}){ - this.$store.commit("pushDialogStack", { - component: "attribute-dialog-container", + this.$store.commit('pushDialogStack', { + component: 'attribute-dialog-container', elementId: _id, data: {_id}, }); }, clickSkill({_id}){ - this.$store.commit("pushDialogStack", { - component: "skill-dialog-container", + this.$store.commit('pushDialogStack', { + component: 'skill-dialog-container', elementId: _id, data: {_id}, }); @@ -192,6 +202,19 @@ 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 + } + }); + }, }, }; diff --git a/app/imports/ui/components/AttributeCreationDialog.vue b/app/imports/ui/components/AttributeCreationDialog.vue new file mode 100644 index 00000000..26d5a360 --- /dev/null +++ b/app/imports/ui/components/AttributeCreationDialog.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/app/imports/ui/components/AttributeDialogContainer.vue b/app/imports/ui/components/AttributeDialogContainer.vue index eefad1b1..a2f42553 100644 --- a/app/imports/ui/components/AttributeDialogContainer.vue +++ b/app/imports/ui/components/AttributeDialogContainer.vue @@ -38,9 +38,14 @@ }, methods: { clickedEffect(e){ - console.log(e); + console.log({TODO: e}); }, 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 => { ack(error); }); diff --git a/app/imports/ui/components/AttributeEdit.vue b/app/imports/ui/components/AttributeEdit.vue index a862898d..09276ff0 100644 --- a/app/imports/ui/components/AttributeEdit.vue +++ b/app/imports/ui/components/AttributeEdit.vue @@ -4,12 +4,16 @@ label="Name" :value="attribute.name" @change="(name, ack) => $emit('change', {name}, ack)" + :error-messages="errors.name" + :debounce-time="debounceTime" />
@@ -60,8 +75,13 @@ props: { attribute: { type: Object, - default: {}, + default: () => ({}), }, + errors: { + type: Object, + default: () => ({}), + }, + debounceTime: Number, }, data(){ return{ attributeTypes: [ diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js index 259eb1c0..38f825fc 100644 --- a/app/imports/ui/dialogStack/DialogComponentIndex.js +++ b/app/imports/ui/dialogStack/DialogComponentIndex.js @@ -1,9 +1,11 @@ import AttributeDialog from '/imports/ui/components/AttributeDialog.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'; export default { AttributeDialog, AttributeDialogContainer, + AttributeCreationDialog, SkillDialogContainer, };