Compare commits

..

46 Commits

Author SHA1 Message Date
Stefan Zermatten
f043c41e12 Fixed fab being confused by hiding the spells tab 2021-08-14 10:44:17 +02:00
Stefan Zermatten
0277de76c4 Removed stray console log 2021-08-12 19:56:01 +02:00
Stefan Zermatten
ffc3211ff9 Fixed some issues with slot filler searching 2021-08-12 18:28:53 +02:00
Stefan Zermatten
8162c76185 Added basic ownership transfer for shared documents 2021-08-10 19:01:31 +02:00
Stefan Zermatten
e21586e9ce Added reasonable storage limits to most string and array schemas 2021-08-10 18:12:55 +02:00
Stefan Zermatten
4c2155d8ff Buffs applied property can now be set in both library and character 2021-08-10 17:02:27 +02:00
Stefan Zermatten
44cc46ed22 Added the ability to add buffs as already applied straight from a library 2021-08-10 16:47:05 +02:00
Stefan Zermatten
d2b5d5f01d Fixed swiping between tabs when spells tab is hidden 2021-08-10 15:20:04 +02:00
Stefan Zermatten
4492c47b00 Fixed item tree node view only using quantity display if quantity is a number 2021-08-10 15:03:30 +02:00
Stefan Zermatten
6600cea9fa Fixed references display in tree views 2021-08-10 15:02:24 +02:00
Stefan Zermatten
741a9b080a Finished implementing new slot fill dialog, including ability to test slots 2021-08-10 14:35:27 +02:00
Stefan Zermatten
b041db22e4 Merge branch 'version-2' into version-2-dev 2021-08-10 13:31:32 +02:00
Stefan Zermatten
a465e2ce87 Continued implementing new slot dialog 2021-08-10 13:29:11 +02:00
Stefan Zermatten
8ecefb03ad Started re-implementing slot fill dialog with different design pattern 2021-08-10 10:32:56 +02:00
Stefan Zermatten
9f62a78eb0 Began work on implementing string and array storage limits 2021-08-09 22:34:04 +02:00
Stefan Zermatten
16e2b1249f Increased slot filler publication load quantity to 50 2021-08-09 20:54:29 +02:00
Stefan Zermatten
a35f9221a2 Fixed granting XP being accidentally locked behind patreon paid benefits 2021-08-09 18:06:04 +02:00
Stefan Zermatten
6364549d50 Added pretty url's from v1 2021-08-09 18:00:55 +02:00
Stefan Zermatten
d999fb46a7 Fixed a warning about source map failure when trying to define a scss variable 2021-08-09 17:57:35 +02:00
Stefan Zermatten
ec01a2adb5 Merge pull request #274 from GuillaumeDerval/version-2
Fix items/buffs/... refresh after casting a spell
2021-08-09 17:11:41 +02:00
Guillaume Derval
1f64558100 Fixed items/buffs/... refresh after casting a spell 2021-08-07 23:20:26 +02:00
Stefan Zermatten
19a2798bf7 Fixed tree item search highlighting in dark mode 2021-08-02 01:13:15 +02:00
Stefan Zermatten
a5f2c2e0d2 Removed duplicate property button on tree tab 2021-08-02 00:54:59 +02:00
Stefan Zermatten
ee174210fd Added search to library tree views 2021-08-02 00:29:56 +02:00
Stefan Zermatten
1e38295164 All properties added to the sheet now use the type/library/create UX 2021-08-01 23:28:04 +02:00
Stefan Zermatten
758cb2f8bc Fixed search icons 2021-07-31 21:53:18 +02:00
Stefan Zermatten
36bb3c3181 New UX for inserting properties from libraries including text search and multi-add 2021-07-31 21:49:15 +02:00
Stefan Zermatten
02434de34c Drastically improved tree tab search UX for locating parts of the sheet 2021-07-31 15:19:54 +02:00
Stefan Zermatten
0dc0bea53e fixes #262, emails from DiceCloud should now be from no-reply@dicecloud.com 2021-07-27 16:59:09 +02:00
Stefan Zermatten
c392119430 Added suggested parents to PROPERTIES for use later with user guidance 2021-07-27 16:36:19 +02:00
Stefan Zermatten
4e2e0ca364 Improved display of referenced properties 2021-07-27 16:21:55 +02:00
Stefan Zermatten
4a8b72f163 Made sure the unsubscribe button always shows on libraries you are subscribed to 2021-07-27 15:51:09 +02:00
Stefan Zermatten
d916dc2b78 Removed add library node buttons from libraries the user deosn't have edit permissions for 2021-07-27 15:44:26 +02:00
Stefan Zermatten
56860ba96d Fixed proficiency Tree node view not showing the name if it is defined 2021-07-27 15:40:16 +02:00
Stefan Zermatten
b607755f9f Fixed attribute base value calculation errors not being cleared if no new errors were made 2021-07-27 15:37:57 +02:00
Stefan Zermatten
86d8fa4325 Fixed items not animating correctly on insert 2021-07-27 15:33:41 +02:00
Stefan Zermatten
2b08249e5e removed lazy from v-menu which caused a console warning 2021-07-27 15:29:05 +02:00
Stefan Zermatten
3133e664d5 Reduced stats computation precision to round off small decimal floating point errors 2021-07-27 15:28:44 +02:00
Stefan Zermatten
48f32e0a8d Removed floating point small decimal oddities in parts of inventory tab 2021-07-27 15:21:35 +02:00
Stefan Zermatten
c72785c9e7 Added components to spell viewer 2021-07-27 15:00:10 +02:00
Stefan Zermatten
421ff2aa7d Fixed DISABLE_PATREON not working, it's now a Meteor setting instead of an ENV variable 2021-07-27 14:31:54 +02:00
Stefan Zermatten
9a9e6491b9 Improved usability with better hints in property forms and property type selection 2021-07-26 18:19:29 +02:00
Stefan Zermatten
332258705c Added service worker 2021-07-21 13:44:55 +02:00
Stefan Zermatten
73ef109d4d Added calculation errors that were missing 2021-07-20 10:37:32 +02:00
Stefan Zermatten
fc240a34c4 Changed tiers to their open beta configuration 2021-07-17 12:50:38 +02:00
Stefan Zermatten
8ac4028f38 Removed limit from guest tiers for closed beta 2021-07-14 00:59:32 +02:00
150 changed files with 3532 additions and 1791 deletions

View File

