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 @@
+
+
+
+ New Attribute
+
+
+
+
+
+ Insert Attribute
+
+
+
+
+
+
+
+
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"
/>
$emit('change', {variableName}, ack)"
hint="Use this name in formulae to reference this attribute"
+ :error-messages="errors.variableName"
+ :debounce-time="debounceTime"
/>
$emit('change', {baseValue: +baseValue}, ack)"
hint="This is the value of the attribute before effects are applied"
+ :error-messages="errors.baseValue"
+ :debounce-time="debounceTime"
/>
$emit('change', {adjustment: -damage || null}, ack)"
+ :error-messages="errors.adjustment"
+ :debounce-time="debounceTime"
/>
$emit('change', {type}, ack)"
+ :debounce-time="debounceTime"
/>
$emit('change', {decimal: !!e})"
/>
$emit('change', {reset}, ack)"
+ :debounce-time="debounceTime"
/>
$emit('change', {resetMultiplier: +resetMultiplier}, ack)"
hint="Some attributes, like hit dice, only reset by half their total on a long rest"
+ :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,
};