Compare commits

...

26 Commits

Author SHA1 Message Date
Stefan Zermatten
6e51df363b Added increment buttons to all attributes with a value 2021-08-27 13:21:08 +02:00
Stefan Zermatten
608ab4e588 Added undo delete and recycle bin to library 2021-08-27 13:00:01 +02:00
Stefan Zermatten
518880fa5c Fixed error where searching for properties to insert while having other properties selected could prevent any insert from happening at all 2021-08-27 12:24:01 +02:00
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
85 changed files with 1305 additions and 612 deletions

View File

@@ -9,6 +9,8 @@ import { recomputeCreatureByDoc } from '/imports/api/creature/computation/method
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.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({
name: 'creatureProperties.castSpellWithSlot',
@@ -64,16 +66,24 @@ const castSpellWithSlot = new ValidatedMethod({
}
let actionContext = getAncestorContext(spell);
doActionWork({
doActionWork({
action: spell,
actionContext: {slotLevel, ...actionContext},
creature,
targets: target ? [target] : [],
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){
recomputeInventory(target._id);
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
}
},

View File

@@ -1,4 +1,5 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureFolders = new Mongo.Collection('creatureFolders');
@@ -7,6 +8,7 @@ let creatureFolderSchema = new SimpleSchema({
type: String,
trim: false,
optional: true,
max: STORAGE_LIMITS.name,
},
creatures: {
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 propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
@@ -16,9 +17,11 @@ let CreaturePropertySchema = new SimpleSchema({
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
disabled: {
type: Boolean,
@@ -27,6 +30,13 @@ let CreaturePropertySchema = new SimpleSchema({
icon: {
type: storedIconsSchema,
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
// 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

@@ -93,7 +93,15 @@ function insertPropertyFromNode(nodeId, ancestors, order){
_id: nodeId,
removed: {$ne: true},
});
if (!node) throw `Node not found for nodeId: ${nodeId}`;
if (!node) {
if (Meteor.isClient) return;
else {
throw new Meteor.Error(
'Insert property from library failed',
`No library document with id '${nodeId}' was found`
);
}
}
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
@@ -107,6 +115,9 @@ function insertPropertyFromNode(nodeId, ancestors, order){
// 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,
@@ -135,6 +146,13 @@ function insertPropertyFromNode(nodeId, ancestors, order){
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
node.libraryNodeId = node._id;
});
}
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){

View File

@@ -4,6 +4,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
@@ -19,9 +20,26 @@ const pushToProperty = new ValidatedMethod({
let rootCreature = getRootCreatureAncestor(property);
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
CreatureProperties.update(_id, {
$push: {[path.join('.')]: value},
$push: {[joinedPath]: value},
}, {
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 ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
//set up the collection for creatures
let Creatures = new Mongo.Collection('creatures');
@@ -47,7 +48,7 @@ let CreatureSettingsSchema = new SimpleSchema({
discordWebhook: {
type: String,
optional: true,
max: 200,
max: STORAGE_LIMITS.url,
},
});
@@ -57,22 +58,27 @@ let CreatureSchema = new SimpleSchema({
type: String,
defaultValue: '',
optional: true,
max: STORAGE_LIMITS.name,
},
alignment: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.name,
},
gender: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true
optional: true,
max: STORAGE_LIMITS.url,
},
avatarPicture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
// Mechanics
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/removeCreature.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';

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,10 +1,10 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
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 Creatures from '/imports/api/creature/creatures/Creatures.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');
@@ -12,6 +12,7 @@ let ExperienceSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The amount of XP this experience gives
xp: {
@@ -90,11 +91,6 @@ const insertExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.insert.denied',
'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 = [];
creatureIds.forEach(creatureId => {
let id = insertExperienceForCreature({experience, creatureId, userId});
@@ -123,11 +119,6 @@ const removeExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.remove.denied',
'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);
if (!experience) return;
let creatureId = experience.creatureId
@@ -168,11 +159,6 @@ const recomputeExperiences = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.recompute.denied',
'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);
let xp = 0;

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Icons = new Mongo.Collection('icons');
@@ -9,22 +10,27 @@ let iconsSchema = new SimpleSchema({
name: {
type: String,
unique: true,
max: STORAGE_LIMITS.name,
index: 1,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
tags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
index: 1,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
shape: {
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 LibraryNodes from '/imports/api/library/LibraryNodes.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
@@ -21,11 +22,8 @@ let Libraries = new Mongo.Collection('libraries');
let LibrarySchema = new SimpleSchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
},
isDefault: {
type: Boolean,
optional: true,
},
});
LibrarySchema.extend(SharingSchema);
@@ -39,7 +37,7 @@ const insertLibrary = new ValidatedMethod({
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner', 'isDefault'),
schema: LibrarySchema.omit('owner'),
run(library) {
if (!this.userId) {
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({
name: 'libraries.remove',
validate: new SimpleSchema({
@@ -128,4 +102,4 @@ export function removeLibaryWork(libraryId){
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };
export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };

View File

@@ -13,6 +13,8 @@ import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import { restore } from '/imports/api/parenting/softRemove.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -24,13 +26,16 @@ let LibraryNodeSchema = new SimpleSchema({
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
icon: {
type: storedIconsSchema,
optional: true,
max: STORAGE_LIMITS.icon,
}
});
@@ -182,6 +187,25 @@ const softRemoveLibraryNode = new ValidatedMethod({
}
});
const restoreLibraryNode = new ValidatedMethod({
name: 'libraryNodes.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
// Permissions
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
// Do work
restore({_id, collection: LibraryNodes});
}
});
export default LibraryNodes;
export {
LibraryNodeSchema,
@@ -190,4 +214,5 @@ export {
pullFromLibraryNode,
pushToLibraryNode,
softRemoveLibraryNode,
restoreLibraryNode,
};

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
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
@@ -9,10 +10,12 @@ let DamageMultiplierSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
damageTypes: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.damageTypeCount,
},
// The technical, lowercase, single-word name used in formulae
'damageTypes.$': {
@@ -29,17 +32,21 @@ let DamageMultiplierSchema = new SimpleSchema({
excludeTags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'excludeTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// Tags which must be present to be affected by this multiplier (AND)
includeTags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'includeTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
});

View File

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

View File

@@ -1,5 +1,7 @@
import SimpleSchema from 'simpl-schema';
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
* that modify their final value or presentation in some way
@@ -8,6 +10,7 @@ let EffectSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
operation: {
type: String,
@@ -30,14 +33,17 @@ let EffectSchema = new SimpleSchema({
calculation: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
//which stats the effect is applied to
stats: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
},
'stats.$': {
type: String,
max: STORAGE_LIMITS.variableName,
},
});
@@ -51,6 +57,7 @@ const ComputedOnlyEffectSchema = new SimpleSchema({
errors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'errors.$':{
type: ErrorSchema,

View File

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

View File

@@ -1,10 +1,12 @@
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
// like 'race' and 'background'
let FolderSchema = new SimpleSchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
},
});

View File

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

View File

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

View File

@@ -1,17 +1,21 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ProficiencySchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The variableNames of the skills, tags, or attributes to apply proficiency to
stats: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
},
'stats.$': {
type: String,
max: STORAGE_LIMITS.variableName,
},
// A number representing how proficient the character is
// 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 STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ReferenceSchema = new SimpleSchema({
ref: {
@@ -13,6 +14,7 @@ let ReferenceSchema = new SimpleSchema({
'ref.collection': {
type: String,
optional: true,
max: STORAGE_LIMITS.collectionName,
},
// Denormalised store of referenced property's details
cache: {
@@ -22,6 +24,7 @@ let ReferenceSchema = new SimpleSchema({
'cache.error': {
type: String,
optional: true,
max: STORAGE_LIMITS.errorMessage,
},
'cache.node': {
type: Object,
@@ -30,9 +33,11 @@ let ReferenceSchema = new SimpleSchema({
'cache.node.name': {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
'cache.node.type': {
type: String,
max: STORAGE_LIMITS.variableName,
},
'cache.node.level': {
type: Number,
@@ -49,6 +54,7 @@ let ReferenceSchema = new SimpleSchema({
'cache.library.name': {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
});

View File

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

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
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
// For the saving throw bonus or proficiency, see ./Skills.js
@@ -7,11 +8,13 @@ let SavingThrowSchema = new SimpleSchema ({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The computed DC
dc: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
// Who this saving throw applies to
target: {
@@ -27,6 +30,7 @@ let SavingThrowSchema = new SimpleSchema ({
stat: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
});
@@ -38,6 +42,7 @@ const ComputedOnlySavingThrowSchema = new SimpleSchema({
dcErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'dcErrors.$':{
type: ErrorSchema,

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.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
@@ -10,6 +11,7 @@ let SkillSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// The technical, lowercase, single-word name used in formulae
// Ignored for skilltype = save
@@ -17,11 +19,13 @@ let SkillSchema = new SimpleSchema({
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// The variable name of the ability this skill relies on
ability: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// What type of skill is this
skillType: {
@@ -42,6 +46,7 @@ let SkillSchema = new SimpleSchema({
baseValueCalculation: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
// The base proficiency of this skill
baseProficiency: {
@@ -52,6 +57,7 @@ let SkillSchema = new SimpleSchema({
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
});
@@ -69,6 +75,7 @@ let ComputedOnlySkillSchema = new SimpleSchema({
baseValueErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'baseValueErrors.$': {
type: ErrorSchema,
@@ -107,6 +114,7 @@ let ComputedOnlySkillSchema = new SimpleSchema({
rollBonuses: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.rollBonusCount,
},
'rollBonuses.$': {
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
// other properties
import SimpleSchema from 'simpl-schema';
let SlotFillerSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
picture: {
type: String,
optional: true,
max: STORAGE_LIMITS.url,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// Overrides the type when searching for properties
slotFillerType: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// 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
@@ -32,6 +36,7 @@ let SlotFillerSchema = new SimpleSchema({
slotFillerCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
});

View File

@@ -1,30 +1,66 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SlotSchema = new SimpleSchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
slotType: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
slotTags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'slotTags.$': {
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: {
type: String,
optional: true,
defaultValue: '1',
max: STORAGE_LIMITS.calculation,
},
ignored: {
type: Boolean,
@@ -33,11 +69,24 @@ let SlotSchema = new SimpleSchema({
slotCondition: {
type: String,
optional: true,
max: STORAGE_LIMITS.calculation,
},
hideWhenFull: {
type: Boolean,
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({
@@ -49,6 +98,7 @@ const ComputedOnlySlotSchema = new SimpleSchema({
slotConditionErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'slotConditionErrors.$':{
type: ErrorSchema,
@@ -62,6 +112,7 @@ const ComputedOnlySlotSchema = new SimpleSchema({
quantityExpectedErrors: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.errorCount,
},
'quantityExpectedErrors.$':{
type: ErrorSchema,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import '/imports/api/sharing/sharing.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SharingSchema = new SimpleSchema({
owner: {
@@ -11,9 +12,9 @@ let SharingSchema = new SimpleSchema({
type: Array,
defaultValue: [],
index: 1,
max: 50,
maxCount: STORAGE_LIMITS.readersCount,
},
"readers.$": {
'readers.$': {
type: String,
regEx: SimpleSchema.RegEx.Id
},
@@ -21,9 +22,9 @@ let SharingSchema = new SimpleSchema({
type: Array,
defaultValue: [],
index: 1,
max: 20,
maxCount: STORAGE_LIMITS.writersCount,
},
"writers.$": {
'writers.$': {
type: String,
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 { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
const setPublic = new ValidatedMethod({
name: 'sharing.setPublic',
@@ -47,7 +48,7 @@ const updateUserSharePermissions = new ValidatedMethod({
run({docRef, userId, role}){
let doc = fetchDocByRef(docRef);
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){
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

@@ -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 ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import roll from '/imports/parser/roll.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
export default class RollNode extends ParseNode {
constructor({left, right}) {
@@ -42,9 +43,9 @@ export default class RollNode extends ParseNode {
if (context.doubleRolls){
number *= 2;
}
if (number > 100) return new ErrorNode({
if (number > STORAGE_LIMITS.diceRollValuesCount) return new ErrorNode({
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,
});
let diceSize = right.value;

View File

@@ -10,17 +10,17 @@ Meteor.startup(() => {
];
/**
* Deletes all soft removed documents that were removed more than 30 minutes ago
* Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function(){
const now = new Date();
const thirtyMinutesAgo = new Date(now.getTime() - 30*60000);
const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
removedAt: {$lt: yesterday} // dates *before* yesterday
}, function(error){
if (error){
console.error(JSON.stringify(error, null, 2));

View File

@@ -69,6 +69,28 @@ Meteor.publish('libraryNodes', function(libraryId){
});
});
Meteor.publish('softRemovedLibraryNodes', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});
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.0.id': libraryId,
removed: true,
removedWith: {$exists: false},
}, {
sort: {order: 1},
}),
];
});
});
Meteor.publish('descendantLibraryNodes', function(nodeId){
let node = LibraryNodes.findOne(nodeId);
let libraryId = node?.ancestors[0]?.id;

View File

@@ -1,6 +1,42 @@
import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
console.log('attempting selectedLibraryNodes')
check(selectedNodeIds, Array);
// Limit to 20 selected nodes
if (selectedNodeIds.length > 20){
selectedNodeIds = selectedNodeIds.slice(0, 20);
}
let libraryViewPermissions = {};
// Check view permissions of all libraries
for (let id of selectedNodeIds){
let node = LibraryNodes.findOne(id);
if (!node) continue;
let libraryId = node.ancestors[0].id;
if (libraryViewPermissions[id]){
continue;
} else {
let library = Libraries.findOne(libraryId, {fields: {
owner: 1,
readers: 1,
writers: 1,
public: 1,
}});
assertViewPermission(library, this.userId);
libraryViewPermissions[id] = true;
}
}
// Return all nodes and their children
return [LibraryNodes.find({
$or: [
{_id: {$in: selectedNodeIds}},
{'ancestors.id': {$in: selectedNodeIds}},
],
})];
});
Meteor.publish('searchLibraryNodes', function(){
let self = this;

View File

@@ -2,8 +2,11 @@ import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.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;
this.autorun(function (){
let userId = this.userId;
@@ -21,7 +24,7 @@ Meteor.publish('slotFillers', function(slotId){
fields: {subscribedLibraries: 1}
});
const subs = user && user.subscribedLibraries || [];
let libraryIds = Libraries.find({
let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
@@ -29,45 +32,29 @@ Meteor.publish('slotFillers', function(slotId){
{_id: {$in: subs}},
]
}, {
fields: {_id: 1},
}).map(lib => lib._id);
fields: {_id: 1, name: 1},
});
let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the slot
let filter = {
'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,
}];
}
let filter = getSlotFillFilter({slot, libraryIds});
this.autorun(function(){
// 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);
// 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' }
_score: { $meta: 'textScore' }
},
sort: {
// `score` property specified in the projection fields above.
score: { $meta: 'textScore' },
_score: { $meta: 'textScore' },
name: 1,
order: 1,
}
@@ -85,7 +72,8 @@ Meteor.publish('slotFillers', function(slotId){
self.setData('countAll', LibraryNodes.find(filter).count());
});
self.autorun(function () {
return LibraryNodes.find(filter, options);
Meteor._sleepForMs(1000);
return [LibraryNodes.find(filter, options), libraries];
});
});
});

View File

@@ -79,17 +79,18 @@
},
data() {
return {
editValue: 0,
editValue: this.value,
operation: 'set',
hover: false,
};
},
watch: {
open(newValue){
if (newValue){
this.resetData();
}
}
open: {
immediate: true,
handler(isOpen) {
if (isOpen) this.resetData();
},
},
},
methods: {
resetData(){

View File

@@ -179,7 +179,7 @@
opacity: 0.4;
}
.found {
background: rgba(200, 0, 0, 0.1);
background: rgba(200, 0, 0, 0.1) !important;
}
.ghost {
opacity: 0.5;

View File

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

View File

@@ -31,22 +31,11 @@
:key="type"
color="primary"
:data-id="`insert-creature-property-type-${type}`"
:label="type ? 'New ' + properties[type].name : 'New Property'"
:label="getPropertyLabel(type)"
:icon="type ? properties[type].icon : 'mdi-plus'"
:disabled="!editPermission"
@click="addProperty(type)"
/>
<template v-if="tabNumber === 5">
<labeled-fab
key="add-property"
color="primary"
data-id="add-creature-property-btn"
label="Add Property"
icon="mdi-plus"
:disabled="!editPermission"
@click="addProperty"
/>
</template>
</v-speed-dial>
</template>
@@ -55,6 +44,7 @@
import { getHighestOrder } from '/imports/api/parenting/order.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.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 insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
@@ -130,13 +120,17 @@
return this.$route.params.id;
},
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(){
return this.speedDialsByTab[tabs[this.tabNumber]];
},
speedDialsByTab() { return {
'stats': ['attribute', 'skill', 'action', 'attack'],
'stats': ['attribute', 'skill', 'action', 'attack', 'buff'],
'features': ['feature'],
'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'],
@@ -147,7 +141,17 @@
return PROPERTIES;
},
},
meteor: {
hideSpellsTab(){
let creature = Creatures.findOne(this.creatureId);
return creature?.settings?.hideSpellsTab;
},
},
methods: {
getPropertyLabel(type){
if (type === 'buff') return 'Buff or Condition';
return type ? PROPERTIES[type].name : 'Property'
},
addProperty(forcedType){
let creatureId = this.creatureId;
let fab = hideFab();

View File

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

View File

@@ -298,6 +298,9 @@
meteor: {
'$subscribe':{
'searchLibraryNodes': [],
'selectedLibraryNodes'(){
return [this.selectedNodeIds];
},
},
showPropertyHelp(){
let user = Meteor.user();

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import ShareDialog from '/imports/ui/sharing/ShareDialog.vue';
import SlotDetailsDialog from '/imports/ui/creature/slots/SlotDetailsDialog.vue';
import SlotFillDialog from '/imports/ui/creature/slots/SlotFillDialog.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';
export default {
@@ -47,5 +48,6 @@ export default {
SlotDetailsDialog,
SlotFillDialog,
TierTooLowDialog,
TransferOwnershipDialog,
UsernameDialog,
};

View File

@@ -66,9 +66,10 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
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] || '?';
return char;
};

View File

@@ -27,6 +27,35 @@
@change="updateName"
/>
</template>
<template v-if="removedDocs.length">
<h3>Recently Deleted Properties</h3>
<v-list>
<v-list-item
v-for="model in removedDocs"
:key="model._id"
>
<v-list-item-content>
<v-list-item-title>
<tree-node-view :model="model" />
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn
color="accent"
text
@click="restore(model._id)"
>
Restore
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
</template>
<v-progress-circular
v-if="!$subReady.softRemovedLibraryNodes"
indeterminate
color="primary"
/>
<template slot="actions">
<v-spacer />
<v-btn
@@ -43,10 +72,13 @@
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Libraries, { updateLibraryName, removeLibrary } from '/imports/api/library/Libraries.js';
import LibraryNodes, { restoreLibraryNode } from '/imports/api/library/LibraryNodes.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
export default {
components: {
DialogBase,
TreeNodeView,
},
props: {
_id: String,
@@ -90,11 +122,28 @@ export default {
},
});
},
restore(_id){
restoreLibraryNode.call({_id});
},
},
meteor: {
'$subscribe':{
softRemovedLibraryNodes(){
return [this._id];
},
},
model(){
return Libraries.findOne(this._id);
},
removedDocs(){
return LibraryNodes.find({
'ancestors.0.id': this._id,
removed: true,
removedWith: {$exists: false},
}, {
sort: {order: 1},
});
}
}
}
</script>

View File

@@ -75,6 +75,7 @@
pushToLibraryNode,
pullFromLibraryNode,
softRemoveLibraryNode,
restoreLibraryNode,
} from '/imports/api/library/LibraryNodes.js';
import duplicateLibraryNode from '/imports/api/library/methods/duplicateLibraryNode.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
@@ -86,6 +87,8 @@
import { get } from 'lodash';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -212,12 +215,20 @@
});
},
remove(){
softRemoveLibraryNode.call({_id: this.currentId});
let _id = this.currentId;
softRemoveLibraryNode.call({_id});
if (this.embedded){
this.$emit('removed');
} else {
this.$store.dispatch('popDialogStack');
}
snackbar({
text: `Deleted ${getPropertyTitle(this.model)}`,
callbackName: 'undo',
callback(){
restoreLibraryNode.call({_id});
},
});
},
}
};

View File

@@ -6,6 +6,7 @@
class="property-viewer"
/>
<tree-node-list
v-if="$subReady.descendantLibraryNodes"
group="library-node-expansion"
:children="propertyChildren"
@selected="clickChild"

View File

@@ -82,9 +82,10 @@
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
import getCreatureUrlName from '/imports/api/creature/creatures/getCreatureUrlName.js';
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] || '?';
return char;
};

View File

@@ -80,12 +80,6 @@ export default {
</script>
<style lang="css" scoped>
.skill-list-tile >>> .v-list__tile {
height: 34px;
}
.skill-list-tile{
background: inherit;
}
.prof-icon {
min-width: 30px;
}
@@ -95,7 +89,3 @@ export default {
text-align: center;
}
</style>
<style lang="scss">
$list-item-min-height: 32px;
</style>

View File

@@ -7,6 +7,25 @@
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<smart-switch
label="Applied"
class="mt-0"
:value="model.applied"
:error-messages="errors.applied"
@change="change('applied', ...arguments)"
/>
<v-expand-transition>
<div v-if="model.applied">
<v-alert
type="info"
outlined
>
When buffs are applied they become active on a creature.
Turn this off if the buff needs to be applied to a target by an action
or spell.
</v-alert>
</div>
</v-expand-transition>
<text-area
label="Description"
:value="model.description"
@@ -24,15 +43,18 @@
@change="change('duration', ...arguments)"
/>
-->
<smart-select
label="Target"
:hint="targetOptionHint"
:items="targetOptions"
:value="model.target"
:error-messages="errors.target"
:menu-props="{auto: true, lazy: true}"
@change="change('target', ...arguments)"
/>
<v-expand-transition>
<smart-select
v-if="!model.applied"
label="Target"
:hint="targetOptionHint"
:items="targetOptions"
:value="model.target"
:error-messages="errors.target"
:menu-props="{auto: true, lazy: true}"
@change="change('target', ...arguments)"
/>
</v-expand-transition>
<smart-combobox
label="Tags"
multiple

View File

@@ -17,16 +17,63 @@
:error-messages="errors.slotType"
@change="change('slotType', ...arguments)"
/>
<smart-combobox
label="Tags Required"
hint="The slot must be filled with a property which has all the listed tags"
multiple
chips
deletable-chips
:value="model.slotTags"
:error-messages="errors.slotTags"
@change="change('slotTags', ...arguments)"
/>
<v-layout align-center>
<v-btn
icon
style="margin-top: -30px;"
class="mr-2"
:loading="addExtraTagsLoading"
:disabled="extraTagsFull"
@click="addExtraTags"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
<smart-combobox
label="Tags Required"
hint="The slot must be filled with a property which has all the listed tags"
multiple
chips
deletable-chips
:value="model.slotTags"
:error-messages="errors.slotTags"
@change="change('slotTags', ...arguments)"
/>
</v-layout>
<v-slide-x-transition group>
<div
v-for="(extras, i) in model.extraTags"
:key="extras._id"
class="extra-tags layout align-center justify-space-between"
>
<smart-select
label="Operation"
style="width: 90px; flex-grow: 0;"
:items="extraTagOperations"
:value="extras.operation"
:error-messages="errors.extraTags && errors.extraTags[i]"
@change="change(['extraTags', i, 'operation'], ...arguments)"
/>
<smart-combobox
label="Tags"
:hint="extras.operation === 'OR' ? 'The slot can be filled with a property that has all of these tags instead' : 'The slot cannot be filled with a property that has any of these tags'"
class="mx-2"
multiple
chips
deletable-chips
:value="extras.tags"
@change="change(['extraTags', i, 'tags'], ...arguments)"
/>
<v-btn
icon
style="margin-top: -30px;"
@click="$emit('pull', {path: ['extraTags', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<text-field
label="Quantity"
hint="How many matching properties must be used to fill this slot, 0 is unlimited"
@@ -45,6 +92,29 @@
/>
<calculation-error-list :errors="model.slotConditionErrors" />
<smart-select
label="Unique"
style="flex-basis: 300px;"
clearable
hint="Do the properties that fill this slot need to be unique?"
:items="uniqueOptions"
:value="model.unique"
:error-messages="errors.unique"
@change="change('unique', ...arguments)"
/>
<v-layout justify-center>
<v-btn
v-if="context.isLibraryForm"
color="accent"
class="ma-2 mb-4"
data-id="test-slot-button"
@click="testSlot"
>
Test Slot
</v-btn>
</v-layout>
<text-area
label="Description"
:value="model.description"
@@ -77,6 +147,7 @@
</div>
<smart-combobox
label="Tags"
hint="This slot's own tags which will be used to fill other slots"
multiple
chips
deletable-chips
@@ -92,6 +163,7 @@
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
export default {
components: {
@@ -99,12 +171,60 @@
CalculationErrorList,
},
mixins: [propertyFormMixin],
inject: {
context: { default: {} }
},
data(){
let slotTypes = [];
for (let key in PROPERTIES){
slotTypes.push({text: PROPERTIES[key].name, value: key});
}
return {slotTypes};
return {
slotTypes,
addExtraTagsLoading: false,
extraTagOperations: ['OR', 'NOT'],
uniqueOptions: [{
text: 'Each property inside this slot should be unique',
value: 'uniqueInSlot',
}, {
text: 'Properties in this slot should be unique accross the whole character',
value: 'uniqueInCreature',
}],
};
},
computed: {
extraTagsFull(){
if (!this.model.extraTags) return false;
let maxCount = SlotSchema.get('extraTags', 'maxCount');
return this.model.extraTags.length >= maxCount;
}
},
methods: {
acknowledgeAddResult(){
this.addExtraTagsLoading = false;
},
addExtraTags(){
this.addExtraTagsLoading = true;
this.$emit('push', {
path: ['extraTags'],
value: {
_id: Random.id(),
operation: 'OR',
tags: [],
},
ack: this.acknowledgeAddResult,
});
},
testSlot(){
if (!this.context.isLibraryForm) return;
this.$store.commit('pushDialogStack', {
component: 'slot-fill-dialog',
elementId: 'test-slot-button',
data: {
dummySlot: this.model,
},
});
}
},
};
</script>

View File

@@ -34,7 +34,7 @@ export default {
title(){
let model = this.model;
if (!model) return;
if (model.quantity !== 1){
if (Number.isFinite(model.quantity) && model.quantity !== 1){
if (model.plural){
return `${model.quantity} ${model.plural}`;
} else if (model.name){

View File

@@ -22,12 +22,12 @@
<script lang="js">
import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeViewMixin.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
export default {
components: {
TreeNodeView,
},
name: 'ReferenceTreeNode',
mixins: [treeNodeViewMixin],
beforeCreate () {
this.$options.components.TreeNodeView = require('/imports/ui/properties/treeNodeViews/TreeNodeView.vue').default
},
}
</script>

View File

@@ -10,6 +10,7 @@
import treeNodeViewIndex from '/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js';
export default {
name: 'TreeNodeView',
components: {
...treeNodeViewIndex
},

View File

@@ -4,19 +4,32 @@
column
align-center
>
<div
v-if="model.value !== undefined"
class="text-h4"
>
<v-layout v-if="model.value !== undefined">
<div
v-if="model.damage !== undefined"
class="text-h4 mr-3"
>
{{ model.value - model.damage }} / {{ model.value }}
<div
v-if="model.damage !== undefined"
>
{{ model.value - model.damage }} / {{ model.value }}
</div>
<div v-else>
{{ model.value }}
</div>
</div>
<div v-else>
{{ model.value }}
</div>
</div>
<increment-button
v-if="context.creatureId"
icon
large
outlined
color="primary"
:value="model.value - (model.damage || 0)"
@change="damageProperty"
>
<v-icon>$vuetify.icons.abacus</v-icon>
</increment-button>
</v-layout>
<div
v-if="model.modifier !== undefined"
class="text-h6"
@@ -68,15 +81,18 @@
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
export default {
components: {
AttributeEffect,
IncrementButton,
},
mixins: [propertyViewerMixin],
inject: {
context: { default: {} }
},
components: {
AttributeEffect,
},
mixins: [propertyViewerMixin],
computed: {
reset(){
let reset = this.model.reset
@@ -96,6 +112,13 @@
elementId: `${id}`,
data: {_id: id},
});
},
damageProperty({type, value}) {
damageProperty.call({
_id: this.model._id,
operation: type,
value: value
});
},
},
meteor: {

View File

@@ -5,7 +5,7 @@
v-if="model.quantity > 1 || model.showIncrement"
class="layout justify-center align-center wrap"
>
<div class="text-h4">
<div class="text-h4 mr-3">
{{ model.quantity }}
</div>
<increment-button

View File

@@ -1,10 +1,22 @@
<template lang="html">
<div
v-if="tagString"
v-if="tags.length"
class="tags"
:class="{'ma-3': !noMargin}"
:class="{'ma-2': !noMargin}"
>
{{ tagString }}
<span
v-if="prefix"
class="mx-1 text-overline"
>
{{ prefix }}
</span>
<v-chip
v-for="(tag, i) in tags"
:key="tag + i"
class="mx-1"
>
{{ tag }}
</v-chip>
</div>
</template>
@@ -16,17 +28,13 @@ export default {
default: () => [],
},
noMargin: Boolean,
prefix: {
type: String,
default: undefined,
}
},
computed:{
tagString(){
return this.tags.join(', ');
},
}
}
</script>
<style lang="css" scoped>
.tags {
font-style: italic;
}
</style>

View File

@@ -130,18 +130,9 @@ RouterFactory.configure(factory => {
meta: {
title: 'Library',
},
},{
path: '/character/:id/:urlName',
components: {
default: CharacterSheetPage,
toolbar: CharacterSheetToolbar,
rightDrawer: CharacterSheetRightDrawer,
},
meta: {
title: 'Character Sheet',
},
},{
path: '/character/:id',
alias: '/character/:id/:urlName',
components: {
default: CharacterSheetPage,
toolbar: CharacterSheetToolbar,

View File

@@ -17,7 +17,7 @@
v-if="model.public && docRef.collection === 'libraries'"
readonly
label="Link"
:value="'https://beta.dicecloud.com' + this.$router.resolve({
:value="'https://beta.dicecloud.com' + $router.resolve({
name: 'singleLibrary',
params: { id: model._id },
}).href"
@@ -56,6 +56,7 @@
<v-menu
bottom
left
:data-id="'menu-' + user._id"
>
<template #activator="{ on }">
<v-btn
@@ -84,6 +85,15 @@
</v-list-item-action>
<v-list-item-title>View only</v-list-item-title>
</v-list-item>
<v-list-item
v-if="user.permission === 'writer'"
@click="makeOwner(user)"
>
<v-list-item-action>
<v-icon>mdi-signature</v-icon>
</v-list-item-action>
<v-list-item-title>Transfer Onwership</v-list-item-title>
</v-list-item>
<v-list-item @click="updateSharing(user._id, 'none')">
<v-list-item-action>
<v-icon>mdi-delete</v-icon>
@@ -181,6 +191,16 @@ export default {
userId,
role,
});
},
makeOwner(user){
this.$store.commit('pushDialogStack', {
component: 'transfer-ownership-dialog',
elementId: 'menu-' + user._id,
data: {
docRef: this.docRef,
user,
},
});
},
},
meteor: {

View File

@@ -0,0 +1,79 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Transfer Ownership
</v-toolbar-title>
<v-alert
type="error"
outlined
>
<template v-if="error">
<p>
{{ error }}
</p>
</template>
<template v-else>
<p>
Are you sure you want to transfer ownership to {{ user.username || user._id }}?
</p><p>
This can only be undone by the user you are transferring ownership to.
</p><p>
You will still have edit permission.
</p>
</template>
</v-alert>
<v-layout justify-center>
<v-btn
color="accent"
@click="transfer"
>
Transfer
<template v-if="user.username">
to {{ user.username }}
</template>
</v-btn>
</v-layout>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { transferOwnership } from '/imports/api/sharing/sharing.js';
export default {
components: {
DialogBase,
},
props: {
docRef: {
type: Object,
required: true,
},
user: {
type: Object,
required: true,
},
},
data(){ return {
error: undefined,
}},
methods: {
transfer(){
transferOwnership.call({
docRef: this.docRef,
userId: this.user._id
}, error => {
if (!error){
this.error = undefined;
this.$store.dispatch('popDialogStack')
return;
}
this.error = error.reason || error.message || error.toString();
});
},
},
}
</script>
<style lang="css" scoped>
</style>

25
app/package-lock.json generated
View File

@@ -2330,6 +2330,11 @@
"yallist": "^4.0.0"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"mongo-object": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-0.1.4.tgz",
@@ -2730,7 +2735,7 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"simpl-schema": {
@@ -2805,6 +2810,11 @@
"source-map": "^0.6.0"
}
},
"speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -2956,9 +2966,9 @@
}
},
"tar": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz",
"integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==",
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@@ -2966,13 +2976,6 @@
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"dependencies": {
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
}
}
},
"text-table": {

View File

@@ -39,6 +39,7 @@
"request": "^2.88.2",
"simpl-schema": "^1.12.0",
"source-map-support": "^0.5.16",
"speakingurl": "^14.0.1",
"styles": "^0.2.1",
"underscore": "^1.13.1",
"vue": "2.6.10",