@@ -78,8 +78,12 @@ NPM_CONFIG_PRODUCTION=true
PROJECT_DIR=app PROJECT_DIR=app
ROOT_URL=https://<url of your DiceCloud instance> ROOT_URL=https://<url of your DiceCloud instance>
DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456"> DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456">
DISABLE_PATREON=<"true" if you want to prevent features being locked behind Patreon tiers>
``` ```
To disable Patreon features and unlock all paid restrictions for all users of your deployment, replace
`"patreon": { "clientId": ... }"` with `"disablePatreon": true` in the public key of the METEOR_SETTINGS environment variable.
Alternatively run `meteor run --settings exampleMeteorSettings.json` to start the app with the example settings that disable Patreon by default.
Now, visiting [](http://localhost:3000/) should show you an empty instance of Now, visiting [](http://localhost:3000/) should show you an empty instance of
DiceCloud running. DiceCloud running.

View File

@@ -1,3 +1,4 @@
import '/imports/ui/vueSetup.js'; import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js'; import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js'; import '/imports/client/config.js';
import '/imports/client/serviceWorker.js';

View File

@@ -0,0 +1,6 @@
{
"public": {
"environment": "production",
"disablePatreon": true
}
}

View File

View File

@@ -9,6 +9,8 @@ import { recomputeCreatureByDoc } from '/imports/api/creature/computation/method
import { doActionWork } from '/imports/api/creature/actions/doAction.js'; import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js'; import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties';
const castSpellWithSlot = new ValidatedMethod({ const castSpellWithSlot = new ValidatedMethod({
name: 'creatureProperties.castSpellWithSlot', name: 'creatureProperties.castSpellWithSlot',
@@ -64,16 +66,24 @@ const castSpellWithSlot = new ValidatedMethod({
} }
let actionContext = getAncestorContext(spell); let actionContext = getAncestorContext(spell);
doActionWork({ doActionWork({
action: spell, action: spell,
actionContext: {slotLevel, ...actionContext}, actionContext: {slotLevel, ...actionContext},
creature, creature,
targets: target ? [target] : [], targets: target ? [target] : [],
method: this, method: this,
}); });
// Note this only recomputes the top-level creature, not the nearest one
recomputeCreatureByDoc(creature); // Note these lines only recompute the top-level creature, not the nearest one
// The acting creature might have a new item
recomputeInventory(creature._id);
// The spell might add properties which need to be activated
recomputeInactiveProperties(creature._id);
recomputeCreatureByDoc(creature);
if (target){ if (target){
recomputeInventory(target._id);
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target); recomputeCreatureByDoc(target);
} }
}, },

View File

@@ -7,7 +7,7 @@ import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js'; import nodesToTree from '/imports/api/parenting/nodesToTree.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js'; import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';

View File

@@ -1,6 +1,7 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js'; import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
import { union } from 'lodash'; import { union } from 'lodash';
export default function combineStat(stat, aggregator, memo){ export default function combineStat(stat, aggregator, memo){
@@ -34,6 +35,8 @@ function getAggregatorResult(stat, aggregator){
} }
if (!stat.decimal && Number.isFinite(result)){ if (!stat.decimal && Number.isFinite(result)){
result = Math.floor(result); result = Math.floor(result);
} else if (Number.isFinite(result)){
result = stripFloatingPointOddities(result);
} }
return result; return result;
} }

View File

@@ -93,6 +93,7 @@ export default function computeStat(stat, memo){
// Compute each active stat's baseValue calculation and apply it // Compute each active stat's baseValue calculation and apply it
if (!statInstance.inactive) { if (!statInstance.inactive) {
delete statInstance.baseValueErrors;
let { let {
result, result,
context, context,

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureFolders = new Mongo.Collection('creatureFolders'); let CreatureFolders = new Mongo.Collection('creatureFolders');
@@ -7,6 +8,7 @@ let creatureFolderSchema = new SimpleSchema({
type: String, type: String,
trim: false, trim: false,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
creatures: { creatures: {
type: Array, type: Array,

View File

@@ -5,6 +5,7 @@ import ChildSchema from '/imports/api/parenting/ChildSchema.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js'; import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties'); let CreatureProperties = new Mongo.Collection('creatureProperties');
@@ -16,9 +17,11 @@ let CreaturePropertySchema = new SimpleSchema({
tags: { tags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'tags.$': { 'tags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
@@ -27,6 +30,13 @@ let CreaturePropertySchema = new SimpleSchema({
icon: { icon: {
type: storedIconsSchema, type: storedIconsSchema,
optional: true, optional: true,
max: STORAGE_LIMITS.icon,
},
// Reference to the library node that this property was copied from
libraryNodeId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
}, },
// Denormalised flag if this property is inactive on the sheet for any reason // Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property // Including being disabled, or a decendent of a disabled property

View File

@@ -0,0 +1,44 @@
export default function getSlotFillFilter({slot, libraryIds}){
let filter = {
removed: {$ne: true},
$and: []
};
if (libraryIds){
filter['ancestors.id'] = {$in: libraryIds};
}
if (slot.slotType){
filter.$and.push({
$or: [{
type: slot.slotType
},{
type: 'slotFiller',
slotFillerType: slot.slotType,
}]
});
}
let tagsOr = [];
let tagsNor = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
}
if (slot.extraTags && slot.extraTags.length){
slot.extraTags.forEach(extra => {
if (!extra.tags || !extra.tags.length) return;
if (extra.operation === 'OR'){
tagsOr.push({tags: {$all: extra.tags}});
} else if (extra.operation === 'NOT'){
tagsNor.push({tags: {$all: extra.tags}});
}
});
}
if (tagsOr.length){
filter.$and.push({$or: tagsOr});
}
if (tagsNor.length){
filter.$and.push({$nor: tagsNor});
}
if (!filter.$and.length){
delete filter.$and;
}
return filter;
}

View File

@@ -21,7 +21,11 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({ const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode', name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({ validate: new SimpleSchema({
nodeId: { nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
@@ -38,7 +42,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({nodeId, parentRef, order}) { run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties // get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef}); let {parentDoc, ancestors} = getAncestry({parentRef});
@@ -53,54 +57,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
} }
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
// Fetch the library node and its decendents, provided they have not been // {libraryId: hasViewPermission}
// removed //let libraryPermissionMemoir = {};
// TODO: Check permission to read the library this node is in let node;
let node = LibraryNodes.findOne({ nodeIds.forEach(nodeId => {
_id: nodeId, // TODO: Check library view permission for each node before starting
removed: {$ne: true}, node = insertPropertyFromNode(nodeId, ancestors, order);
}); });
if (!node) throw `Node not found for nodeId: ${nodeId}`;
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
}).fetch();
// Convert all references into actual nodes // get one of the root inserted docs
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
// get the root inserted doc
let rootId = node._id; let rootId = node._id;
// Tree structure changed by inserts, reorder the tree // Tree structure changed by inserts, reorder the tree
@@ -110,7 +75,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}); });
// The library properties need to denormalise which of them are inactive // The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootId); recomputeInactiveProperties(rootCreature._id);
// Some of the library properties may be items or containers // Some of the library properties may be items or containers
recomputeInventory(rootCreature._id); recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute // Inserting a creature property invalidates dependencies: full recompute
@@ -120,6 +85,66 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}, },
}); });
function insertPropertyFromNode(nodeId, ancestors, order){
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
});
if (!node) throw `Node not found for nodeId: ${nodeId}`;
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
}).fetch();
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// set libraryNodeIds
storeLibraryNodeReferences(nodes, nodeId);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
node.libraryNodeId = node._id;
});
}
// Covert node references into actual nodes // Covert node references into actual nodes
// TODO: check permissions for each library a reference node references // TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
@@ -194,7 +219,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
// TODO: Force the referencedNode to take the old id of the reference // TODO: Force the referencedNode to take the old id of the reference
// such that the reference's children can be kept // such that the reference's children can be kept
// Give the new referenced sub-tree new ids // Give the new referenced sub-tree new ids
renewDocIds({ renewDocIds({
docArray: addedNodes, docArray: addedNodes,

View File

@@ -4,6 +4,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({ const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push', name: 'creatureProperties.push',
@@ -19,9 +20,26 @@ const pushToProperty = new ValidatedMethod({
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
let joinedPath = path.join('.');
// Respect maxCount
let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){
let array = get(property, path);
let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){
throw new Meteor.Error(
'Array is full',
`Cannot have more than ${maxCount} values`
);
}
}
// Do work // Do work
CreatureProperties.update(_id, { CreatureProperties.update(_id, {
$push: {[path.join('.')]: value}, $push: {[joinedPath]: value},
}, { }, {
selector: {type: property.type}, selector: {type: property.type},
}); });

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js' import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js'; import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
//set up the collection for creatures //set up the collection for creatures
let Creatures = new Mongo.Collection('creatures'); let Creatures = new Mongo.Collection('creatures');
@@ -47,7 +48,7 @@ let CreatureSettingsSchema = new SimpleSchema({
discordWebhook: { discordWebhook: {
type: String, type: String,
optional: true, optional: true,
max: 200, max: STORAGE_LIMITS.url,
}, },
}); });
@@ -57,22 +58,27 @@ let CreatureSchema = new SimpleSchema({
type: String, type: String,
defaultValue: '', defaultValue: '',
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
alignment: { alignment: {
type: String, type: String,
optional: true optional: true,
max: STORAGE_LIMITS.name,
}, },
gender: { gender: {
type: String, type: String,
optional: true optional: true,
max: STORAGE_LIMITS.name,
}, },
picture: { picture: {
type: String, type: String,
optional: true optional: true,
max: STORAGE_LIMITS.url,
}, },
avatarPicture: { avatarPicture: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.url,
}, },
// Mechanics // Mechanics
deathSave: { deathSave: {

View File

@@ -0,0 +1,5 @@
import getSlug from 'speakingurl';
export default function getCreatureUrlName({name}){
return getSlug(name, {maintainCase: true}) || '-';
}

View File

@@ -1,5 +1,4 @@
import '/imports/api/creature/creatures/methods/insertCreature.js'; import '/imports/api/creature/creatures/methods/insertCreature.js';
import '/imports/api/creature/creatures/methods/removeCreature.js'; import '/imports/api/creature/creatures/methods/removeCreature.js';
import '/imports/api/creature/creatures/methods/restCreature.js'; import '/imports/api/creature/creatures/methods/restCreature.js';
import '/imports/api/creature/creatures/methods/transferCreatureOwnership.js';
import '/imports/api/creature/creatures/methods/updateCreature.js'; import '/imports/api/creature/creatures/methods/updateCreature.js';

View File

@@ -57,7 +57,7 @@ const insertCreature = new ValidatedMethod({
if (Meteor.isServer){ if (Meteor.isServer){
// Insert the 5e ruleset as the default base // Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({ insertPropertyFromLibraryNode.call({
nodeId: 'iHbhfcg3AL5isSWbw', nodeIds: ['iHbhfcg3AL5isSWbw'],
parentRef: {id: baseId, collection: 'creatureProperties'}, parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5, order: 0.5,
}); });

View File

@@ -1,55 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
const transferCreatureOwnership = new ValidatedMethod({
name: 'creatures.methods.transferOwnership',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, userId}) {
assertOwnership(creatureId, this.userId);
let tier = getUserTier(userId);
let currentCharacterCount = Creatures.find({
owner: userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.transferOwnership.denied',
'The new owner is already at their character limit')
}
Creatures.update(creatureId, {
$set: {owner: userId},
});
return creatureId;
},
});
export default transferCreatureOwnership;

View File

@@ -1,6 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js'; import nodesToTree from '/imports/api/parenting/nodesToTree.js';
export default function recomputeInventory(creatureId){ export default function recomputeInventory(creatureId){
let inventoryForest = nodesToTree({ let inventoryForest = nodesToTree({

View File

@@ -1,10 +1,10 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Experiences = new Mongo.Collection('experiences'); let Experiences = new Mongo.Collection('experiences');
@@ -12,6 +12,7 @@ let ExperienceSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The amount of XP this experience gives // The amount of XP this experience gives
xp: { xp: {
@@ -90,11 +91,6 @@ const insertExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.insert.denied', throw new Meteor.Error('Experiences.methods.insert.denied',
'You need to be logged in to insert an experience'); 'You need to be logged in to insert an experience');
} }
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.insert.denied',
`The ${tier.name} tier does not allow you to grant experience`);
}
let insertedIds = []; let insertedIds = [];
creatureIds.forEach(creatureId => { creatureIds.forEach(creatureId => {
let id = insertExperienceForCreature({experience, creatureId, userId}); let id = insertExperienceForCreature({experience, creatureId, userId});
@@ -123,11 +119,6 @@ const removeExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.remove.denied', throw new Meteor.Error('Experiences.methods.remove.denied',
'You need to be logged in to remove an experience'); 'You need to be logged in to remove an experience');
} }
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.remove.denied',
`The ${tier.name} tier does not allow you to remove an experience`);
}
let experience = Experiences.findOne(experienceId); let experience = Experiences.findOne(experienceId);
if (!experience) return; if (!experience) return;
let creatureId = experience.creatureId let creatureId = experience.creatureId
@@ -168,11 +159,6 @@ const recomputeExperiences = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.recompute.denied', throw new Meteor.Error('Experiences.methods.recompute.denied',
'You need to be logged in to recompute a creature\'s experiences'); 'You need to be logged in to recompute a creature\'s experiences');
} }
let tier = getUserTier(this.userId);
if (!tier.paidBenefits){
throw new Meteor.Error('Experiences.methods.recompute.denied',
`The ${tier.name} tier does not allow you to recompute a creature's experiences`);
}
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
let xp = 0; let xp = 0;

View File

@@ -1,14 +1,17 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ExperienceSchema = new SimpleSchema({ let ExperienceSchema = new SimpleSchema({
title: { title: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// Potentially long description of the event // Potentially long description of the event
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// The real-world date that it occured // The real-world date that it occured
date: { date: {
@@ -24,14 +27,17 @@ let ExperienceSchema = new SimpleSchema({
worldDate: { worldDate: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// Tags to better find this entry later // Tags to better find this entry later
tags: { tags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'tags.$': { 'tags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
// ID of the journal this entry belongs to // ID of the journal this entry belongs to
journalId: { journalId: {

View File

@@ -10,6 +10,7 @@ import {
prettifyParseError prettifyParseError
} from '/imports/parser/parser.js'; } from '/imports/parser/parser.js';
const PER_CREATURE_LOG_LIMIT = 100; const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
if (Meteor.isServer){ if (Meteor.isServer){
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
@@ -21,7 +22,7 @@ let CreatureLogSchema = new SimpleSchema({
content: { content: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 25, maxCount: STORAGE_LIMITS.logContentCount,
}, },
'content.$': { 'content.$': {
type: LogContentSchema, type: LogContentSchema,
@@ -45,6 +46,7 @@ let CreatureLogSchema = new SimpleSchema({
creatureName: { creatureName: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
}); });

View File

@@ -1,18 +1,21 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js'; import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let LogContentSchema = new SimpleSchema({ let LogContentSchema = new SimpleSchema({
// The name of the field, included in discord webhook message // The name of the field, included in discord webhook message
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The details of the field, included in discord webhook message // The details of the field, included in discord webhook message
// Markdown support // Markdown support
value: { value: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.summary,
}, },
context: { context: {
type: Object, type: Object,
@@ -21,6 +24,7 @@ let LogContentSchema = new SimpleSchema({
'context.errors':{ 'context.errors':{
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.errorCount,
}, },
'context.errors.$': { 'context.errors.$': {
type: ErrorSchema, type: ErrorSchema,
@@ -28,6 +32,7 @@ let LogContentSchema = new SimpleSchema({
'context.rolls': { 'context.rolls': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.rollCount,
}, },
'context.rolls.$': { 'context.rolls.$': {
type: RollDetailsSchema, type: RollDetailsSchema,

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Icons = new Mongo.Collection('icons'); let Icons = new Mongo.Collection('icons');
@@ -9,22 +10,27 @@ let iconsSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
unique: true, unique: true,
max: STORAGE_LIMITS.name,
index: 1, index: 1,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
tags: { tags: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.tagCount,
index: 1, index: 1,
}, },
'tags.$': { 'tags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
shape: { shape: {
type: String, type: String,
max: STORAGE_LIMITS.icon,
}, },
}); });

View File

@@ -6,6 +6,7 @@ import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js
import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js' import { getUserTier } from '/imports/api/users/patreon/tiers.js'
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/** /**
* Libraries are trees of library nodes where each node represents a character * Libraries are trees of library nodes where each node represents a character
@@ -21,11 +22,8 @@ let Libraries = new Mongo.Collection('libraries');
let LibrarySchema = new SimpleSchema({ let LibrarySchema = new SimpleSchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name,
}, },
isDefault: {
type: Boolean,
optional: true,
},
}); });
LibrarySchema.extend(SharingSchema); LibrarySchema.extend(SharingSchema);
@@ -39,7 +37,7 @@ const insertLibrary = new ValidatedMethod({
mixins: [ mixins: [
simpleSchemaMixin, simpleSchemaMixin,
], ],
schema: LibrarySchema.omit('owner', 'isDefault'), schema: LibrarySchema.omit('owner'),
run(library) { run(library) {
if (!this.userId) { if (!this.userId) {
throw new Meteor.Error('Libraries.methods.insert.denied', throw new Meteor.Error('Libraries.methods.insert.denied',
@@ -78,30 +76,6 @@ const updateLibraryName = new ValidatedMethod({
}, },
}); });
const setLibraryDefault = new ValidatedMethod({
name: 'libraries.makeLibraryDefault',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
isDefault: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, isDefault}) {
if (!Meteor.users.isAdmin()){
throw new Meteor.Error('Permission denied', 'User must be admin to set libraries as default');
}
return Libraries.update(_id, {$set: {isDefault}});
},
});
const removeLibrary = new ValidatedMethod({ const removeLibrary = new ValidatedMethod({
name: 'libraries.remove', name: 'libraries.remove',
validate: new SimpleSchema({ validate: new SimpleSchema({
@@ -128,4 +102,4 @@ export function removeLibaryWork(libraryId){
LibraryNodes.remove({'ancestors.id': libraryId}); LibraryNodes.remove({'ancestors.id': libraryId});
} }
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary }; export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };

View File

@@ -13,6 +13,7 @@ import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js'; import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js'; import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let LibraryNodes = new Mongo.Collection('libraryNodes'); let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -24,13 +25,16 @@ let LibraryNodeSchema = new SimpleSchema({
tags: { tags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'tags.$': { 'tags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
icon: { icon: {
type: storedIconsSchema, type: storedIconsSchema,
optional: true, optional: true,
max: STORAGE_LIMITS.icon,
} }
}); });

View File

@@ -55,7 +55,7 @@ function updateReferenceNodeWork(node, userId){
return; return;
} }
cache = { cache = {
node: {name: doc.name, type: doc.type}, node: doc,
}; };
if (library){ if (library){
cache.library = {name: library.name}; cache.library = {name: library.name};

View File

@@ -1,11 +0,0 @@
import SimpleSchema from 'simpl-schema';
Blacklist = new Mongo.Collection("blacklist");
Schemas.Blacklist = new SimpleSchema({
userId: {
type: String,
},
});
Blacklist.attachSchema(Schemas.Blacklist);

View File

@@ -1,29 +0,0 @@
import SimpleSchema from 'simpl-schema';
ChangeLogs = new Mongo.Collection("changeLogs");
Schemas.ChangeLog = new SimpleSchema({
version: {
type: String,
},
changes: {
type: [String],
},
});
ChangeLogs.attachSchema(Schemas.ChangeLog);
ChangeLogs.allow({
insert: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
update: function(userId, doc, fields, modifier) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
remove: function(userId, doc) {
var user = Meteor.users.findOne(userId);
if (user) return _.contains(user.roles, "admin");
},
});

View File

@@ -1,81 +0,0 @@
import SimpleSchema from 'simpl-schema';
Reports = new Mongo.Collection("reports");
Schemas.Report = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
title: {
type: String,
trim: false,
optional: true,
},
description: {
type: String,
trim: false,
optional: true,
},
type: {
type: String,
allowedValues: ["General Feedback", "Bug", "Suggested Change", "Feature Request"],
defaultValue: "General Feedback",
},
//the immediate impact of doing this action (eg. -1 rages)
severity: {
type: SimpleSchema.Integer,
defaultValue: 5,
min: 1,
max: 10,
},
metaData: {
type: Object,
blackbox: true,
},
});
Reports.attachSchema(Schemas.Report);
Meteor.methods({
insertReport: function(report) {
check(report, {
title: String,
description: String,
type: String,
severity: Number,
metaData: Object,
});
report.owner = this.userId;
var id = Reports.insert(report);
var user = Meteor.users.findOne(this.userId);
var sender = user &&
user.emails &&
user.emails[0] &&
user.emails[0].address ||
user.services &&
user.services.google &&
user.services.google.email ||
"reports@dicecloud.com";
var bodyText = "Report ID: " + id +
"\nSeverity: " + report.severity +
"\nType: " + report.type +
"\n\n" + report.description;
Email.send({
from: sender,
to: "stefan.zermatten@gmail.com",
subject: "DiceCloud feedback - " + report.title,
text: bodyText,
});
},
deleteReport: function(id) {
var user = Meteor.users.findOne(this.userId);
if (!_.contains(user.roles, "admin")){
throw new Meteor.Error(
"not admin",
"The user must be an administrator to delete feedback"
);
}
Reports.remove(id);
},
});

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const RefSchema = new SimpleSchema({ const RefSchema = new SimpleSchema({
id: { id: {
@@ -12,7 +13,8 @@ const RefSchema = new SimpleSchema({
index: 1 index: 1
}, },
collection: { collection: {
type: String type: String,
max: STORAGE_LIMITS.collectionName,
}, },
}); });
@@ -27,7 +29,7 @@ let ChildSchema = new SimpleSchema({
ancestors: { ancestors: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
max: 100, maxCount: STORAGE_LIMITS.ancestorCount,
}, },
'ancestors.$': { 'ancestors.$': {
type: RefSchema, type: RefSchema,

View File

@@ -1,4 +1,4 @@
import { nodesToTree } from '/imports/api/parenting/parenting.js'; import nodesToTree from '/imports/api/parenting/nodesToTree.js';
export default function getDescendantsInDepthFirstOrder({ export default function getDescendantsInDepthFirstOrder({
collection, collection,

View File

@@ -0,0 +1,115 @@
import { union, difference, sortBy, findLast } from 'lodash';
export function nodeArrayToTree(nodes){
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
nodes.forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of its nearest found ancestor, or in the forest as a root
nodeList.forEach(treeNode => {
let ancestorInForest = findLast(
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
}
});
return forest;
}
// Fetch the documents from a collection, and return the tree of those documents
export default function nodesToTree({
collection, ancestorId, filter, options = {},
includeFilteredDocAncestors = false, includeFilteredDocDescendants = false
}){
// Setup the filter
let collectionFilter = {
'ancestors.id': ancestorId,
'removed': {$ne: true},
};
if (filter){
collectionFilter = {
...collectionFilter,
...filter,
}
}
// Set up the options
let collectionSort = {
order: 1
};
if (options && options.sort){
collectionSort = {
...collectionSort,
...options.sort,
}
}
let collectionOptions = {
sort: collectionSort,
}
if (options){
collectionOptions = {
...collectionOptions,
...options,
}
}
// Find all the nodes that match the filter
let docs = collection.find(collectionFilter, collectionOptions).map(doc => {
if (!filter) return doc;
// Mark the nodes that were found by the custom filter
doc._matchedDocumentFilter = true;
return doc;
});
let ancestors = [];
let ancestorIds = [];
let docIds = [];
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){
docIds = docs.map(doc => doc._id)
}
if (filter && includeFilteredDocAncestors){
// Add all ancestor ids to an array
docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
});
// Remove the IDs of docs we have already found
ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they
ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => {
// Mark that the nodes are ancestors of the found nodes
doc._ancestorOfMatchedDocument = true;
return doc;
});
}
let descendants = [];
if (filter && includeFilteredDocDescendants){
let exludeIds = union(ancestorIds, docIds);
descendants = collection.find({
'_id': {$nin: exludeIds},
'ancestors.id': {$in: docIds},
'removed': {$ne: true},
}).map(doc => {
// Mark that the nodes are descendants of the found nodes
doc._descendantOfMatchedDocument = true;
return doc;
});
}
let nodes = sortBy([
...ancestors,
...docs,
...descendants
], 'order');
// Find all the nodes
return nodeArrayToTree(nodes);
}

View File

@@ -1,6 +1,6 @@
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { flatten, findLast } from 'lodash'; import { flatten } from 'lodash';
const generalParents = [ const generalParents = [
'attribute', 'attribute',
@@ -217,41 +217,3 @@ export function getName(doc){
if (doc.ancestors[i].name) return doc.ancestors[i].name; if (doc.ancestors[i].name) return doc.ancestors[i].name;
} }
} }
export function nodeArrayToTree(nodes){
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
nodes.forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of its nearest found ancestor, or in the forest as a root
nodeList.forEach(treeNode => {
let ancestorInForest = findLast(
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
}
});
return forest;
}
export function nodesToTree({collection, ancestorId, filter = {}, options = {}}){
if (!('ancestors.id' in filter)) filter['ancestors.id'] = ancestorId;
if (!('removed' in filter)) filter['removed'] = {$ne: true};
if (!options.sort) options.sort = {order: 1};
if (!('order' in options.sort)) options.sort.order = 1;
let nodes = collection.find(filter, options);
return nodeArrayToTree(nodes);
}

View File

@@ -1,7 +1,9 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js' import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* Actions are things a character can do * Actions are things a character can do
* Any rolls that are children of actions will be rolled when taking the action * Any rolls that are children of actions will be rolled when taking the action
@@ -12,14 +14,17 @@ let ActionSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
summary: { summary: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.summary,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// What time-resource is used to take the action in combat // What time-resource is used to take the action in combat
// long actions take longer than 1 round to cast // long actions take longer than 1 round to cast
@@ -38,13 +43,6 @@ let ActionSchema = new SimpleSchema({
'multipleTargets', 'multipleTargets',
], ],
}, },
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
},
// Duplicate the ResourceSchema here so we can extend it elegantly. // Duplicate the ResourceSchema here so we can extend it elegantly.
resources: { resources: {
type: Object, type: Object,
@@ -53,6 +51,7 @@ let ActionSchema = new SimpleSchema({
'resources.itemsConsumed': { 'resources.itemsConsumed': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.resourcesCount,
}, },
'resources.itemsConsumed.$': { 'resources.itemsConsumed.$': {
type: Object, type: Object,
@@ -67,6 +66,7 @@ let ActionSchema = new SimpleSchema({
'resources.itemsConsumed.$.tag': { 'resources.itemsConsumed.$.tag': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.tagLength,
}, },
'resources.itemsConsumed.$.quantity': { 'resources.itemsConsumed.$.quantity': {
type: Number, type: Number,
@@ -75,10 +75,12 @@ let ActionSchema = new SimpleSchema({
'resources.itemsConsumed.$.itemId': { 'resources.itemsConsumed.$.itemId': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
'resources.attributesConsumed': { 'resources.attributesConsumed': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.resourcesCount,
}, },
'resources.attributesConsumed.$': { 'resources.attributesConsumed.$': {
type: Object, type: Object,
@@ -93,6 +95,7 @@ let ActionSchema = new SimpleSchema({
'resources.attributesConsumed.$.variableName': { 'resources.attributesConsumed.$.variableName': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
'resources.attributesConsumed.$.quantity': { 'resources.attributesConsumed.$.quantity': {
type: Number, type: Number,
@@ -102,6 +105,7 @@ let ActionSchema = new SimpleSchema({
uses: { uses: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Integer of how many times it has already been used // Integer of how many times it has already been used
usesUsed: { usesUsed: {
@@ -120,14 +124,14 @@ const ComputedOnlyActionSchema = new SimpleSchema({
summaryCalculations: { summaryCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'summaryCalculations.$': InlineComputationSchema, 'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
@@ -138,6 +142,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
usesErrors: { usesErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'usesErrors.$':{ 'usesErrors.$':{
type: ErrorSchema, type: ErrorSchema,
@@ -158,15 +163,18 @@ const ComputedOnlyActionSchema = new SimpleSchema({
}, },
'resources.itemsConsumed.$.itemName': { 'resources.itemsConsumed.$.itemName': {
type: String, type: String,
max: STORAGE_LIMITS.name,
optional: true, optional: true,
}, },
'resources.itemsConsumed.$.itemIcon': { 'resources.itemsConsumed.$.itemIcon': {
type: storedIconsSchema, type: storedIconsSchema,
optional: true, optional: true,
max: STORAGE_LIMITS.icon,
}, },
'resources.itemsConsumed.$.itemColor': { 'resources.itemsConsumed.$.itemColor': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.color,
}, },
'resources.attributesConsumed': Array, 'resources.attributesConsumed': Array,
'resources.attributesConsumed.$': Object, 'resources.attributesConsumed.$': Object,
@@ -182,6 +190,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
'resources.attributesConsumed.$.statName': { 'resources.attributesConsumed.$.statName': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// True if the uses left is zero, or any item or attribute consumed is // True if the uses left is zero, or any item or attribute consumed is
// insufficient // insufficient

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const AdjustmentSchema = new SimpleSchema({ const AdjustmentSchema = new SimpleSchema({
// The roll that determines how much to change the attribute // The roll that determines how much to change the attribute
@@ -8,6 +9,7 @@ const AdjustmentSchema = new SimpleSchema({
type: String, type: String,
optional: true, optional: true,
defaultValue: '1', defaultValue: '1',
max: STORAGE_LIMITS.calculation,
}, },
// Who this adjustment applies to // Who this adjustment applies to
target: { target: {
@@ -23,6 +25,7 @@ const AdjustmentSchema = new SimpleSchema({
stat: { stat: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
operation: { operation: {
type: String, type: String,
@@ -39,6 +42,7 @@ const ComputedOnlyAdjustmentSchema = new SimpleSchema({
amountErrors: { amountErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'amountErrors.$':{ 'amountErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
// Attacks are special instances of actions // Attacks are special instances of actions
let AttackSchema = new SimpleSchema() let AttackSchema = new SimpleSchema()
@@ -11,18 +12,22 @@ let AttackSchema = new SimpleSchema()
type: String, type: String,
defaultValue: 'strength.modifier + proficiencyBonus', defaultValue: 'strength.modifier + proficiencyBonus',
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Set better defaults for the action // Set better defaults for the action
actionType: { actionType: {
type: String, type: String,
defaultValue: 'attack', defaultValue: 'attack',
max: STORAGE_LIMITS.name,
}, },
tags: { tags: {
type: Array, type: Array,
defaultValue: ['attack'], defaultValue: ['attack'],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'tags.$': { 'tags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
}); });
@@ -36,6 +41,7 @@ const ComputedOnlyAttackSchema = new SimpleSchema()
rollBonusErrors: { rollBonusErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'rollBonusErrors.$':{ 'rollBonusErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* Attributes are numbered stats of a character * Attributes are numbered stats of a character
@@ -10,6 +11,7 @@ let AttributeSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
defaultValue: 'New Attribute', defaultValue: 'New Attribute',
max: STORAGE_LIMITS.name,
}, },
// The technical, lowercase, single-word name used in formulae // The technical, lowercase, single-word name used in formulae
variableName: { variableName: {
@@ -17,6 +19,7 @@ let AttributeSchema = new SimpleSchema({
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
min: 2, min: 2,
defaultValue: 'newAttribute', defaultValue: 'newAttribute',
max: STORAGE_LIMITS.variableName,
}, },
// How it is displayed and computed is determined by type // How it is displayed and computed is determined by type
attributeType: { attributeType: {
@@ -45,16 +48,19 @@ let AttributeSchema = new SimpleSchema({
spellSlotLevelCalculation: { spellSlotLevelCalculation: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// The starting value, before effects // The starting value, before effects
baseValueCalculation: { baseValueCalculation: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Description of what the attribute is used for // Description of what the attribute is used for
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// The damage done to the attribute, always positive // The damage done to the attribute, always positive
damage: { damage: {
@@ -79,7 +85,7 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
// The result of baseValueCalculation // The result of baseValueCalculation
@@ -93,6 +99,7 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
}, },
'baseValueErrors.$': { 'baseValueErrors.$': {
type: ErrorSchema, type: ErrorSchema,
maxCount: STORAGE_LIMITS.errorCount,
}, },
// The result of spellSlotLevelCalculation // The result of spellSlotLevelCalculation
spellSlotLevelValue: { spellSlotLevelValue: {
@@ -102,6 +109,7 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
spellSlotLevelErrors: { spellSlotLevelErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'spellSlotLevelErrors.$': { 'spellSlotLevelErrors.$': {
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,18 +1,22 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let BuffSchema = new SimpleSchema({ let BuffSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
duration: { duration: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
applied: { applied: {
type: Boolean, type: Boolean,
@@ -34,7 +38,7 @@ let ComputedOnlyBuffSchema = new SimpleSchema({
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
durationSpent: { durationSpent: {
@@ -48,6 +52,7 @@ let ComputedOnlyBuffSchema = new SimpleSchema({
}, },
'appliedBy.name': { 'appliedBy.name': {
type: String, type: String,
max: STORAGE_LIMITS.name,
}, },
'appliedBy.id': { 'appliedBy.id': {
type: String, type: String,
@@ -55,6 +60,7 @@ let ComputedOnlyBuffSchema = new SimpleSchema({
}, },
'appliedBy.collection': { 'appliedBy.collection': {
type: String, type: String,
max: STORAGE_LIMITS.collectionName,
}, },
}) })

View File

@@ -1,21 +1,25 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ClassLevelSchema = new SimpleSchema({ let ClassLevelSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// Only used by slot filling dialog, not computed // Only used by slot filling dialog, not computed
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// The name of this class level's variable // The name of this class level's variable
variableName: { variableName: {
type: String, type: String,
min: 2, min: 2,
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
max: STORAGE_LIMITS.variableName,
}, },
level: { level: {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
@@ -32,6 +36,7 @@ let ClassLevelSchema = new SimpleSchema({
slotFillerCondition: { slotFillerCondition: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
}); });

View File

@@ -8,6 +8,8 @@ import {
} from '/imports/parser/parser.js'; } from '/imports/parser/parser.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* Constants are primitive values that can be used elsewhere in computations * Constants are primitive values that can be used elsewhere in computations
*/ */
@@ -15,6 +17,7 @@ let ConstantSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The technical, lowercase, single-word name used in formulae // The technical, lowercase, single-word name used in formulae
variableName: { variableName: {
@@ -22,15 +25,18 @@ let ConstantSchema = new SimpleSchema({
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
min: 2, min: 2,
defaultValue: 'newConstant', defaultValue: 'newConstant',
max: STORAGE_LIMITS.variableName,
}, },
// The input value to be parsed, must return a constant node or an array // The input value to be parsed, must return a constant node or an array
// of constant nodes to be valid // of constant nodes to be valid
calculation: { calculation: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
errors: { errors: {
type: Array, type: Array,
maxCount: STORAGE_LIMITS.errorCount,
autoValue(){ autoValue(){
let calc = this.field('calculation'); let calc = this.field('calculation');
if (!calc.isSet && this.isModifier) { if (!calc.isSet && this.isModifier) {

View File

@@ -1,11 +1,13 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ContainerSchema = new SimpleSchema({ let ContainerSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
trim: false trim: false,
max: STORAGE_LIMITS.name,
}, },
carried: { carried: {
type: Boolean, type: Boolean,
@@ -29,7 +31,8 @@ let ContainerSchema = new SimpleSchema({
description: { description: {
type: String, type: String,
optional: true, optional: true,
trim: false trim: false,
max: STORAGE_LIMITS.description,
}, },
}); });
@@ -37,7 +40,7 @@ const ComputedOnlyContainerSchema = new SimpleSchema({
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
// Weight of all the contents, zero if `contentsWeightless` is true // Weight of all the contents, zero if `contentsWeightless` is true

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* DamageMultipliers are multipliers that affect how much damage is taken from * DamageMultipliers are multipliers that affect how much damage is taken from
@@ -9,10 +10,12 @@ let DamageMultiplierSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
damageTypes: { damageTypes: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.damageTypeCount,
}, },
// The technical, lowercase, single-word name used in formulae // The technical, lowercase, single-word name used in formulae
'damageTypes.$': { 'damageTypes.$': {
@@ -29,17 +32,21 @@ let DamageMultiplierSchema = new SimpleSchema({
excludeTags: { excludeTags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'excludeTags.$': { 'excludeTags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
// Tags which must be present to be affected by this multiplier (AND) // Tags which must be present to be affected by this multiplier (AND)
includeTags: { includeTags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'includeTags.$': { 'includeTags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
}, },
}); });

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const DamageSchema = new SimpleSchema({ const DamageSchema = new SimpleSchema({
// The roll that determines how much to damage the attribute // The roll that determines how much to damage the attribute
@@ -9,6 +10,7 @@ const DamageSchema = new SimpleSchema({
type: String, type: String,
optional: true, optional: true,
defaultValue: '1d8 + strength.modifier', defaultValue: '1d8 + strength.modifier',
max: STORAGE_LIMITS.calculation,
}, },
// Who this damage applies to // Who this damage applies to
target: { target: {
@@ -35,6 +37,7 @@ const ComputedOnlyDamageSchema = new SimpleSchema({
amountErrors: { amountErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'amountErrors.$':{ 'amountErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,5 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* Effects are reason-value attached to skills and abilities * Effects are reason-value attached to skills and abilities
* that modify their final value or presentation in some way * that modify their final value or presentation in some way
@@ -8,6 +10,7 @@ let EffectSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
operation: { operation: {
type: String, type: String,
@@ -30,14 +33,17 @@ let EffectSchema = new SimpleSchema({
calculation: { calculation: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
//which stats the effect is applied to //which stats the effect is applied to
stats: { stats: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
}, },
'stats.$': { 'stats.$': {
type: String, type: String,
max: STORAGE_LIMITS.variableName,
}, },
}); });
@@ -51,6 +57,7 @@ const ComputedOnlyEffectSchema = new SimpleSchema({
errors: { errors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'errors.$':{ 'errors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,17 +1,21 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let FeatureSchema = new SimpleSchema({ let FeatureSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name,
}, },
summary: { summary: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.summary,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
}); });
@@ -20,14 +24,14 @@ let ComputedOnlyFeatureSchema = new SimpleSchema({
summaryCalculations: { summaryCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'summaryCalculations.$': InlineComputationSchema, 'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,

View File

@@ -1,10 +1,12 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
// Folders organize a character sheet into a tree, particularly to group things // Folders organize a character sheet into a tree, particularly to group things
// like 'race' and 'background' // like 'race' and 'background'
let FolderSchema = new SimpleSchema({ let FolderSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name,
}, },
}); });

View File

@@ -1,19 +1,23 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const ItemSchema = new SimpleSchema({ const ItemSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// Plural name of the item, if there is more than one // Plural name of the item, if there is more than one
plural: { plural: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// Number currently held // Number currently held
quantity: { quantity: {
@@ -58,7 +62,7 @@ let ComputedOnlyItemSchema = new SimpleSchema({
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
}); });

View File

@@ -1,18 +1,22 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let NoteSchema = new SimpleSchema({ let NoteSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
summary: { summary: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.summary,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
}); });
@@ -21,14 +25,14 @@ let ComputedOnlyNoteSchema = new SimpleSchema({
summaryCalculations: { summaryCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'summaryCalculations.$': InlineComputationSchema, 'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,

View File

@@ -1,17 +1,21 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ProficiencySchema = new SimpleSchema({ let ProficiencySchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The variableNames of the skills, tags, or attributes to apply proficiency to // The variableNames of the skills, tags, or attributes to apply proficiency to
stats: { stats: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
}, },
'stats.$': { 'stats.$': {
type: String, type: String,
max: STORAGE_LIMITS.variableName,
}, },
// A number representing how proficient the character is // A number representing how proficient the character is
// where 0.49 is half rounded down and 0.5 is half rounded up // where 0.49 is half rounded down and 0.5 is half rounded up

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ReferenceSchema = new SimpleSchema({ let ReferenceSchema = new SimpleSchema({
ref: { ref: {
@@ -13,6 +14,7 @@ let ReferenceSchema = new SimpleSchema({
'ref.collection': { 'ref.collection': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.collectionName,
}, },
// Denormalised store of referenced property's details // Denormalised store of referenced property's details
cache: { cache: {
@@ -22,6 +24,7 @@ let ReferenceSchema = new SimpleSchema({
'cache.error': { 'cache.error': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.errorMessage,
}, },
'cache.node': { 'cache.node': {
type: Object, type: Object,
@@ -30,9 +33,19 @@ let ReferenceSchema = new SimpleSchema({
'cache.node.name': { 'cache.node.name': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
'cache.node.type': { 'cache.node.type': {
type: String, type: String,
max: STORAGE_LIMITS.variableName,
},
'cache.node.level': {
type: Number,
optional: true,
},
'cache.node.value': {
type: Number,
optional: true,
}, },
'cache.library': { 'cache.library': {
type: Object, type: Object,
@@ -41,6 +54,7 @@ let ReferenceSchema = new SimpleSchema({
'cache.library.name': { 'cache.library.name': {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
}); });

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/** /**
* Rolls are children to actions or other rolls, they are triggered with 0 or * Rolls are children to actions or other rolls, they are triggered with 0 or
@@ -24,6 +25,7 @@ let RollSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
defaultValue: 'New Roll', defaultValue: 'New Roll',
max: STORAGE_LIMITS.name,
}, },
// The technical, lowercase, single-word name used in formulae // The technical, lowercase, single-word name used in formulae
variableName: { variableName: {
@@ -31,20 +33,13 @@ let RollSchema = new SimpleSchema({
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
min: 2, min: 2,
defaultValue: 'newRoll', defaultValue: 'newRoll',
max: STORAGE_LIMITS.variableName,
}, },
// The roll, can be simplified, but only computed in context // The roll, can be simplified, but only computed in context
roll: { roll: {
type: String, type: String,
optional: true, optional: true,
}, max: STORAGE_LIMITS.calculation,
// Effects can apply to this tag specifically
// Ranged spell attack, Ranged weapon attack, etc.
tags: {
type: Array,
defaultValue: [],
},
'tags.$': {
type: String,
}, },
}); });
@@ -56,6 +51,7 @@ let ComputedOnlyRollSchema = new SimpleSchema({
rollErrors: { rollErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'rollErrors.$':{ 'rollErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
// These are the rolls made when saves are called for // These are the rolls made when saves are called for
// For the saving throw bonus or proficiency, see ./Skills.js // For the saving throw bonus or proficiency, see ./Skills.js
@@ -7,11 +8,13 @@ let SavingThrowSchema = new SimpleSchema ({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The computed DC // The computed DC
dc: { dc: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Who this saving throw applies to // Who this saving throw applies to
target: { target: {
@@ -27,6 +30,7 @@ let SavingThrowSchema = new SimpleSchema ({
stat: { stat: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
}); });
@@ -38,6 +42,7 @@ const ComputedOnlySavingThrowSchema = new SimpleSchema({
dcErrors: { dcErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'dcErrors.$':{ 'dcErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/* /*
* Skills are anything that results in a modifier to be added to a D20 * Skills are anything that results in a modifier to be added to a D20
@@ -10,6 +11,7 @@ let SkillSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// The technical, lowercase, single-word name used in formulae // The technical, lowercase, single-word name used in formulae
// Ignored for skilltype = save // Ignored for skilltype = save
@@ -17,11 +19,13 @@ let SkillSchema = new SimpleSchema({
type: String, type: String,
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
min: 2, min: 2,
max: STORAGE_LIMITS.variableName,
}, },
// The variable name of the ability this skill relies on // The variable name of the ability this skill relies on
ability: { ability: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
// What type of skill is this // What type of skill is this
skillType: { skillType: {
@@ -42,6 +46,7 @@ let SkillSchema = new SimpleSchema({
baseValueCalculation: { baseValueCalculation: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// The base proficiency of this skill // The base proficiency of this skill
baseProficiency: { baseProficiency: {
@@ -52,6 +57,7 @@ let SkillSchema = new SimpleSchema({
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
}); });
@@ -69,6 +75,7 @@ let ComputedOnlySkillSchema = new SimpleSchema({
baseValueErrors: { baseValueErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'baseValueErrors.$': { 'baseValueErrors.$': {
type: ErrorSchema, type: ErrorSchema,
@@ -107,6 +114,7 @@ let ComputedOnlySkillSchema = new SimpleSchema({
rollBonuses: { rollBonuses: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.rollBonusCount,
}, },
'rollBonuses.$': { 'rollBonuses.$': {
type: String, type: String,

View File

@@ -1,25 +1,29 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
// SlotFiller fillers specifically fill a slot with a bit more control than // SlotFiller fillers specifically fill a slot with a bit more control than
// other properties // other properties
import SimpleSchema from 'simpl-schema';
let SlotFillerSchema = new SimpleSchema({ let SlotFillerSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
picture: { picture: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.url,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
// Overrides the type when searching for properties // Overrides the type when searching for properties
slotFillerType: { slotFillerType: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
// Fill more than one quantity in a slot, like feats and ability score // 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 // improvements, filtered out of UI if there isn't space in quantityExpected
@@ -32,6 +36,7 @@ let SlotFillerSchema = new SimpleSchema({
slotFillerCondition: { slotFillerCondition: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
}); });

View File

@@ -1,30 +1,66 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SlotSchema = new SimpleSchema({ let SlotSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
slotType: { slotType: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.variableName,
}, },
slotTags: { slotTags: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
}, },
'slotTags.$': { 'slotTags.$': {
type: String, type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
}, },
quantityExpected: { quantityExpected: {
type: String, type: String,
optional: true, optional: true,
defaultValue: '1', defaultValue: '1',
max: STORAGE_LIMITS.calculation,
}, },
ignored: { ignored: {
type: Boolean, type: Boolean,
@@ -33,11 +69,24 @@ let SlotSchema = new SimpleSchema({
slotCondition: { slotCondition: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
hideWhenFull: { hideWhenFull: {
type: Boolean, type: Boolean,
optional: true, optional: true,
} defaultValue: true,
},
unique: {
type: String,
allowedValues: [
// Can't choose the same slot filler twice in this slot
'uniqueInSlot',
// Can't choose the same slot filler twice accross the whole creature
'uniqueInCreature'
],
optional: true,
defaultValue: 'uniqueInSlot',
},
}); });
const ComputedOnlySlotSchema = new SimpleSchema({ const ComputedOnlySlotSchema = new SimpleSchema({
@@ -49,6 +98,7 @@ const ComputedOnlySlotSchema = new SimpleSchema({
slotConditionErrors: { slotConditionErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'slotConditionErrors.$':{ 'slotConditionErrors.$':{
type: ErrorSchema, type: ErrorSchema,
@@ -62,6 +112,7 @@ const ComputedOnlySlotSchema = new SimpleSchema({
quantityExpectedErrors: { quantityExpectedErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'quantityExpectedErrors.$':{ 'quantityExpectedErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,37 +1,36 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SpellListSchema = new SimpleSchema({ let SpellListSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
description: { description: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.description,
}, },
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
optional: true,
},
// Calculation of how many spells in this list can be prepared // Calculation of how many spells in this list can be prepared
maxPrepared: { maxPrepared: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Calculation of The attack roll bonus used by spell attacks in this list // Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: { attackRollBonus: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
// Calculation of the save dc used by spells in this list // Calculation of the save dc used by spells in this list
dc: { dc: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
}); });
@@ -39,7 +38,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
descriptionCalculations: { descriptionCalculations: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: 32, maxCount: STORAGE_LIMITS.inlineCalculationCount,
}, },
'descriptionCalculations.$': InlineComputationSchema, 'descriptionCalculations.$': InlineComputationSchema,
@@ -51,6 +50,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
maxPreparedErrors: { maxPreparedErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'maxPreparedErrors.$':{ 'maxPreparedErrors.$':{
type: ErrorSchema, type: ErrorSchema,
@@ -64,6 +64,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
attackRollBonusErrors: { attackRollBonusErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'attackRollBonusErrors.$':{ 'attackRollBonusErrors.$':{
type: ErrorSchema, type: ErrorSchema,
@@ -77,6 +78,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
dcErrors: { dcErrors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'dcErrors.$':{ 'dcErrors.$':{
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,5 +1,6 @@
import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const magicSchools = [ const magicSchools = [
'abjuration', 'abjuration',
@@ -18,6 +19,7 @@ let SpellSchema = new SimpleSchema({})
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
// If it's always prepared, it doesn't count against the number of spells // If it's always prepared, it doesn't count against the number of spells
// prepared in a spell list, and enabled should be true // prepared in a spell list, and enabled should be true
@@ -42,15 +44,18 @@ let SpellSchema = new SimpleSchema({})
type: String, type: String,
optional: true, optional: true,
defaultValue: 'action', defaultValue: 'action',
max: STORAGE_LIMITS.name,
}, },
range: { range: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
duration: { duration: {
type: String, type: String,
optional: true, optional: true,
defaultValue: 'Instantaneous', defaultValue: 'Instantaneous',
max: STORAGE_LIMITS.name,
}, },
verbal: { verbal: {
type: Boolean, type: Boolean,
@@ -67,6 +72,7 @@ let SpellSchema = new SimpleSchema({})
material: { material: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
ritual: { ritual: {
type: Boolean, type: Boolean,

View File

@@ -1,10 +1,12 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const ToggleSchema = new SimpleSchema({ const ToggleSchema = new SimpleSchema({
name: { name: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.name,
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
@@ -19,6 +21,7 @@ const ToggleSchema = new SimpleSchema({
condition: { condition: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
}); });
@@ -32,6 +35,7 @@ const ComputedOnlyToggleSchema = new SimpleSchema({
errors: { errors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'errors.$': { 'errors.$': {
type: ErrorSchema, type: ErrorSchema,

View File

@@ -1,11 +1,14 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const ErrorSchema = new SimpleSchema({ const ErrorSchema = new SimpleSchema({
message: { message: {
type: String, type: String,
max: STORAGE_LIMITS.errorMessage,
}, },
type: { type: {
type: String, type: String,
max: STORAGE_LIMITS.name,
}, },
}); });

View File

@@ -1,18 +1,22 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const InlineComputationSchema = new SimpleSchema({ const InlineComputationSchema = new SimpleSchema({
// The part between bracers {} // The part between bracers {}
calculation: { calculation: {
type: String, type: String,
max: STORAGE_LIMITS.calculation,
}, },
result: { result: {
type: String, type: String,
optional: true, optional: true,
max: STORAGE_LIMITS.calculation,
}, },
errors: { errors: {
type: Array, type: Array,
optional: true, optional: true,
maxCount: STORAGE_LIMITS.errorCount,
}, },
'errors.$': ErrorSchema, 'errors.$': ErrorSchema,
}); });

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const RollDetailsSchema = new SimpleSchema({ const RollDetailsSchema = new SimpleSchema({
number: { number: {
@@ -10,6 +11,7 @@ const RollDetailsSchema = new SimpleSchema({
values: { values: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.diceRollValuesCount,
}, },
'values.$': { 'values.$': {
type: Number, type: Number,

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import '/imports/api/sharing/sharing.js'; import '/imports/api/sharing/sharing.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SharingSchema = new SimpleSchema({ let SharingSchema = new SimpleSchema({
owner: { owner: {
@@ -11,9 +12,9 @@ let SharingSchema = new SimpleSchema({
type: Array, type: Array,
defaultValue: [], defaultValue: [],
index: 1, index: 1,
max: 50, maxCount: STORAGE_LIMITS.readersCount,
}, },
"readers.$": { 'readers.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id regEx: SimpleSchema.RegEx.Id
}, },
@@ -21,9 +22,9 @@ let SharingSchema = new SimpleSchema({
type: Array, type: Array,
defaultValue: [], defaultValue: [],
index: 1, index: 1,
max: 20, maxCount: STORAGE_LIMITS.writersCount,
}, },
"writers.$": { 'writers.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id regEx: SimpleSchema.RegEx.Id
}, },

View File

@@ -5,6 +5,7 @@ import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
const setPublic = new ValidatedMethod({ const setPublic = new ValidatedMethod({
name: 'sharing.setPublic', name: 'sharing.setPublic',
@@ -47,7 +48,7 @@ const updateUserSharePermissions = new ValidatedMethod({
run({docRef, userId, role}){ run({docRef, userId, role}){
let doc = fetchDocByRef(docRef); let doc = fetchDocByRef(docRef);
if (role === 'none'){ if (role === 'none'){
// only asser ownership if you aren't removing yourself // only assert ownership if you aren't removing yourself
if (this.userId !== userId){ if (this.userId !== userId){
assertOwnership(doc, this.userId); assertOwnership(doc, this.userId);
} }
@@ -74,4 +75,58 @@ const updateUserSharePermissions = new ValidatedMethod({
}, },
}); });
export { setPublic, updateUserSharePermissions }; const transferOwnership = new ValidatedMethod({
name: 'sharing.transferOwnership',
validate: new SimpleSchema({
docRef: RefSchema,
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, userId}){
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
let collection = getCollectionByName(docRef.collection);
let tier = getUserTier(userId);
if (docRef.collection === 'creatures'){
let currentCharacterCount = collection.find({
owner: userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Sharing.methods.transferOwnership.denied',
'The new owner is already at their character limit')
}
} else if (docRef.collection === 'libraries'){
if (!tier.paidBenefits){
throw new Meteor.Error('Sharing.methods.transferOwnership.denied',
'The new owner\'s Patreon tier does not have access to library ownership');
}
}
// First remove current permissions for the user
collection.update(docRef.id, {
$pullAll: { writers: userId, readers: userId },
});
// Then make the user the owner and the current owner a writer
return collection.update(docRef.id, {
$set: {owner: userId},
$addToSet: { writers: this.userId },
});
},
});
export { setPublic, updateUserSharePermissions, transferOwnership };

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js'; import '/imports/api/users/deleteMyAccount.js';
import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
const userSchema = new SimpleSchema({ const userSchema = new SimpleSchema({
@@ -94,6 +95,10 @@ const userSchema = new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
'preferences.hidePropertySelectDialogHelp': {
type: Boolean,
optional: true,
},
}); });
Meteor.users.attachSchema(userSchema); Meteor.users.attachSchema(userSchema);
@@ -155,7 +160,7 @@ Meteor.users.sendVerificationEmail = new ValidatedMethod({
throw new Meteor.Error('User not found', throw new Meteor.Error('User not found',
'Can\'t send a validation email to a user that does not exist'); 'Can\'t send a validation email to a user that does not exist');
} }
if (!_.some(user.emails, email => email.address === address)) { if (!some(user.emails, email => email.address === address)) {
throw new Meteor.Error('Email address not found', throw new Meteor.Error('Email address not found',
'The specified email address wasn\'t found on this user account'); 'The specified email address wasn\'t found on this user account');
} }

View File

@@ -1,47 +1,47 @@
import { findLast } from 'lodash'; import { findLast } from 'lodash';
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
import Invites from '/imports/api/users/Invites.js'; import Invites from '/imports/api/users/Invites.js';
const patreonDisabled = !!process.env.DISABLE_PATREON; const patreonDisabled = !!Meteor.settings?.public?.disablePatreon;
const TIERS = Object.freeze([ const TIERS = Object.freeze([
{ {
name: 'Commoner', name: 'Commoner',
minimumEntitledCents: 0, minimumEntitledCents: 0,
invites: 0, invites: 0,
characterSlots: 0, //5, characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Dreamer', name: 'Dreamer',
minimumEntitledCents: 100, minimumEntitledCents: 100,
invites: 0, invites: 0,
characterSlots: 0, //5, characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Wanderer', name: 'Wanderer',
minimumEntitledCents: 300, minimumEntitledCents: 300,
invites: 0, invites: 0,
characterSlots: 0, //5, characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
//cost per user $5 //cost per user $5
name: 'Adventurer', name: 'Adventurer',
minimumEntitledCents: 500, minimumEntitledCents: 500,
invites: 0, invites: 0,
characterSlots: -1, //20, characterSlots: 20,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.33 //cost per user $3.33
name: 'Hero', name: 'Hero',
minimumEntitledCents: 1000, minimumEntitledCents: 1000,
invites: 2, invites: 2,
characterSlots: -1, //50, characterSlots: 50,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.333 //cost per user $3.333
name: 'Legend', name: 'Legend',
minimumEntitledCents: 2000, minimumEntitledCents: 2000,
invites: 5, invites: 5,
characterSlots: -1, //120, characterSlots: 120,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.125 //cost per user $3.125

View File

@@ -0,0 +1,5 @@
Meteor.startup(() => {
navigator.serviceWorker.register('/sw.js')
.then()
.catch(error => console.log('ServiceWorker registration failed: ', error));
});

View File

@@ -1,104 +1,157 @@
const PROPERTIES = Object.freeze({ const PROPERTIES = Object.freeze({
action: { action: {
icon: '$vuetify.icons.action', icon: '$vuetify.icons.action',
name: 'Action' name: 'Action',
helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attack: { attack: {
icon: '$vuetify.icons.attack', icon: '$vuetify.icons.attack',
name: 'Attack' name: 'Attack',
helpText: 'Attacks are a special form of action that includes an attack roll. Attacks can critical hit, which doubles the number of damage dice in properties under the attack.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attribute: { attribute: {
icon: '$vuetify.icons.attribute', icon: '$vuetify.icons.attribute',
name: 'Attribute' name: 'Attribute',
helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.',
examples: 'Ability scores, speed, hit points, ki',
suggestedParents: ['classLevel', 'buff'],
}, },
adjustment: { adjustment: {
icon: '$vuetify.icons.attribute_damage', icon: '$vuetify.icons.attribute_damage',
name: 'Attribute damage' name: 'Attribute damage',
helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
buff: { buff: {
icon: '$vuetify.icons.buff', icon: '$vuetify.icons.buff',
name: 'Buff' name: 'Buff',
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
classLevel: { classLevel: {
icon: '$vuetify.icons.class_level', icon: '$vuetify.icons.class_level',
name: 'Class level' name: 'Class level',
helpText: 'Class levels represent a single level gained in a class',
suggestedParents: ['class'],
}, },
constant: { constant: {
icon: 'mdi-anchor', icon: 'mdi-anchor',
name: 'Constant' name: 'Constant',
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
suggestedParents: [],
}, },
container: { container: {
icon: 'mdi-bag-personal-outline', icon: 'mdi-bag-personal-outline',
name: 'Container' name: 'Container',
helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack',
suggestedParents: ['folder'],
}, },
damage: { damage: {
icon: '$vuetify.icons.damage', icon: '$vuetify.icons.damage',
name: 'Damage' name: 'Damage',
helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
damageMultiplier: { damageMultiplier: {
icon: '$vuetify.icons.damage_multiplier', icon: '$vuetify.icons.damage_multiplier',
name: 'Damage multiplier' name: 'Damage multiplier',
helpText: 'Resistance, vulnerability, and immunity.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
effect: { effect: {
icon: '$vuetify.icons.effect', icon: '$vuetify.icons.effect',
name: 'Effect' name: 'Effect',
helpText: 'Effects change the value or state of attributes and skills.',
examples: '+2 Strength, Advantage on dexterity saving throws',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
}, },
feature: { feature: {
icon: 'mdi-text-subject', icon: 'mdi-text-subject',
name: 'Feature' name: 'Feature',
helpText: 'Descriptive or narrative features your character has access to',
suggestedParents: ['classLevel', 'folder'],
}, },
folder: { folder: {
icon: 'mdi-folder-outline', icon: 'mdi-folder-outline',
name: 'Folder' name: 'Folder',
helpText: 'A way to organise other properties on the character',
suggestedParents: ['folder'],
}, },
item: { item: {
icon: 'mdi-cube-outline', icon: 'mdi-cube-outline',
name: 'Item' name: 'Item',
helpText: 'Objects and equipment your charcter finds on their adventures',
suggestedParents: ['container'],
}, },
note: { note: {
icon: 'mdi-note-outline', icon: 'mdi-note-outline',
name: 'Note' name: 'Note',
helpText: 'Notes about your character and their adventures',
suggestedParents: ['folder'],
}, },
proficiency: { proficiency: {
icon: 'mdi-brightness-1', icon: 'mdi-brightness-1',
name: 'Proficiency' name: 'Proficiency',
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
}, },
roll: { roll: {
icon: '$vuetify.icons.roll', icon: '$vuetify.icons.roll',
name: 'Roll' name: 'Roll',
helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
reference: { reference: {
icon: 'mdi-vector-link', icon: 'mdi-vector-link',
name: 'Reference', name: 'Reference',
libraryOnly: true, libraryOnly: true,
helpText: 'A reference is a link to a different property in a library. When a reference gets copied to a character sheet, it is replaced with the referenced property and all its children.',
suggestedParents: [],
}, },
savingThrow: { savingThrow: {
icon: '$vuetify.icons.saving_throw', icon: '$vuetify.icons.saving_throw',
name: 'Saving throw' name: 'Saving throw',
helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.',
suggestedParents: ['action', 'attack', 'spell'],
}, },
skill: { skill: {
icon: '$vuetify.icons.skill', icon: '$vuetify.icons.skill',
name: 'Skill' name: 'Skill',
helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.',
suggestedParents: ['classLevel', 'folder'],
}, },
propertySlot: { propertySlot: {
icon: 'mdi-power-socket-eu', icon: 'mdi-power-socket-eu',
name: 'Slot' name: 'Slot',
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: [],
}, },
slotFiller: { slotFiller: {
icon: 'mdi-power-plug-outline', icon: 'mdi-power-plug-outline',
name: 'Slot filler' name: 'Slot filler',
helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
suggestedParents: ['propertySlot'],
}, },
spellList: { spellList: {
icon: '$vuetify.icons.spell_list', icon: '$vuetify.icons.spell_list',
name: 'Spell list' name: 'Spell list',
helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within',
suggestedParents: [],
}, },
spell: { spell: {
icon: '$vuetify.icons.spell', icon: '$vuetify.icons.spell',
name: 'Spell' name: 'Spell',
helpText: 'A spell your character can potentially cast',
suggestedParents: ['spellList'],
}, },
toggle: { toggle: {
icon: '$vuetify.icons.toggle', icon: '$vuetify.icons.toggle',
name: 'Toggle' name: 'Toggle',
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
suggestedParents: [],
}, },
}); });

View File

@@ -0,0 +1,32 @@
const STORAGE_LIMITS = Object.freeze({
// String lengths
calculation: 256,
collectionName: 64,
color: 10000,
description: 49473, //the length of the Bee Movie script
errorMessage: 256,
icon: 10000,
name: 128,
summary: 10000,
tagLength: 128,
url: 256,
variableName: 64,
//Array counts
ancestorCount: 100,
damageTypeCount: 32,
diceRollValuesCount: 100,
errorCount: 32,
extraTagsCount: 5,
inlineCalculationCount: 32,
logContentCount: 32,
readersCount: 50,
resourcesCount: 32,
rollCount: 64,
rollBonusCount: 32,
statsToTarget: 32,
tagCount: 64,
writersCount: 20,
});
export default STORAGE_LIMITS;

View File

@@ -2,6 +2,7 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js'; import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import roll from '/imports/parser/roll.js'; import roll from '/imports/parser/roll.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
export default class RollNode extends ParseNode { export default class RollNode extends ParseNode {
constructor({left, right}) { constructor({left, right}) {
@@ -42,9 +43,9 @@ export default class RollNode extends ParseNode {
if (context.doubleRolls){ if (context.doubleRolls){
number *= 2; number *= 2;
} }
if (number > 100) return new ErrorNode({ if (number > STORAGE_LIMITS.diceRollValuesCount) return new ErrorNode({
node: this, node: this,
error: 'Can\'t roll more than 100 dice at once', error: `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`,
context, context,
}); });
let diceSize = right.value; let diceSize = right.value;

View File

@@ -0,0 +1,4 @@
import { Accounts } from 'meteor/accounts-base'
Accounts.emailTemplates.from = 'no-reply@dicecloud.com';
Accounts.emailTemplates.siteName = 'DiceCloud';

View File

@@ -9,3 +9,4 @@ import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'; import '/imports/server/publications/slotFillers.js';
import '/imports/server/publications/ownedDocuments.js'; import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/archivedCreatures.js'; import '/imports/server/publications/archivedCreatures.js';
import '/imports/server/publications/searchLibraryNodes.js';

View File

@@ -2,13 +2,6 @@ import SimpleSchema from 'simpl-schema';
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
const standardLibraryIds = [
'SRDLibraryGA3XWsd',
];
Meteor.publish('standardLibraries', function(){
return Libraries.find({_id: {$in: standardLibraryIds}});
});
Meteor.publish('libraries', function(){ Meteor.publish('libraries', function(){
this.autorun(function (){ this.autorun(function (){
@@ -75,3 +68,24 @@ Meteor.publish('libraryNodes', function(libraryId){
]; ];
}); });
}); });
Meteor.publish('descendantLibraryNodes', function(nodeId){
let node = LibraryNodes.findOne(nodeId);
let libraryId = node?.ancestors[0]?.id;
if (!libraryId) return [];
this.autorun(function (){
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){
return this.error(e);
}
return [
LibraryNodes.find({
'ancestors.id': nodeId,
}, {
sort: {order: 1},
}),
];
});
});

View File

@@ -0,0 +1,116 @@
import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
Meteor.publish('searchLibraryNodes', function(){
let self = this;
this.autorun(function (){
let type = self.data('type');
if (!type) return [];
let userId = this.userId;
if (!userId) {
return [];
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
if (!user) return [];
const subs = user.subscribedLibraries || [];
let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
]
}, {
fields: {_id: 1, name: 1},
});
let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the type
let filter = {
'ancestors.id': {$in: libraryIds},
removed: {$ne: true},
tags: {$ne: []}, // Only tagged library nodes are considered
};
if (type){
filter.$or = [{
type,
},{
type: 'slotFiller',
slotFillerType: type,
}];
}
this.autorun(function(){
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 32;
check(limit, Number);
// Get the search term
let searchTerm = self.data('searchTerm') || '';
check(searchTerm, String);
let options = undefined;
if (searchTerm){
filter.$text = {$search: searchTerm};
options = {
// relevant documents have a higher score.
fields: {
score: { $meta: 'textScore' }
},
sort: {
// `score` property specified in the projection fields above.
score: { $meta: 'textScore' },
'ancestors.0.id': 1,
name: 1,
order: 1,
}
}
} else {
delete filter.$text
options = {sort: {
'ancestors.0.id': 1,
name: 1,
order: 1,
}};
}
options.limit = limit;
this.autorun(function () {
self.setData('countAll', LibraryNodes.find(filter).count());
});
let cursor = LibraryNodes.find(filter, options);
Mongo.Collection._publishCursor(libraries, self, 'libraries');
let observeHandle = cursor.observeChanges({
added: function (id, fields) {
fields._searchResult = true;
self.added('libraryNodes', id, fields);
},
changed: function (id, fields) {
self.changed('libraryNodes', id, fields);
},
removed: function (id) {
self.removed('libraryNodes', id);
}
},
// Publications don't mutate the documents
{ nonMutatingCallbacks: true }
);
// register stop callback (expects lambda w/ no args).
this.onStop(function () {
observeHandle.stop();
});
// this.ready();
});
});
});

View File

@@ -2,8 +2,11 @@ import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
Meteor.publish('slotFillers', function(slotId, searchTerm){
if (searchTerm) check(searchTerm, String);
Meteor.publish('slotFillers', function(slotId){
let self = this; let self = this;
this.autorun(function (){ this.autorun(function (){
let userId = this.userId; let userId = this.userId;
@@ -21,7 +24,7 @@ Meteor.publish('slotFillers', function(slotId){
fields: {subscribedLibraries: 1} fields: {subscribedLibraries: 1}
}); });
const subs = user && user.subscribedLibraries || []; const subs = user && user.subscribedLibraries || [];
let libraryIds = Libraries.find({ let libraries = Libraries.find({
$or: [ $or: [
{owner: this.userId}, {owner: this.userId},
{writers: this.userId}, {writers: this.userId},
@@ -29,45 +32,29 @@ Meteor.publish('slotFillers', function(slotId){
{_id: {$in: subs}}, {_id: {$in: subs}},
] ]
}, { }, {
fields: {_id: 1}, fields: {_id: 1, name: 1},
}).map(lib => lib._id); });
let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the slot // Build a filter for nodes in those libraries that match the slot
let filter = { let filter = getSlotFillFilter({slot, libraryIds});
'ancestors.id': {$in: libraryIds},
removed: {$ne: true},
};
if (slot.slotTags && slot.slotTags.length){
filter.tags = {$all: slot.slotTags};
}
if (slot.slotType){
filter.$or = [{
type: slot.slotType
},{
type: 'slotFiller',
slotFillerType: slot.slotType,
}];
}
this.autorun(function(){ this.autorun(function(){
// Get the limit of the documents the user can fetch // Get the limit of the documents the user can fetch
var limit = self.data('limit') || 20; var limit = self.data('limit') || 50;
check(limit, Number); check(limit, Number);
// Get the search term
let searchTerm = self.data('searchTerm') || '';
check(searchTerm, String);
let options = undefined; let options = undefined;
if (searchTerm){ if (searchTerm){
filter.$text = {$search: searchTerm}; filter.$text = {$search: searchTerm};
options = { options = {
// relevant documents have a higher score. // relevant documents have a higher score.
fields: { fields: {
score: { $meta: 'textScore' } _score: { $meta: 'textScore' }
}, },
sort: { sort: {
// `score` property specified in the projection fields above. // `score` property specified in the projection fields above.
score: { $meta: 'textScore' }, _score: { $meta: 'textScore' },
name: 1, name: 1,
order: 1, order: 1,
} }
@@ -85,7 +72,8 @@ Meteor.publish('slotFillers', function(slotId){
self.setData('countAll', LibraryNodes.find(filter).count()); self.setData('countAll', LibraryNodes.find(filter).count());
}); });
self.autorun(function () { self.autorun(function () {
return LibraryNodes.find(filter, options); Meteor._sleepForMs(1000);
return [LibraryNodes.find(filter, options), libraries];
}); });
}); });
}); });

View File

@@ -35,7 +35,7 @@
<text-field <text-field
ref="iconSearchField" ref="iconSearchField"
label="Search icons" label="Search icons"
append-icon="mdi-search" append-icon="mdi-magnify"
clearable clearable
hide-details hide-details
class="ma-2" class="ma-2"

View File

@@ -1,7 +1,10 @@
<template lang="html"> <template lang="html">
<v-sheet <v-sheet
class="tree-node" class="tree-node"
:class="!hasChildren ? 'empty' : null" :class="{
'empty': !hasChildren,
'found': node._matchedDocumentFilter,
}"
:data-id="`tree-node-${node._id}`" :data-id="`tree-node-${node._id}`"
> >
<div <div
@@ -52,7 +55,7 @@
:children="computedChildren" :children="computedChildren"
:group="group" :group="group"
:organize="organize" :organize="organize"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
@reordered="e => $emit('reordered', e)" @reordered="e => $emit('reordered', e)"
@reorganized="e => $emit('reorganized', e)" @reorganized="e => $emit('reorganized', e)"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@@ -80,6 +83,7 @@
import { canBeParent } from '/imports/api/parenting/parenting.js'; import { canBeParent } from '/imports/api/parenting/parenting.js';
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js'; import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import { some } from 'lodash';
export default { export default {
name: 'TreeNode', name: 'TreeNode',
@@ -87,16 +91,33 @@
TreeNodeView, TreeNodeView,
}, },
props: { props: {
node: Object, node: {
group: String, type: Object,
required: true,
},
group: {
type: String,
required: true,
},
organize: Boolean, organize: Boolean,
children: Array, children: {
getChildren: Function, type: Array,
selectedNodeId: String, default: () => [],
},
getChildren: {
type: Function,
default: undefined,
},
selectedNode: {
type: Object,
default: undefined,
},
selected: Boolean, selected: Boolean,
}, },
data(){ return { data(){return {
expanded: false, expanded: this.node._ancestorOfMatchedDocument ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
false,
}}, }},
computed: { computed: {
hasChildren(){ hasChildren(){
@@ -119,6 +140,15 @@
return canBeParent(this.node.type); return canBeParent(this.node.type);
}, },
}, },
watch: {
'node._ancestorOfMatchedDocument'(value){
this.expanded = !!value ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
},
'selectedNode.ancestors'(value){
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
},
},
beforeCreate() { beforeCreate() {
this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default
}, },
@@ -148,9 +178,12 @@
.empty .v-btn { .empty .v-btn {
opacity: 0.4; opacity: 0.4;
} }
.found {
background: rgba(200, 0, 0, 0.1) !important;
}
.ghost { .ghost {
opacity: 0.5; opacity: 0.5;
background: #fbc8c8; background: rgba(251, 0, 0, 0.3);
} }
.v-icon.v-icon--disabled { .v-icon.v-icon--disabled {
opacity: 0; opacity: 0;

View File

@@ -18,8 +18,9 @@
:node="child.node" :node="child.node"
:children="child.children" :children="child.children"
:group="group" :group="group"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
:selected="selectedNodeId === child.node._id" :selected="selectedNode && selectedNode._id === child.node._id"
:ancestors-of-selected-node="ancestorsOfSelectedNode"
:organize="organize" :organize="organize"
:lazy="lazy" :lazy="lazy"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@@ -49,7 +50,14 @@
type: Array, type: Array,
default: () => [], default: () => [],
}, },
selectedNodeId: String, selectedNode: {
type: Object,
default: undefined,
},
ancestorsOfSelectedNode: {
type: Array,
default: () => [],
},
}, },
data(){ return { data(){ return {
expanded: false, expanded: false,

View File

@@ -0,0 +1,81 @@
<template lang="html">
<v-combobox
v-model="filterTerms"
:items="filterOptions"
prepend-inner-icon="mdi-magnify"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/>
</template>
<script lang="js">
export default {
props: {
value: {
type: Object,
default: undefined,
},
},
data(){return {
filterTerms: [],
filterOptions: [
{text: 'Actions', value: 'action'},
{text: 'Attacks', value: 'attack'},
{text: 'Attributes', value: 'attribute'},
{text: 'Buffs', value: 'buff'},
{text: 'Class Levels', value: 'classLevel'},
{text: 'Damage Multipliers', value: 'damageMultiplier'},
{text: 'Effects', value: 'effect'},
{text: 'Experiences', value: 'experience'},
{text: 'Features', value: 'feature'},
{text: 'Folders', value: 'folder'},
{text: 'Notes', value: 'note'},
{text: 'Proficiencies', value: 'proficiency'},
{text: 'Rolls', value: 'roll'},
{text: 'Saving Throws', value: 'savingThrow'},
{text: 'Skills', value: 'skill'},
{text: 'Spell Lists', value: 'spellList'},
{text: 'Spells', value: 'spell'},
{text: 'Containers', value: 'container'},
{text: 'Items', value: 'item'},
],
}},
computed: {
filter(){
if (!this.filterTerms.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterTerms.forEach(filter => {
if (filter.value){
typeFilters.push(filter.value);
} else {
// escape string
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
var reg = new RegExp( '.*' + term + '.*', 'i' );
nameFilters.push(reg)
}
});
let filter = {};
if (typeFilters.length){
filter.type = {$in: typeFilters};
}
if (nameFilters.length){
filter.name = {$in: nameFilters};
}
return filter;
},
},
watch:{
filter(value){
this.$emit('input', value);
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,4 +1,4 @@
<template> if<template>
<div class="character-sheet fill-height"> <div class="character-sheet fill-height">
<v-fade-transition mode="out-in"> <v-fade-transition mode="out-in">
<div <div
@@ -35,6 +35,10 @@
class="fill-height" class="fill-height"
> >
<v-tabs-items <v-tabs-items
:key=" '' +
creature.settings.hideSpellsTab +
creature.settings.showTreeTab
"
:value="$store.getters.tabById($route.params.id)" :value="$store.getters.tabById($route.params.id)"
class="card-background" class="card-background"
@change="e => $store.commit( @change="e => $store.commit(
@@ -51,13 +55,17 @@
<v-tab-item> <v-tab-item>
<inventory-tab :creature-id="creatureId" /> <inventory-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item v-show="!creature.settings.hideSpellsTab"> <v-tab-item
v-if="!creature.settings.hideSpellsTab"
>
<spells-tab :creature-id="creatureId" /> <spells-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item> <v-tab-item>
<character-tab :creature-id="creatureId" /> <character-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item v-if="creature.settings.showTreeTab"> <v-tab-item
v-if="creature.settings.showTreeTab"
>
<tree-tab :creature-id="creatureId" /> <tree-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
</v-tabs-items> </v-tabs-items>

View File

@@ -31,41 +31,23 @@
:key="type" :key="type"
color="primary" color="primary"
:data-id="`insert-creature-property-type-${type}`" :data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name" :label="getPropertyLabel(type)"
:icon="properties[type].icon" :icon="type ? properties[type].icon : 'mdi-plus'"
:disabled="!editPermission" :disabled="!editPermission"
@click="insertPropertyOfType(type)" @click="addProperty(type)"
/> />
<template v-if="tabNumber === 5">
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-btn"
label="New Property"
icon="mdi-pencil"
:disabled="!editPermission"
@click="insertTreeProperty"
/>
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-from-library-btn"
label="Property From Library"
icon="mdi-library-shelves"
:disabled="!editPermission"
@click="propertyFromLibrary"
/>
</template>
</v-speed-dial> </v-speed-dial>
</template> </template>
<script lang="js"> <script lang="js">
import LabeledFab from '/imports/ui/components/LabeledFab.vue'; import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { getHighestOrder } from '/imports/api/parenting/order.js'; 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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js'; import PROPERTIES from '/imports/constants/PROPERTIES.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function getParentAndOrderFromSelectedTreeNode(creatureId){ function getParentAndOrderFromSelectedTreeNode(creatureId){
// find the parent based on the currently selected property // find the parent based on the currently selected property
@@ -138,104 +120,75 @@
return this.$route.params.id; return this.$route.params.id;
}, },
tabNumber(){ tabNumber(){
return this.$store.getters.tabById(this.creatureId); let tabNumber = this.$store.getters.tabById(this.creatureId);
if (this.hideSpellsTab && tabNumber > 2){
tabNumber += 1;
}
return tabNumber;
}, },
speedDials(){ speedDials(){
return this.speedDialsByTab[tabs[this.tabNumber]]; return this.speedDialsByTab[tabs[this.tabNumber]];
}, },
speedDialsByTab() { return { speedDialsByTab() { return {
'stats': ['attribute', 'skill', 'action', 'attack'], 'stats': ['attribute', 'skill', 'action', 'attack', 'buff'],
'features': ['feature'], 'features': ['feature'],
'inventory': ['item', 'container'], 'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'], 'spells': ['spellList', 'spell'],
'character': ['note'], 'character': ['note'],
'tree': [], 'tree': [null],
};}, };},
properties(){ properties(){
return PROPERTIES; return PROPERTIES;
}, },
}, },
meteor: {
hideSpellsTab(){
let creature = Creatures.findOne(this.creatureId);
return creature?.settings.hideSpellsTab;
},
},
methods: { methods: {
insertPropertyOfType(type){ getPropertyLabel(type){
if (type === 'buff') return 'Buff or Condition';
return type ? PROPERTIES[type].name : 'Property'
},
addProperty(forcedType){
let creatureId = this.creatureId; let creatureId = this.creatureId;
let fab = hideFab(); let fab = hideFab();
// Open the dialog to insert the property let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let parent;
try {
parent = fetchDocByRef(parentRef);
} catch (e) {
console.warn(e);
}
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog', component: 'add-creature-property-dialog',
elementId: 'insert-creature-property-type-' + type, elementId: 'insert-creature-property-type-' + forcedType,
data: { data: {
forcedType: type, parentDoc: forcedType ? undefined : parent,
forcedType,
}, },
callback(creatureProperty){ callback(result){
if (!creatureProperty) return 'insert-creature-property-fab'; if (!result){
revealFab(fab); return 'insert-creature-property-fab';
}
// Insert the property if (Array.isArray(result)){
creatureProperty.order = getHighestOrder({ revealFab(fab);
collection: CreatureProperties, let nodeIds = result;
ancestorId: creatureId let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
}) + 1; return forcedType ? id : `tree-node-${id}`;
} else {
let tagDetails; revealFab(fab);
switch (type){ let creatureProperty = result;
case 'item': // Get order and parent
tagDetails = {tag: 'carried', name: 'Carried'}; creatureProperty.order = order;
break; // Insert the property
case 'container': let id = insertProperty.call({creatureProperty, parentRef});
tagDetails = {tag: 'inventory', name: 'Inventory'}; return forcedType ? id : `tree-node-${id}`;
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({nodeId, parentRef, order});
return `tree-node-${id}`;
} }
}); });
}, },

View File

@@ -80,6 +80,10 @@
> >
<v-tabs <v-tabs
v-if="creature && creature.settings" v-if="creature && creature.settings"
:key=" '' +
creature.settings.hideSpellsTab +
creature.settings.showTreeTab
"
class="flex" class="flex"
style="min-width: 0" style="min-width: 0"
centered centered
@@ -102,7 +106,7 @@
<v-tab> <v-tab>
Inventory Inventory
</v-tab> </v-tab>
<v-tab v-show="!creature.settings.hideSpellsTab"> <v-tab v-if="!creature.settings.hideSpellsTab">
Spells Spells
</v-tab> </v-tab>
<v-tab> <v-tab>
@@ -134,13 +138,13 @@ import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import SharedIcon from '/imports/ui/components/SharedIcon.vue'; import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default { export default {
inject: {
context: { default: {} }
},
components: { components: {
CharacterSheetFab, CharacterSheetFab,
SharedIcon, SharedIcon,
}, },
inject: {
context: { default: {} }
},
computed: { computed: {
creatureId(){ creatureId(){
return this.$route.params.id; return this.$route.params.id;

View File

@@ -15,7 +15,7 @@
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-list-item-title> <v-list-item-title>
{{ creature.denormalizedStats.weightCarried || 0 }} lb {{ weightCarried }} lb
</v-list-item-title> </v-list-item-title>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
@@ -107,6 +107,7 @@ import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import CoinValue from '/imports/ui/components/CoinValue.vue'; import CoinValue from '/imports/ui/components/CoinValue.vue';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
export default { export default {
components: { components: {
@@ -207,6 +208,11 @@ export default {
containerIds(){ containerIds(){
return this.containers.map(container => container._id); return this.containers.map(container => container._id);
}, },
weightCarried(){
return stripFloatingPointOddities(
this.creature.denormalizedStats.weightCarried || 0
);
},
}, },
methods: { methods: {
clickProperty(_id){ clickProperty(_id){

View File

@@ -23,19 +23,11 @@
:disabled="organizeDisabled" :disabled="organizeDisabled"
style="flex-grow: 0; height: 32px;" style="flex-grow: 0; height: 32px;"
/> />
<v-combobox <tree-search-input
ref="searchBox" ref="searchBox"
slot="extension" slot="extension"
v-model="filterString" v-model="filter"
:items="filterOptions"
prepend-inner-icon="mdi-search"
class="mx-4" class="mx-4"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/> />
</v-toolbar> </v-toolbar>
<creature-properties-tree <creature-properties-tree
@@ -43,7 +35,7 @@
style="overflow-y: auto;" style="overflow-y: auto;"
:root="{collection: 'creatures', id: creatureId}" :root="{collection: 'creatures', id: creatureId}"
:organize="organize" :organize="organize"
:selected-node-id="selected" :selected-node="selectedNode"
:filter="filter" :filter="filter"
@selected="clickNode" @selected="clickNode"
/> />
@@ -51,9 +43,9 @@
<template slot="detail"> <template slot="detail">
<creature-property-dialog <creature-property-dialog
embedded embedded
:_id="selected" :_id="selectedNodeId"
@removed="selected = undefined" @removed="selectedNodeId = undefined"
@duplicated="id => selected = id" @duplicated="id => selectedNodeId = id"
/> />
</template> </template>
</tree-detail-layout> </tree-detail-layout>
@@ -65,13 +57,14 @@
import TreeDetailLayout from '/imports/ui/components/TreeDetailLayout.vue'; import TreeDetailLayout from '/imports/ui/components/TreeDetailLayout.vue';
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue'; import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'; import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default { export default {
components: { components: {
TreeDetailLayout, TreeDetailLayout,
TreeSearchInput,
CreaturePropertiesTree, CreaturePropertiesTree,
CreaturePropertyDialog, CreaturePropertyDialog,
}, },
@@ -87,52 +80,10 @@
data(){ return { data(){ return {
organize: false, organize: false,
organizeDisabled: false, organizeDisabled: false,
selected: undefined, selectedNodeId: undefined,
fab: false, fab: false,
filterString: '', filter: undefined,
filterOptions: [
{text: 'Actions', value: 'action'},
{text: 'Attacks', value: 'attack'},
{text: 'Attributes', value: 'attribute'},
{text: 'Buffs', value: 'buff'},
{text: 'Class Levels', value: 'classLevel'},
{text: 'Damage Multipliers', value: 'damageMultiplier'},
{text: 'Effects', value: 'effect'},
{text: 'Experiences', value: 'experience'},
{text: 'Features', value: 'feature'},
{text: 'Folders', value: 'folder'},
{text: 'Notes', value: 'note'},
{text: 'Proficiencies', value: 'proficiency'},
{text: 'Rolls', value: 'roll'},
{text: 'Saving Throws', value: 'savingThrow'},
{text: 'Skills', value: 'skill'},
{text: 'Spell Lists', value: 'spellList'},
{text: 'Spells', value: 'spell'},
{text: 'Containers', value: 'container'},
{text: 'Items', value: 'item'},
],
};}, };},
computed: {
filter(){
if (!this.filterString.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterString.forEach(filter => {
if (filter.value){
typeFilters.push(filter.value);
} else {
// escape string
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
var reg = new RegExp( '.*' + term + '.*', 'i' );
nameFilters.push(reg)
}
});
return {$or: [
{type: {$in: typeFilters}},
{name: {$in: nameFilters}},
]};
},
},
watch: { watch: {
filter(filter){ filter(filter){
if (filter) { if (filter) {
@@ -144,14 +95,14 @@
}, },
'$vuetify.breakpoint.mdAndUp'(mdAndUp){ '$vuetify.breakpoint.mdAndUp'(mdAndUp){
if (!mdAndUp){ if (!mdAndUp){
this.selected = undefined; this.selectedNodeId = undefined;
} }
}, },
}, },
methods: { methods: {
clickNode(id){ clickNode(id){
if (this.$vuetify.breakpoint.mdAndUp){ if (this.$vuetify.breakpoint.mdAndUp){
this.selected = id; this.selectedNodeId = id;
} else { } else {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
@@ -167,7 +118,7 @@
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: 'selected-node-card', elementId: 'selected-node-card',
data: { data: {
_id: this.selected, _id: this.selectedNodeId,
startInEditTab: true, startInEditTab: true,
}, },
}); });
@@ -175,9 +126,9 @@
getPropertyName, getPropertyName,
}, },
meteor: { meteor: {
selectedProperty(){ selectedNode(){
return CreatureProperties.findOne({ return CreatureProperties.findOne({
_id: this.selected, _id: this.selectedNodeId,
removed: {$ne: true} removed: {$ne: true}
}); });
} }

View File

@@ -0,0 +1,333 @@
<template lang="html">
<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"
>
<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-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"
v-if="type"
class="creature-property-form"
:model="model"
:errors="errors"
@change="change"
@push="push"
@pull="pull"
/>
</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 LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
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: {
PropertySelector,
DialogBase,
TreeNodeView,
LibraryNodeExpansionContent,
...propertyFormIndex,
},
mixins: [schemaFormMixin],
props: {
forcedType: {
type: String,
default: undefined,
},
suggestedTypes: {
type: Array,
default: undefined,
},
suggestedType: {
type: String,
default: undefined,
},
parentDoc: {
type: Object,
default: undefined,
},
},
reactiveProvide: {
name: 'context',
include: ['debounceTime'],
},
data(){return {
selectedNodeIds: [],
type: this.forcedType || this.suggestedType,
model: {
type: this.type,
},
searchValue: undefined,
debounceTime: 0,
tab: 0,
};},
computed: {
typeName(){
return getPropertyName(this.type) || 'Property';
},
toolbarColor(){
return getThemeColor('secondary');
}
},
watch: {
type(newType){
this.changeType(newType);
},
},
mounted(){
this.changeType(this.type);
},
methods: {
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);
this._subs.searchLibraryNodes.setData('limit', undefined);
this.selectedNode = undefined;
this.searchValue = val;
setTimeout(ack, 200);
},
loadMore(){
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;
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({});
model.type = type;
this.model = model;
},
openPropertyDetails(id){
this.$store.commit('pushDialogStack', {
component: 'library-node-dialog',
elementId: id,
data: {
_id: id,
},
});
},
},
meteor: {
'$subscribe':{
'searchLibraryNodes': [],
},
showPropertyHelp(){
let user = Meteor.user();
return !(user?.preferences?.hidePropertySelectDialogHelp)
},
currentLimit(){
return this._subs.searchLibraryNodes.data('limit') || 32;
},
countAll(){
return this._subs.searchLibraryNodes.data('countAll');
},
libraryNodes(){
return LibraryNodes.find({
_searchResult: true
},{
sort: {
'ancestors.0.id': 1,
name: 1,
order: 1,
},
});
},
libraryNames(){
let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names;
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -4,7 +4,7 @@
:children="children" :children="children"
:group="group" :group="group"
:organize="organize" :organize="organize"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@reordered="reordered" @reordered="reordered"
@reorganized="reorganized" @reorganized="reorganized"
@@ -13,7 +13,7 @@
<script lang="js"> <script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js' import nodesToTree from '/imports/api/parenting/nodesToTree.js'
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js'; import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
@@ -24,8 +24,14 @@
props: { props: {
root: Object, root: Object,
organize: Boolean, organize: Boolean,
selectedNodeId: String, selectedNode: {
filter: Object, type: Object,
default: undefined,
},
filter: {
type: Object,
default: undefined,
},
group: { group: {
type: String, type: String,
default: 'creatureProperties' default: 'creatureProperties'
@@ -37,6 +43,8 @@
collection: CreatureProperties, collection: CreatureProperties,
ancestorId: this.root.id, ancestorId: this.root.id,
filter: this.filter, filter: this.filter,
includeFilteredDocAncestors: true,
includeFilteredDocDescendants: true,
}); });
}, },
}, },

View File

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

View File

@@ -46,44 +46,44 @@ import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js'; import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
export default { export default {
components: { components: {
...propertyFormIndex, ...propertyFormIndex,
DialogBase, DialogBase,
ColorPicker, ColorPicker,
}, },
mixins: [schemaFormMixin], mixins: [schemaFormMixin],
props: { props: {
propertyName: String, propertyName: String,
type: String, type: String,
}, },
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['debounceTime'], include: ['debounceTime'],
}, },
data(){return { data(){return {
model: { model: {
type: this.type, type: this.type,
}, },
schema: undefined, schema: undefined,
validationContext: undefined, validationContext: undefined,
debounceTime: 0, debounceTime: 0,
};}, };},
watch: { watch: {
type(newType){ type(newType){
this.changeType(newType); this.changeType(newType);
}, },
}, },
mounted(){ mounted(){
this.changeType(this.type); this.changeType(this.type);
}, },
methods:{ methods:{
changeType(type){ changeType(type){
if (!type) return; if (!type) return;
this.schema = propertySchemasIndex[type]; this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext(); this.validationContext = this.schema.newContext();
let model = this.schema.clean({}); let model = this.schema.clean({});
model.type = type; model.type = type;
this.model = model; this.model = model;
} }
}, },
} }

View File

@@ -8,145 +8,173 @@
{{ model.name }} {{ model.name }}
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
<text-field <v-text-field
prepend-inner-icon="mdi-search" v-model="searchInput"
prepend-inner-icon="mdi-magnify"
regular regular
clearable
hide-details hide-details
:value="searchValue" class="flex-grow-0"
:debounce="300" style="flex-basis: 300px;"
@change="searchChanged" :loading="searchLoading"
@keyup.enter="insert" @change="searchValue = searchInput || undefined"
@click:clear="searchValue = undefined"
/> />
</template> </template>
<div <property-description
class="library-nodes" :string="model.description"
/>
<p>
{{ slotPropertyTypeName }} with tags:
<template v-for="(tags, index) in tagsSearched.or">
<property-tags
:key="index"
:tags="tags"
:prefix="index ? 'OR' : undefined"
/>
</template>
<template v-for="(tags, index) in tagsSearched.not">
<property-tags
:key="index"
:tags="tags"
prefix="NOT"
/>
</template>
</p>
<v-expansion-panels
multiple
inset
> >
<v-fade-transition mode="out-in"> <template v-for="libraryNode in libraryNodes">
<div v-if="libraryNodes && libraryNodes.length"> <v-expansion-panel
<section v-if="showDisabled || !libraryNode._disabledBySlotFillerCondition"
class="layout wrap justify-between" :key="libraryNode._id"
> :model="libraryNode"
<v-card :data-id="libraryNode._id"
v-for="node in libraryNodes" :class="{disabled: isDisabled(libraryNode)}"
:key="node._id" >
hover <v-expansion-panel-header>
ripple <template #default="{ open }">
class="slot-card layout column justify-end" <v-layout
:class="{'selected': node._id === (selectedNode && selectedNode._id)}" align-center
:dark="node._id === (selectedNode && selectedNode._id)" class="flex-grow-0 mr-2"
@click="selectedNode = node"
>
<v-img
v-if="node.picture"
:src="node.picture"
:height="200"
contain
class="slot-card-image"
/>
<v-card-title primary-title>
<tree-node-view
class="mr-2 text-h6 mb-0"
:class="{'theme--dark': node._id === (selectedNode && selectedNode._id)}"
:hide-icon="node.picture"
:model="node"
:color="node.color"
/>
</v-card-title>
<v-card-text
v-if="node.description"
class="pt-0"
> >
<property-description <v-checkbox
class="slot-card-text line-clamp" v-if="libraryNode._disabledByAlreadyAdded"
:string="node.description" class="my-0 py-0"
hide-details
:input-value="true"
disabled
/> />
</v-card-text> <v-checkbox
</v-card> v-else
</section> v-model="selectedNodeIds"
</div> class="my-0 py-0"
<div hide-details
v-else-if="countAll" :disabled="isDisabled(libraryNode)"
class="ma-4" :value="libraryNode._id"
> @click.stop
<h4 v-if="numFiltered"> />
Requirements of {{ numFiltered }} library properties were not met. </v-layout>
</h4> <v-layout column>
<h4 v-else> <v-layout align-center>
Nothing suitable was found in your libraries. <tree-node-view :model="libraryNode" />
</h4> <div
</div> v-if="libraryNode._disabledBySlotFillerCondition"
<div class="error--text text-no-wrap text-truncate"
v-else-if="$subReady.slotFillers" >
class="ma-4" {{ libraryNode.slotFillerCondition }}
> </div>
<h4> </v-layout>
Nothing suitable was found in your libraries <div class="text-caption text-no-wrap text-truncate">
<span v-if="searchValue"> {{ libraryNames[libraryNode.ancestors[0].id ] }}
matching "{{ searchValue }}" </div>
</span> </v-layout>
</h4> <div
<p> v-if="libraryNode.slotQuantityFilled !== undefined && libraryNode.slotQuantityFilled !== 1"
This slot requires a {{ slotPropertyTypeName }} class="text-overline flex-grow-0 text-no-wrap"
<template v-if="model.slotTags.length == 1"> :class="{
with the tag <code>{{ model.slotTags[0] }}</code>, 'error--text': isDisabled(libraryNode) &&
</template> libraryNode._disabledByQuantityFilled
<template v-else-if="model.slotTags.length > 1"> }"
with the following tags:
<span
v-for="(tag, index) in model.slotTags"
:key="index"
> >
<code>{{ tag }}</code>, {{ libraryNode.slotQuantityFilled }} slots
</span> </div>
<template v-if="open">
<v-btn
icon
class="flex-grow-0"
@click.stop="openPropertyDetails(libraryNode._id)"
>
<v-icon>mdi-window-restore</v-icon>
</v-btn>
</template>
</template> </template>
<span v-if="model.spaceLeft"> </v-expansion-panel-header>
that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }} <v-expansion-panel-content>
</span> <library-node-expansion-content :model="libraryNode" />
</p> </v-expansion-panel-content>
</v-expansion-panel>
</template>
</v-expansion-panels>
<v-layout
v-if="(!$subReady.slotFillers && !searchValue) || currentLimit < countAll"
column
align-center
justify-center
class="ma-3"
>
<v-btn
:loading="!$subReady.slotFillers"
color="accent"
@click="loadMore"
>
Load More
</v-btn>
</v-layout>
<template v-if="!showDisabled && disabledNodeCount">
<v-layout
column
align-center
justify-center
class="ma-3"
>
<div>
Requirements of {{ disabledNodeCount }} properties were not met
</div> </div>
</v-fade-transition> <v-btn
<v-fade-transition mode="out-in"> class="mt-2"
<div elevation="0"
v-if="!$subReady.slotFillers" color="accent"
key="character-loading" @click="showDisabled = true"
class="fill-height layout justify-center align-center"
> >
<v-progress-circular Show All
indeterminate </v-btn>
color="primary" </v-layout>
size="64" </template>
/>
</div>
</v-fade-transition>
<v-fade-transition mode="out-in">
<div
v-if="currentLimit < countAll"
class="layout justify-center align-stretch"
>
<v-btn
:loading="!$subReady.slotFillers"
class="primary"
@click="loadMore"
>
Load More
</v-btn>
</div>
</v-fade-transition>
</div>
<template slot="actions"> <template slot="actions">
<v-spacer />
<v-btn <v-btn
text text
@click="$store.dispatch('popDialogStack')" @click="$store.dispatch('popDialogStack')"
> >
Cancel Cancel
</v-btn> </v-btn>
<v-spacer />
<v-btn <v-btn
text text
:disabled="!selectedNode" color="primary"
@click="insert" :disabled="!dummySlot && !selectedNodeIds.length"
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
> >
Insert <template v-if="model.spaceLeft">
{{ totalQuantitySelected }} / {{ model.spaceLeft }}
</template>
<template v-if="slotId">
Insert
</template>
<template v-else>
Close Test
</template>
</v-btn> </v-btn>
</template> </template>
</dialog-base> </dialog-base>
@@ -157,127 +185,201 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue' import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { clone } from 'lodash';
export default { export default {
components: { components: {
DialogBase, DialogBase,
TreeNodeView, TreeNodeView,
PropertyDescription, PropertyDescription,
LibraryNodeExpansionContent,
PropertyTags,
}, },
props:{ props:{
slotId: { slotId: {
type: String, type: String,
required: true, default: undefined,
}, },
creatureId: { creatureId: {
type: String, type: String,
required: true, default: undefined,
},
dummySlot: {
type: Object,
default: undefined,
}, },
}, },
data(){return { data(){return {
selectedNode: undefined, selectedNodeIds: [],
searchInput: undefined,
searchValue: undefined, searchValue: undefined,
numFiltered: 0, showDisabled: false,
disabledNodeCount: undefined,
}}, }},
computed: {
slotPropertyTypeName(){
if (!this.model) return;
if (!this.model.slotType) return 'property';
let propName = getPropertyName(this.model.slotType);
return propName && propName.toLowerCase();
},
},
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['creatureId'], include: ['creatureId'],
}, },
methods:{ computed: {
getTitle(model){ tagsSearched(){
if (!model) return; let or = [];
if (model.name) return model.name; let not = [];
let prop = PROPERTIES[model.type] if (this.model.slotTags && this.model.slotTags.length){
return prop && prop.name; or.push(this.model.slotTags);
}
this.model.extraTags?.forEach(extras => {
if (extras.tags?.length){
if(extras.operation === 'OR'){
or.push(extras.tags);
} else if (extras.operation === 'NOT'){
not.push(extras.tags);
}
}
});
return {or, not};
}, },
searchChanged(val, ack){ slotPropertyTypeName(){
this._subs['slotFillers'].setData('searchTerm', val); if (!this.model) return;
this._subs['slotFillers'].setData('limit', undefined); if (!this.model.slotType) return 'Property';
this.selectedNode = undefined; let propName = getPropertyName(this.model.slotType);
this.searchValue = val; return propName;
setTimeout(ack, 200);
}, },
},
methods: {
loadMore(){ loadMore(){
if (this.currentLimit >= this.countAll) return; if (this.currentLimit >= this.countAll) return;
this._subs['slotFillers'].setData('limit', this.currentLimit + 20); this._subs['slotFillers'].setData('limit', this.currentLimit + 50);
},
openPropertyDetails(id){
this.$store.commit('pushDialogStack', {
component: 'library-node-dialog',
elementId: id,
data: {
_id: id,
},
});
},
isDisabled(node){
return node._disabledBySlotFillerCondition ||
node._disabledByAlreadyAdded ||
(
node._disabledByQuantityFilled &&
!this.selectedNodeIds.includes(node._id)
)
}, },
insert(){
if (!this.selectedNode) return;
this.$store.dispatch('popDialogStack', this.selectedNode);
}
}, },
meteor: { meteor: {
$subscribe: { $subscribe: {
'slotFillers'(){ 'slotFillers'(){
return [this.slotId] return [this.slotId, this.searchValue || undefined]
}, },
}, },
searchLoading(){
return !!this.searchValue && !this.$subReady.slotFillers;
},
model(){ model(){
return CreatureProperties.findOne(this.slotId); if (this.slotId){
return CreatureProperties.findOne(this.slotId);
} else if (this.dummySlot) {
let model = clone(this.dummySlot)
model.quantityExpectedResult = +model.quantityExpected;
model.spaceLeft = model.quantityExpectedResult;
return model;
}
}, },
creature(){ creature(){
if (!this.creatureId) return {variables: {}};
return Creatures.findOne(this.creatureId); return Creatures.findOne(this.creatureId);
}, },
currentLimit(){ currentLimit(){
return this._subs['slotFillers'].data('limit') || 20; return this._subs['slotFillers'].data('limit') || 50;
}, },
countAll(){ countAll(){
return this._subs['slotFillers'].data('countAll'); return this._subs['slotFillers'].data('countAll');
}, },
libraryNodes(){ alreadyAdded(){
let filter = { let added = new Set();
if (!this.model.unique) return added;
let ancestorId;
if (this.model.unique === 'uniqueInSlot'){
ancestorId = this.model._id;
} else if (this.model.unique === 'uniqueInCreature'){
ancestorId = this.creatureId;
}
CreatureProperties.find({
'ancestors.id': ancestorId,
libraryNodeId: {$exists: true},
removed: {$ne: true}, removed: {$ne: true},
}; }, {
if (this.model.slotTags && this.model.slotTags.length){ fields: {libraryNodeId: 1},
filter.tags = {$all: this.model.slotTags}; }).forEach(prop => {
} added.add(prop.libraryNodeId);
if (this.model.slotType){ });
filter.$or = [{ return added;
type: this.model.slotType },
},{ totalQuantitySelected(){
type: 'slotFiller', let quantitySelected = 0;
slotFillerType: this.model.slotType, LibraryNodes.find({
}]; _id: {$in: this.selectedNodeIds}
} }, {
fields: {slotQuantityFilled: 1},
}).forEach(node => {
if (Number.isFinite(node.slotQuantityFilled)){
quantitySelected += node.slotQuantityFilled;
} else {
quantitySelected += 1;
}
});
return quantitySelected;
},
spaceLeft(){
if (this.model.quantityExpectedResult === 0) return undefined;
return this.model.spaceLeft - this.totalQuantitySelected;
},
libraryNames(){
let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names;
},
libraryNodes(){
let filter = getSlotFillFilter({slot: this.model});
let nodes = LibraryNodes.find(filter, { let nodes = LibraryNodes.find(filter, {
sort: {name: 1, order: 1} sort: {name: 1, order: 1}
}).fetch(); }).fetch();
let totalNodes = nodes.length; let disabledNodeCount = 0;
// Filter out slotFillers whose condition isn't met or are too big to fit // Mark slotFillers whose condition isn't met or are too big to fit
// the quantity to fill // the quantity to fill
nodes = nodes.filter(node => { nodes.forEach(node => {
if (node.slotFillerCondition){ if (node.slotFillerCondition){
let {result} = evaluateString({ let {result} = evaluateString({
string: node.slotFillerCondition, string: node.slotFillerCondition,
scope: this.creature.variables, scope: this.creature.variables,
fn: 'reduce', fn: 'reduce',
}); });
if (!result.value) return false; if (!result.value){
node._disabledBySlotFillerCondition = true;
disabledNodeCount += 1;
}
} }
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
if ( if (
node.type === 'slotFiller' && quantityToFill > this.spaceLeft
this.model.spaceLeft > 0 &&
node.slotQuantityFilled > this.model.spaceLeft
){ ){
return false; node._disabledByQuantityFilled = true;
}
if (this.alreadyAdded.has(node._id)){
node._disabledByAlreadyAdded = true;
} }
return true;
}); });
this.numFiltered = totalNodes - nodes.length; this.disabledNodeCount = disabledNodeCount;
if (nodes.length === 1) this.selectedNode = nodes[0];
return nodes; return nodes;
}, },
} }
@@ -285,17 +387,7 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.slot-card { .disabled {
max-width: 500px; opacity: 0.7;
width: 300px;
flex-grow: 1;
flex-shrink: 1;
margin: 4px;
}
.slot-card-text.line-clamp {
-webkit-line-clamp: 5;
}
.slot-card.selected {
background: #8E1B1B;
} }
</style> </style>

View File

@@ -90,10 +90,10 @@ export default {
slotId, slotId,
creatureId, creatureId,
}, },
callback(node){ callback(nodeIds){
if(!node) return; if (!nodeIds || !nodeIds.length) return;
let newPropertyId = insertPropertyFromLibraryNode.call({ let newPropertyId = insertPropertyFromLibraryNode.call({
nodeId: node._id, nodeIds,
parentRef: { parentRef: {
'id': slotId, 'id': slotId,
'collection': 'creatureProperties', 'collection': 'creatureProperties',

View File

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

View File

@@ -1,3 +1,4 @@
import AddCreaturePropertyDialog from '/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue';
import ArchiveDialog from '/imports/ui/creature/archive/ArchiveDialog.vue'; import ArchiveDialog from '/imports/ui/creature/archive/ArchiveDialog.vue';
import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue'; import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue'; import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
@@ -20,9 +21,11 @@ import ShareDialog from '/imports/ui/sharing/ShareDialog.vue';
import SlotDetailsDialog from '/imports/ui/creature/slots/SlotDetailsDialog.vue'; import SlotDetailsDialog from '/imports/ui/creature/slots/SlotDetailsDialog.vue';
import SlotFillDialog from '/imports/ui/creature/slots/SlotFillDialog.vue'; import SlotFillDialog from '/imports/ui/creature/slots/SlotFillDialog.vue';
import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue'; import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
import TransferOwnershipDialog from '/imports/ui/sharing/TransferOwnershipDialog.vue';
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue'; import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
export default { export default {
AddCreaturePropertyDialog,
ArchiveDialog, ArchiveDialog,
CastSpellWithSlotDialog, CastSpellWithSlotDialog,
CreatureFormDialog, CreatureFormDialog,
@@ -45,5 +48,6 @@ export default {
SlotDetailsDialog, SlotDetailsDialog,
SlotFillDialog, SlotFillDialog,
TierTooLowDialog, TierTooLowDialog,
TransferOwnershipDialog,
UsernameDialog, UsernameDialog,
}; };

View File

@@ -22,17 +22,17 @@ const dialogStackStore = {
}); });
updateHistory(); updateHistory();
}, },
replaceDialog(state, {component, data, elementId, callback}){ replaceDialog(state, {component, data}){
const _id = Random.id();
if (!state.dialogs.length){ if (!state.dialogs.length){
throw new Meteor.Error('can\'t replace dialog if no dialogs are open'); throw new Meteor.Error('can\'t replace dialog if no dialogs are open');
} }
let currentDialog = state.dialogs[state.dialogs.length - 1]
Vue.set(state.dialogs, state.dialogs.length - 1, { Vue.set(state.dialogs, state.dialogs.length - 1, {
_id, _id: currentDialog._id,
component, component,
data, data,
elementId, elementId: currentDialog.elementId,
callback, callback: currentDialog.callback,
}); });
}, },
popDialogStackMutation (state, result){ popDialogStackMutation (state, result){

View File

@@ -66,9 +66,10 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue'; import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
const characterTransform = function(char){ const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`; char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
char.initial = char.name && char.name[0] || '?'; char.initial = char.name && char.name[0] || '?';
return char; return char;
}; };

View File

@@ -25,13 +25,20 @@
class="mx-3" class="mx-3"
style="flex-grow: 0; height: 32px;" style="flex-grow: 0; height: 32px;"
/> />
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
<insert-library-node-button <insert-library-node-button
v-if="libraryId" v-if="libraryId && canEditLibrary"
style="bottom: -32px" slot="extension"
style="bottom: -24px"
fab fab
:library-id="libraryId" :library-id="libraryId"
:selected-node-id="selected" :selected-node-id="selectedNodeId"
@selected="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}" @selected="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
/> />
</v-toolbar> </v-toolbar>
<div <div
@@ -41,8 +48,9 @@
<library-contents-container <library-contents-container
:library-id="libraryId" :library-id="libraryId"
:organize-mode="organize" :organize-mode="organize"
:selected-node-id="selected" :selected-node="selectedNode"
should-subscribe should-subscribe
:filter="filter"
@selected="clickNode" @selected="clickNode"
/> />
</div> </div>
@@ -50,8 +58,9 @@
v-else v-else
edit-mode edit-mode
:organize-mode="organize" :organize-mode="organize"
:selected-node-id="selected" :selected-node="selectedNode"
style="overflow-y: auto; padding: 12px;" style="overflow-y: auto; padding: 12px;"
:filter="filter"
@selected="clickNode" @selected="clickNode"
/> />
</div> </div>
@@ -61,10 +70,10 @@
style="overflow: hidden;" style="overflow: hidden;"
> >
<library-node-dialog <library-node-dialog
:_id="selected" :_id="selectedNodeId"
embedded embedded
@removed="selected = undefined" @removed="selectedNodeId = undefined"
@duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}" @duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
/> />
</div> </div>
</tree-detail-layout> </tree-detail-layout>
@@ -82,6 +91,7 @@ import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
export default { export default {
components: { components: {
@@ -90,6 +100,7 @@ export default {
LibraryNodeDialog, LibraryNodeDialog,
LibraryContentsContainer, LibraryContentsContainer,
InsertLibraryNodeButton, InsertLibraryNodeButton,
TreeSearchInput,
}, },
props: { props: {
selection: Boolean, selection: Boolean,
@@ -100,7 +111,8 @@ export default {
}, },
data(){ return { data(){ return {
organize: false, organize: false,
selected: undefined, selectedNodeId: undefined,
filter: undefined,
};}, };},
computed: { computed: {
isToolbarDark(){ isToolbarDark(){
@@ -120,12 +132,12 @@ export default {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'library-node-edit-dialog', component: 'library-node-edit-dialog',
elementId: 'selected-node-card', elementId: 'selected-node-card',
data: {_id: this.selected}, data: {_id: this.selectedNodeId},
}); });
}, },
clickNode(id){ clickNode(id){
if (this.$vuetify.breakpoint.mdAndUp){ if (this.$vuetify.breakpoint.mdAndUp){
this.selected = id; this.selectedNodeId = id;
} else { } else {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'library-node-dialog', component: 'library-node-dialog',
@@ -136,7 +148,7 @@ export default {
}, },
callback: result => { callback: result => {
if (result){ if (result){
this.selected = id; this.selectedNodeId = id;
} }
}, },
}); });
@@ -175,7 +187,7 @@ export default {
}, },
selectedNode(){ selectedNode(){
return LibraryNodes.findOne({ return LibraryNodes.findOne({
_id: this.selected, _id: this.selectedNodeId,
removed: {$ne: true} removed: {$ne: true}
}); });
}, },

View File

@@ -28,8 +28,9 @@
class="ma-2" class="ma-2"
> >
<insert-library-node-button <insert-library-node-button
v-if="editPermission(library)"
:library-id="library._id" :library-id="library._id"
:selected-node-id="selectedNodeId" :selected-node-id="selectedNode && selectedNode._id"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
<v-btn <v-btn
@@ -45,7 +46,8 @@
:library-id="library._id" :library-id="library._id"
:organize-mode="organizeMode && editPermission(library)" :organize-mode="organizeMode && editPermission(library)"
:edit-mode="editMode" :edit-mode="editMode"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
:filter="filter"
should-subscribe should-subscribe
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
@@ -82,8 +84,12 @@ export default {
props: { props: {
organizeMode: Boolean, organizeMode: Boolean,
editMode: Boolean, editMode: Boolean,
selectedNodeId: { selectedNode: {
type: String, type: Object,
default: undefined,
},
filter: {
type: Object,
default: undefined, default: undefined,
}, },
}, },

View File

@@ -7,7 +7,7 @@
group="library" group="library"
:children="libraryChildren" :children="libraryChildren"
:organize="organizeMode" :organize="organizeMode"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@reordered="reordered" @reordered="reordered"
@reorganized="reorganized" @reorganized="reorganized"
@@ -29,7 +29,7 @@
<script lang="js"> <script lang="js">
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js' import nodesToTree from '/imports/api/parenting/nodesToTree.js';
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js'; import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
@@ -38,10 +38,20 @@
TreeNodeList, TreeNodeList,
}, },
props: { props: {
libraryId: String, libraryId: {
type: String,
default: undefined,
},
organizeMode: Boolean, organizeMode: Boolean,
selectedNodeId: String, selectedNode: {
type: Object,
default: undefined,
},
shouldSubscribe: Boolean, shouldSubscribe: Boolean,
filter: {
type: Object,
default: undefined,
},
}, },
data(){return { data(){return {
slowShouldSubscribe: this.shouldSubscribe, slowShouldSubscribe: this.shouldSubscribe,
@@ -76,7 +86,13 @@
}, },
libraryChildren(){ libraryChildren(){
if (!this.library) return; if (!this.library) return;
return nodesToTree({collection: LibraryNodes, ancestorId: this.library._id}); return nodesToTree({
collection: LibraryNodes,
ancestorId: this.library._id,
filter: this.filter,
includeFilteredDocAncestors: true,
includeFilteredDocDescendants: true,
});
}, },
}, },
methods: { methods: {

View File

@@ -0,0 +1,62 @@
<template lang="html">
<div>
<component
:is="model.type"
:model="model"
class="property-viewer"
/>
<tree-node-list
v-if="$subReady.descendantLibraryNodes"
group="library-node-expansion"
:children="propertyChildren"
@selected="clickChild"
/>
</div>
</template>
<script lang="js">
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
export default {
components: {
TreeNodeList,
...propertyViewerIndex,
},
props: {
model: {
type: Object,
required: true,
},
},
methods: {
clickChild(id){
this.$store.commit('pushDialogStack', {
component: 'library-node-dialog',
elementId: `tree-node-${id}`,
data: {
_id: id,
},
});
},
},
meteor: {
$subscribe: {
descendantLibraryNodes(){
return [this.model._id];
},
},
propertyChildren(){
return nodesToTree({
collection: LibraryNodes,
ancestorId: this.model._id
});
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -53,15 +53,16 @@ export default {
subscribed(){ subscribed(){
let libraryId = this.$route.params.id; let libraryId = this.$route.params.id;
let user = Meteor.user(); let user = Meteor.user();
if (!user) return false; return user?.subscribedLibraries?.includes(libraryId);
let subs = user.subscribedLibraries;
return subs && subs.includes(libraryId);
}, },
showSubscribeButton(){ showSubscribeButton(){
let userId = Meteor.userId(); let user = Meteor.user();
let library = this.library; let library = this.library;
if (!library) return; if (!user || !library) return;
if ( let userId = user._id;
if (user.subscribedLibraries.includes(library._id)){
return true
} else if (
library.readers.includes(userId) || library.readers.includes(userId) ||
library.writers.includes(userId) || library.writers.includes(userId) ||
library.owner === userId library.owner === userId

View File

@@ -82,9 +82,10 @@
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js'; import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue'; import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue'; import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
const characterTransform = function(char){ const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`; char.url = `/character/${char._id}/${getCreatureUrlName(char)}`;
char.initial = char.name && char.name[0] || '?'; char.initial = char.name && char.name[0] || '?';
return char; return char;
}; };

Some files were not shown because too many files have changed in this diff Show More