Compare commits
25 Commits
2.0-beta.2
...
2.0-beta.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19a2798bf7 | ||
|
|
a5f2c2e0d2 | ||
|
|
ee174210fd | ||
|
|
1e38295164 | ||
|
|
758cb2f8bc | ||
|
|
36bb3c3181 | ||
|
|
02434de34c | ||
|
|
0dc0bea53e | ||
|
|
c392119430 | ||
|
|
4e2e0ca364 | ||
|
|
4a8b72f163 | ||
|
|
d916dc2b78 | ||
|
|
56860ba96d | ||
|
|
b607755f9f | ||
|
|
86d8fa4325 | ||
|
|
2b08249e5e | ||
|
|
3133e664d5 | ||
|
|
48f32e0a8d | ||
|
|
c72785c9e7 | ||
|
|
421ff2aa7d | ||
|
|
9a9e6491b9 | ||
|
|
332258705c | ||
|
|
73ef109d4d | ||
|
|
fc240a34c4 | ||
|
|
8ac4028f38 |
@@ -78,8 +78,12 @@ NPM_CONFIG_PRODUCTION=true
|
||||
PROJECT_DIR=app
|
||||
ROOT_URL=https://<url of your DiceCloud instance>
|
||||
DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456">
|
||||
DISABLE_PATREON=<"true" if you want to prevent features being locked behind Patreon tiers>
|
||||
```
|
||||
|
||||
To disable Patreon features and unlock all paid restrictions for all users of your deployment, replace
|
||||
`"patreon": { "clientId": ... }"` with `"disablePatreon": true` in the public key of the METEOR_SETTINGS environment variable.
|
||||
|
||||
Alternatively run `meteor run --settings exampleMeteorSettings.json` to start the app with the example settings that disable Patreon by default.
|
||||
|
||||
Now, visiting [](http://localhost:3000/) should show you an empty instance of
|
||||
DiceCloud running.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '/imports/ui/vueSetup.js';
|
||||
import '/imports/ui/styles/stylesIndex.js';
|
||||
import '/imports/client/config.js';
|
||||
import '/imports/client/serviceWorker.js';
|
||||
|
||||
6
app/exampleMeteorSettings.json
Normal file
6
app/exampleMeteorSettings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"public": {
|
||||
"environment": "production",
|
||||
"disablePatreon": true
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js';
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
|
||||
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
|
||||
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
|
||||
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
|
||||
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
|
||||
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
|
||||
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
|
||||
import { union } from 'lodash';
|
||||
|
||||
export default function combineStat(stat, aggregator, memo){
|
||||
@@ -34,6 +35,8 @@ function getAggregatorResult(stat, aggregator){
|
||||
}
|
||||
if (!stat.decimal && Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
} else if (Number.isFinite(result)){
|
||||
result = stripFloatingPointOddities(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function computeStat(stat, memo){
|
||||
|
||||
// Compute each active stat's baseValue calculation and apply it
|
||||
if (!statInstance.inactive) {
|
||||
delete statInstance.baseValueErrors;
|
||||
let {
|
||||
result,
|
||||
context,
|
||||
|
||||
@@ -21,7 +21,11 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
nodeId: {
|
||||
nodeIds: {
|
||||
type: Array,
|
||||
max: 20,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
@@ -38,7 +42,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({nodeId, parentRef, order}) {
|
||||
run({nodeIds, parentRef, order}) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
|
||||
@@ -53,54 +57,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Fetch the library node and its decendents, provided they have not been
|
||||
// removed
|
||||
// TODO: Check permission to read the library this node is in
|
||||
let node = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
removed: {$ne: true},
|
||||
});
|
||||
if (!node) throw `Node not found for nodeId: ${nodeId}`;
|
||||
let oldParent = node.parent;
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
removed: {$ne: true},
|
||||
}).fetch();
|
||||
// {libraryId: hasViewPermission}
|
||||
//let libraryPermissionMemoir = {};
|
||||
let node;
|
||||
nodeIds.forEach(nodeId => {
|
||||
// TODO: Check library view permission for each node before starting
|
||||
node = insertPropertyFromNode(nodeId, ancestors, order);
|
||||
});
|
||||
|
||||
// Convert all references into actual nodes
|
||||
nodes = reifyNodeReferences(nodes);
|
||||
|
||||
// The root node is first in the array of nodes
|
||||
// It must get the first generated ID to prevent flickering
|
||||
nodes = [node, ...nodes];
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry: ancestors,
|
||||
oldParent,
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: {'libraryNodes': 'creatureProperties'}
|
||||
});
|
||||
|
||||
// Order the root node
|
||||
if (order === undefined){
|
||||
setDocToLastOrder({
|
||||
collection: CreatureProperties,
|
||||
doc: node,
|
||||
});
|
||||
} else {
|
||||
node.order = order;
|
||||
}
|
||||
|
||||
// Insert the creature properties
|
||||
CreatureProperties.batchInsert(nodes);
|
||||
|
||||
// get the root inserted doc
|
||||
// get one of the root inserted docs
|
||||
let rootId = node._id;
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
@@ -110,7 +75,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
});
|
||||
|
||||
// The library properties need to denormalise which of them are inactive
|
||||
recomputeInactiveProperties(rootId);
|
||||
recomputeInactiveProperties(rootCreature._id);
|
||||
// Some of the library properties may be items or containers
|
||||
recomputeInventory(rootCreature._id);
|
||||
// Inserting a creature property invalidates dependencies: full recompute
|
||||
@@ -120,6 +85,56 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
// Fetch the library node and its decendents, provided they have not been
|
||||
// removed
|
||||
// TODO: Check permission to read the library this node is in
|
||||
let node = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
removed: {$ne: true},
|
||||
});
|
||||
if (!node) throw `Node not found for nodeId: ${nodeId}`;
|
||||
let oldParent = node.parent;
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
removed: {$ne: true},
|
||||
}).fetch();
|
||||
|
||||
// Convert all references into actual nodes
|
||||
nodes = reifyNodeReferences(nodes);
|
||||
|
||||
// The root node is first in the array of nodes
|
||||
// It must get the first generated ID to prevent flickering
|
||||
nodes = [node, ...nodes];
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry: ancestors,
|
||||
oldParent,
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: {'libraryNodes': 'creatureProperties'}
|
||||
});
|
||||
|
||||
// Order the root node
|
||||
if (order === undefined){
|
||||
setDocToLastOrder({
|
||||
collection: CreatureProperties,
|
||||
doc: node,
|
||||
});
|
||||
} else {
|
||||
node.order = order;
|
||||
}
|
||||
|
||||
// Insert the creature properties
|
||||
CreatureProperties.batchInsert(nodes);
|
||||
return node;
|
||||
}
|
||||
|
||||
// Covert node references into actual nodes
|
||||
// TODO: check permissions for each library a reference node references
|
||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
@@ -194,7 +209,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
|
||||
// TODO: Force the referencedNode to take the old id of the reference
|
||||
// such that the reference's children can be kept
|
||||
|
||||
|
||||
// Give the new referenced sub-tree new ids
|
||||
renewDocIds({
|
||||
docArray: addedNodes,
|
||||
|
||||
@@ -57,7 +57,7 @@ const insertCreature = new ValidatedMethod({
|
||||
if (Meteor.isServer){
|
||||
// Insert the 5e ruleset as the default base
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeId: 'iHbhfcg3AL5isSWbw',
|
||||
nodeIds: ['iHbhfcg3AL5isSWbw'],
|
||||
parentRef: {id: baseId, collection: 'creatureProperties'},
|
||||
order: 0.5,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js';
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
|
||||
|
||||
export default function recomputeInventory(creatureId){
|
||||
let inventoryForest = nodesToTree({
|
||||
|
||||
@@ -55,7 +55,7 @@ function updateReferenceNodeWork(node, userId){
|
||||
return;
|
||||
}
|
||||
cache = {
|
||||
node: {name: doc.name, type: doc.type},
|
||||
node: doc,
|
||||
};
|
||||
if (library){
|
||||
cache.library = {name: library.name};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js';
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
|
||||
|
||||
export default function getDescendantsInDepthFirstOrder({
|
||||
collection,
|
||||
|
||||
115
app/imports/api/parenting/nodesToTree.js
Normal file
115
app/imports/api/parenting/nodesToTree.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { union, difference, sortBy, findLast } from 'lodash';
|
||||
|
||||
export function nodeArrayToTree(nodes){
|
||||
// Store a dict and list of all the nodes
|
||||
let nodeIndex = {};
|
||||
let nodeList = [];
|
||||
nodes.forEach( node => {
|
||||
let treeNode = {
|
||||
node: node,
|
||||
children: [],
|
||||
};
|
||||
nodeIndex[node._id] = treeNode;
|
||||
nodeList.push(treeNode);
|
||||
});
|
||||
// Create a forest of trees
|
||||
let forest = [];
|
||||
// Either the node is a child of its nearest found ancestor, or in the forest as a root
|
||||
nodeList.forEach(treeNode => {
|
||||
let ancestorInForest = findLast(
|
||||
treeNode.node.ancestors,
|
||||
ancestor => !!nodeIndex[ancestor.id]
|
||||
);
|
||||
if (ancestorInForest){
|
||||
nodeIndex[ancestorInForest.id].children.push(treeNode);
|
||||
} else {
|
||||
forest.push(treeNode);
|
||||
}
|
||||
});
|
||||
return forest;
|
||||
}
|
||||
|
||||
// Fetch the documents from a collection, and return the tree of those documents
|
||||
export default function nodesToTree({
|
||||
collection, ancestorId, filter, options = {},
|
||||
includeFilteredDocAncestors = false, includeFilteredDocDescendants = false
|
||||
}){
|
||||
// Setup the filter
|
||||
let collectionFilter = {
|
||||
'ancestors.id': ancestorId,
|
||||
'removed': {$ne: true},
|
||||
};
|
||||
if (filter){
|
||||
collectionFilter = {
|
||||
...collectionFilter,
|
||||
...filter,
|
||||
}
|
||||
}
|
||||
// Set up the options
|
||||
let collectionSort = {
|
||||
order: 1
|
||||
};
|
||||
if (options && options.sort){
|
||||
collectionSort = {
|
||||
...collectionSort,
|
||||
...options.sort,
|
||||
}
|
||||
}
|
||||
let collectionOptions = {
|
||||
sort: collectionSort,
|
||||
}
|
||||
if (options){
|
||||
collectionOptions = {
|
||||
...collectionOptions,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
// Find all the nodes that match the filter
|
||||
let docs = collection.find(collectionFilter, collectionOptions).map(doc => {
|
||||
if (!filter) return doc;
|
||||
// Mark the nodes that were found by the custom filter
|
||||
doc._matchedDocumentFilter = true;
|
||||
return doc;
|
||||
});
|
||||
let ancestors = [];
|
||||
let ancestorIds = [];
|
||||
let docIds = [];
|
||||
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){
|
||||
docIds = docs.map(doc => doc._id)
|
||||
}
|
||||
if (filter && includeFilteredDocAncestors){
|
||||
// Add all ancestor ids to an array
|
||||
docs.forEach(doc => {
|
||||
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
|
||||
});
|
||||
// Remove the IDs of docs we have already found
|
||||
ancestorIds = difference(ancestorIds, docIds);
|
||||
// Get the docs from the collection, don't worry about `removed` docs,
|
||||
// if their descendant was not removed, neither are they
|
||||
ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => {
|
||||
// Mark that the nodes are ancestors of the found nodes
|
||||
doc._ancestorOfMatchedDocument = true;
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
let descendants = [];
|
||||
if (filter && includeFilteredDocDescendants){
|
||||
let exludeIds = union(ancestorIds, docIds);
|
||||
descendants = collection.find({
|
||||
'_id': {$nin: exludeIds},
|
||||
'ancestors.id': {$in: docIds},
|
||||
'removed': {$ne: true},
|
||||
}).map(doc => {
|
||||
// Mark that the nodes are descendants of the found nodes
|
||||
doc._descendantOfMatchedDocument = true;
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
let nodes = sortBy([
|
||||
...ancestors,
|
||||
...docs,
|
||||
...descendants
|
||||
], 'order');
|
||||
// Find all the nodes
|
||||
return nodeArrayToTree(nodes);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
|
||||
import { flatten, findLast } from 'lodash';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
const generalParents = [
|
||||
'attribute',
|
||||
@@ -217,41 +217,3 @@ export function getName(doc){
|
||||
if (doc.ancestors[i].name) return doc.ancestors[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
export function nodeArrayToTree(nodes){
|
||||
// Store a dict and list of all the nodes
|
||||
let nodeIndex = {};
|
||||
let nodeList = [];
|
||||
nodes.forEach( node => {
|
||||
let treeNode = {
|
||||
node: node,
|
||||
children: [],
|
||||
};
|
||||
nodeIndex[node._id] = treeNode;
|
||||
nodeList.push(treeNode);
|
||||
});
|
||||
// Create a forest of trees
|
||||
let forest = [];
|
||||
// Either the node is a child of its nearest found ancestor, or in the forest as a root
|
||||
nodeList.forEach(treeNode => {
|
||||
let ancestorInForest = findLast(
|
||||
treeNode.node.ancestors,
|
||||
ancestor => !!nodeIndex[ancestor.id]
|
||||
);
|
||||
if (ancestorInForest){
|
||||
nodeIndex[ancestorInForest.id].children.push(treeNode);
|
||||
} else {
|
||||
forest.push(treeNode);
|
||||
}
|
||||
});
|
||||
return forest;
|
||||
}
|
||||
|
||||
export function nodesToTree({collection, ancestorId, filter = {}, options = {}}){
|
||||
if (!('ancestors.id' in filter)) filter['ancestors.id'] = ancestorId;
|
||||
if (!('removed' in filter)) filter['removed'] = {$ne: true};
|
||||
if (!options.sort) options.sort = {order: 1};
|
||||
if (!('order' in options.sort)) options.sort.order = 1;
|
||||
let nodes = collection.find(filter, options);
|
||||
return nodeArrayToTree(nodes);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,14 @@ let ReferenceSchema = new SimpleSchema({
|
||||
'cache.node.type': {
|
||||
type: String,
|
||||
},
|
||||
'cache.node.level': {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
'cache.node.value': {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
'cache.library': {
|
||||
type: Object,
|
||||
optional: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
|
||||
let SpellListSchema = new SimpleSchema({
|
||||
@@ -12,12 +11,6 @@ let SpellListSchema = new SimpleSchema({
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
variableName: {
|
||||
type: String,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
optional: true,
|
||||
},
|
||||
// Calculation of how many spells in this list can be prepared
|
||||
maxPrepared: {
|
||||
type: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import '/imports/api/users/deleteMyAccount.js';
|
||||
import { some } from 'lodash';
|
||||
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
||||
|
||||
const userSchema = new SimpleSchema({
|
||||
@@ -94,6 +95,10 @@ const userSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'preferences.hidePropertySelectDialogHelp': {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.attachSchema(userSchema);
|
||||
@@ -155,7 +160,7 @@ Meteor.users.sendVerificationEmail = new ValidatedMethod({
|
||||
throw new Meteor.Error('User not found',
|
||||
'Can\'t send a validation email to a user that does not exist');
|
||||
}
|
||||
if (!_.some(user.emails, email => email.address === address)) {
|
||||
if (!some(user.emails, email => email.address === address)) {
|
||||
throw new Meteor.Error('Email address not found',
|
||||
'The specified email address wasn\'t found on this user account');
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import { findLast } from 'lodash';
|
||||
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
|
||||
import Invites from '/imports/api/users/Invites.js';
|
||||
const patreonDisabled = !!process.env.DISABLE_PATREON;
|
||||
const patreonDisabled = !!Meteor.settings?.public?.disablePatreon;
|
||||
|
||||
const TIERS = Object.freeze([
|
||||
{
|
||||
name: 'Commoner',
|
||||
minimumEntitledCents: 0,
|
||||
invites: 0,
|
||||
characterSlots: 0, //5,
|
||||
characterSlots: 5,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
name: 'Dreamer',
|
||||
minimumEntitledCents: 100,
|
||||
invites: 0,
|
||||
characterSlots: 0, //5,
|
||||
characterSlots: 5,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
name: 'Wanderer',
|
||||
minimumEntitledCents: 300,
|
||||
invites: 0,
|
||||
characterSlots: 0, //5,
|
||||
characterSlots: 5,
|
||||
paidBenefits: false,
|
||||
}, {
|
||||
//cost per user $5
|
||||
name: 'Adventurer',
|
||||
minimumEntitledCents: 500,
|
||||
invites: 0,
|
||||
characterSlots: -1, //20,
|
||||
characterSlots: 20,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.33
|
||||
name: 'Hero',
|
||||
minimumEntitledCents: 1000,
|
||||
invites: 2,
|
||||
characterSlots: -1, //50,
|
||||
characterSlots: 50,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.333
|
||||
name: 'Legend',
|
||||
minimumEntitledCents: 2000,
|
||||
invites: 5,
|
||||
characterSlots: -1, //120,
|
||||
characterSlots: 120,
|
||||
paidBenefits: true,
|
||||
}, {
|
||||
//cost per user $3.125
|
||||
|
||||
5
app/imports/client/serviceWorker.js
Normal file
5
app/imports/client/serviceWorker.js
Normal file
@@ -0,0 +1,5 @@
|
||||
Meteor.startup(() => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then()
|
||||
.catch(error => console.log('ServiceWorker registration failed: ', error));
|
||||
});
|
||||
@@ -1,104 +1,157 @@
|
||||
const PROPERTIES = Object.freeze({
|
||||
action: {
|
||||
icon: '$vuetify.icons.action',
|
||||
name: 'Action'
|
||||
name: 'Action',
|
||||
helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.',
|
||||
suggestedParents: ['classLevel', 'feature', 'item'],
|
||||
},
|
||||
attack: {
|
||||
icon: '$vuetify.icons.attack',
|
||||
name: 'Attack'
|
||||
name: 'Attack',
|
||||
helpText: 'Attacks are a special form of action that includes an attack roll. Attacks can critical hit, which doubles the number of damage dice in properties under the attack.',
|
||||
suggestedParents: ['classLevel', 'feature', 'item'],
|
||||
},
|
||||
attribute: {
|
||||
icon: '$vuetify.icons.attribute',
|
||||
name: 'Attribute'
|
||||
name: 'Attribute',
|
||||
helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.',
|
||||
examples: 'Ability scores, speed, hit points, ki',
|
||||
suggestedParents: ['classLevel', 'buff'],
|
||||
},
|
||||
adjustment: {
|
||||
icon: '$vuetify.icons.attribute_damage',
|
||||
name: 'Attribute damage'
|
||||
name: 'Attribute damage',
|
||||
helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
|
||||
},
|
||||
buff: {
|
||||
icon: '$vuetify.icons.buff',
|
||||
name: 'Buff'
|
||||
name: 'Buff',
|
||||
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
|
||||
},
|
||||
classLevel: {
|
||||
icon: '$vuetify.icons.class_level',
|
||||
name: 'Class level'
|
||||
name: 'Class level',
|
||||
helpText: 'Class levels represent a single level gained in a class',
|
||||
suggestedParents: ['class'],
|
||||
},
|
||||
constant: {
|
||||
icon: 'mdi-anchor',
|
||||
name: 'Constant'
|
||||
name: 'Constant',
|
||||
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
|
||||
suggestedParents: [],
|
||||
},
|
||||
container: {
|
||||
icon: 'mdi-bag-personal-outline',
|
||||
name: 'Container'
|
||||
name: 'Container',
|
||||
helpText: 'A container holds items in the inventory',
|
||||
examples: 'Coin pouch, backpack',
|
||||
suggestedParents: ['folder'],
|
||||
},
|
||||
damage: {
|
||||
icon: '$vuetify.icons.damage',
|
||||
name: 'Damage'
|
||||
name: 'Damage',
|
||||
helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
|
||||
},
|
||||
damageMultiplier: {
|
||||
icon: '$vuetify.icons.damage_multiplier',
|
||||
name: 'Damage multiplier'
|
||||
name: 'Damage multiplier',
|
||||
helpText: 'Resistance, vulnerability, and immunity.',
|
||||
suggestedParents: ['classLevel', 'feature', 'item'],
|
||||
},
|
||||
effect: {
|
||||
icon: '$vuetify.icons.effect',
|
||||
name: 'Effect'
|
||||
name: 'Effect',
|
||||
helpText: 'Effects change the value or state of attributes and skills.',
|
||||
examples: '+2 Strength, Advantage on dexterity saving throws',
|
||||
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
|
||||
},
|
||||
feature: {
|
||||
icon: 'mdi-text-subject',
|
||||
name: 'Feature'
|
||||
name: 'Feature',
|
||||
helpText: 'Descriptive or narrative features your character has access to',
|
||||
suggestedParents: ['classLevel', 'folder'],
|
||||
},
|
||||
folder: {
|
||||
icon: 'mdi-folder-outline',
|
||||
name: 'Folder'
|
||||
name: 'Folder',
|
||||
helpText: 'A way to organise other properties on the character',
|
||||
suggestedParents: ['folder'],
|
||||
},
|
||||
item: {
|
||||
icon: 'mdi-cube-outline',
|
||||
name: 'Item'
|
||||
name: 'Item',
|
||||
helpText: 'Objects and equipment your charcter finds on their adventures',
|
||||
suggestedParents: ['container'],
|
||||
},
|
||||
note: {
|
||||
icon: 'mdi-note-outline',
|
||||
name: 'Note'
|
||||
name: 'Note',
|
||||
helpText: 'Notes about your character and their adventures',
|
||||
suggestedParents: ['folder'],
|
||||
},
|
||||
proficiency: {
|
||||
icon: 'mdi-brightness-1',
|
||||
name: 'Proficiency'
|
||||
name: 'Proficiency',
|
||||
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
|
||||
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
|
||||
},
|
||||
roll: {
|
||||
icon: '$vuetify.icons.roll',
|
||||
name: 'Roll'
|
||||
name: 'Roll',
|
||||
helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
|
||||
},
|
||||
reference: {
|
||||
icon: 'mdi-vector-link',
|
||||
name: 'Reference',
|
||||
libraryOnly: true,
|
||||
helpText: 'A reference is a link to a different property in a library. When a reference gets copied to a character sheet, it is replaced with the referenced property and all its children.',
|
||||
suggestedParents: [],
|
||||
},
|
||||
savingThrow: {
|
||||
icon: '$vuetify.icons.saving_throw',
|
||||
name: 'Saving throw'
|
||||
name: 'Saving throw',
|
||||
helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.',
|
||||
suggestedParents: ['action', 'attack', 'spell'],
|
||||
},
|
||||
skill: {
|
||||
icon: '$vuetify.icons.skill',
|
||||
name: 'Skill'
|
||||
name: 'Skill',
|
||||
helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.',
|
||||
suggestedParents: ['classLevel', 'folder'],
|
||||
},
|
||||
propertySlot: {
|
||||
icon: 'mdi-power-socket-eu',
|
||||
name: 'Slot'
|
||||
name: 'Slot',
|
||||
helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot',
|
||||
suggestedParents: [],
|
||||
},
|
||||
slotFiller: {
|
||||
icon: 'mdi-power-plug-outline',
|
||||
name: 'Slot filler'
|
||||
name: 'Slot filler',
|
||||
helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
|
||||
suggestedParents: ['propertySlot'],
|
||||
},
|
||||
spellList: {
|
||||
icon: '$vuetify.icons.spell_list',
|
||||
name: 'Spell list'
|
||||
name: 'Spell list',
|
||||
helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within',
|
||||
suggestedParents: [],
|
||||
},
|
||||
spell: {
|
||||
icon: '$vuetify.icons.spell',
|
||||
name: 'Spell'
|
||||
name: 'Spell',
|
||||
helpText: 'A spell your character can potentially cast',
|
||||
suggestedParents: ['spellList'],
|
||||
},
|
||||
toggle: {
|
||||
icon: '$vuetify.icons.toggle',
|
||||
name: 'Toggle'
|
||||
name: 'Toggle',
|
||||
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
|
||||
suggestedParents: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
4
app/imports/server/config/accountsEmailConfig.js
Normal file
4
app/imports/server/config/accountsEmailConfig.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Accounts } from 'meteor/accounts-base'
|
||||
|
||||
Accounts.emailTemplates.from = 'no-reply@dicecloud.com';
|
||||
Accounts.emailTemplates.siteName = 'DiceCloud';
|
||||
@@ -9,3 +9,4 @@ import '/imports/server/publications/tabletops.js';
|
||||
import '/imports/server/publications/slotFillers.js';
|
||||
import '/imports/server/publications/ownedDocuments.js';
|
||||
import '/imports/server/publications/archivedCreatures.js';
|
||||
import '/imports/server/publications/searchLibraryNodes.js';
|
||||
|
||||
@@ -2,13 +2,6 @@ import SimpleSchema from 'simpl-schema';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
const standardLibraryIds = [
|
||||
'SRDLibraryGA3XWsd',
|
||||
];
|
||||
|
||||
Meteor.publish('standardLibraries', function(){
|
||||
return Libraries.find({_id: {$in: standardLibraryIds}});
|
||||
});
|
||||
|
||||
Meteor.publish('libraries', function(){
|
||||
this.autorun(function (){
|
||||
@@ -75,3 +68,24 @@ Meteor.publish('libraryNodes', function(libraryId){
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish('descendantLibraryNodes', function(nodeId){
|
||||
let node = LibraryNodes.findOne(nodeId);
|
||||
let libraryId = node?.ancestors[0]?.id;
|
||||
if (!libraryId) return [];
|
||||
this.autorun(function (){
|
||||
let userId = this.userId;
|
||||
let library = Libraries.findOne(libraryId);
|
||||
try { assertViewPermission(library, userId) }
|
||||
catch(e){
|
||||
return this.error(e);
|
||||
}
|
||||
return [
|
||||
LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
}),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
116
app/imports/server/publications/searchLibraryNodes.js
Normal file
116
app/imports/server/publications/searchLibraryNodes.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
|
||||
Meteor.publish('searchLibraryNodes', function(){
|
||||
let self = this;
|
||||
this.autorun(function (){
|
||||
let type = self.data('type');
|
||||
if (!type) return [];
|
||||
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all the ids of libraries the user can access
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {subscribedLibraries: 1}
|
||||
});
|
||||
if (!user) return [];
|
||||
|
||||
const subs = user.subscribedLibraries || [];
|
||||
let libraries = Libraries.find({
|
||||
$or: [
|
||||
{owner: this.userId},
|
||||
{writers: this.userId},
|
||||
{readers: this.userId},
|
||||
{_id: {$in: subs}},
|
||||
]
|
||||
}, {
|
||||
fields: {_id: 1, name: 1},
|
||||
});
|
||||
let libraryIds = libraries.map(lib => lib._id);
|
||||
|
||||
// Build a filter for nodes in those libraries that match the type
|
||||
let filter = {
|
||||
'ancestors.id': {$in: libraryIds},
|
||||
removed: {$ne: true},
|
||||
tags: {$ne: []}, // Only tagged library nodes are considered
|
||||
};
|
||||
if (type){
|
||||
filter.$or = [{
|
||||
type,
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
slotFillerType: type,
|
||||
}];
|
||||
}
|
||||
|
||||
this.autorun(function(){
|
||||
// Get the limit of the documents the user can fetch
|
||||
var limit = self.data('limit') || 32;
|
||||
check(limit, Number);
|
||||
|
||||
// Get the search term
|
||||
let searchTerm = self.data('searchTerm') || '';
|
||||
check(searchTerm, String);
|
||||
|
||||
let options = undefined;
|
||||
if (searchTerm){
|
||||
filter.$text = {$search: searchTerm};
|
||||
options = {
|
||||
// relevant documents have a higher score.
|
||||
fields: {
|
||||
score: { $meta: 'textScore' }
|
||||
},
|
||||
sort: {
|
||||
// `score` property specified in the projection fields above.
|
||||
score: { $meta: 'textScore' },
|
||||
'ancestors.0.id': 1,
|
||||
name: 1,
|
||||
order: 1,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete filter.$text
|
||||
options = {sort: {
|
||||
'ancestors.0.id': 1,
|
||||
name: 1,
|
||||
order: 1,
|
||||
}};
|
||||
}
|
||||
options.limit = limit;
|
||||
|
||||
this.autorun(function () {
|
||||
self.setData('countAll', LibraryNodes.find(filter).count());
|
||||
});
|
||||
|
||||
let cursor = LibraryNodes.find(filter, options);
|
||||
|
||||
Mongo.Collection._publishCursor(libraries, self, 'libraries');
|
||||
|
||||
let observeHandle = cursor.observeChanges({
|
||||
added: function (id, fields) {
|
||||
fields._searchResult = true;
|
||||
self.added('libraryNodes', id, fields);
|
||||
},
|
||||
changed: function (id, fields) {
|
||||
self.changed('libraryNodes', id, fields);
|
||||
},
|
||||
removed: function (id) {
|
||||
self.removed('libraryNodes', id);
|
||||
}
|
||||
},
|
||||
// Publications don't mutate the documents
|
||||
{ nonMutatingCallbacks: true }
|
||||
);
|
||||
|
||||
// register stop callback (expects lambda w/ no args).
|
||||
this.onStop(function () {
|
||||
observeHandle.stop();
|
||||
});
|
||||
// this.ready();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,7 +35,7 @@
|
||||
<text-field
|
||||
ref="iconSearchField"
|
||||
label="Search icons"
|
||||
append-icon="mdi-search"
|
||||
append-icon="mdi-magnify"
|
||||
clearable
|
||||
hide-details
|
||||
class="ma-2"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template lang="html">
|
||||
<v-sheet
|
||||
class="tree-node"
|
||||
:class="!hasChildren ? 'empty' : null"
|
||||
:class="{
|
||||
'empty': !hasChildren,
|
||||
'found': node._matchedDocumentFilter,
|
||||
}"
|
||||
:data-id="`tree-node-${node._id}`"
|
||||
>
|
||||
<div
|
||||
@@ -52,7 +55,7 @@
|
||||
:children="computedChildren"
|
||||
:group="group"
|
||||
:organize="organize"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@reordered="e => $emit('reordered', e)"
|
||||
@reorganized="e => $emit('reorganized', e)"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@@ -80,6 +83,7 @@
|
||||
import { canBeParent } from '/imports/api/parenting/parenting.js';
|
||||
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import { some } from 'lodash';
|
||||
|
||||
export default {
|
||||
name: 'TreeNode',
|
||||
@@ -87,16 +91,33 @@
|
||||
TreeNodeView,
|
||||
},
|
||||
props: {
|
||||
node: Object,
|
||||
group: String,
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
organize: Boolean,
|
||||
children: Array,
|
||||
getChildren: Function,
|
||||
selectedNodeId: String,
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
getChildren: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
selected: Boolean,
|
||||
},
|
||||
data(){ return {
|
||||
expanded: false,
|
||||
data(){return {
|
||||
expanded: this.node._ancestorOfMatchedDocument ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
|
||||
false,
|
||||
}},
|
||||
computed: {
|
||||
hasChildren(){
|
||||
@@ -119,6 +140,15 @@
|
||||
return canBeParent(this.node.type);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node._ancestorOfMatchedDocument'(value){
|
||||
this.expanded = !!value ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
|
||||
},
|
||||
'selectedNode.ancestors'(value){
|
||||
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default
|
||||
},
|
||||
@@ -148,9 +178,12 @@
|
||||
.empty .v-btn {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.found {
|
||||
background: rgba(200, 0, 0, 0.1) !important;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #fbc8c8;
|
||||
background: rgba(251, 0, 0, 0.3);
|
||||
}
|
||||
.v-icon.v-icon--disabled {
|
||||
opacity: 0;
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
:node="child.node"
|
||||
:children="child.children"
|
||||
:group="group"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected="selectedNodeId === child.node._id"
|
||||
:selected-node="selectedNode"
|
||||
:selected="selectedNode && selectedNode._id === child.node._id"
|
||||
:ancestors-of-selected-node="ancestorsOfSelectedNode"
|
||||
:organize="organize"
|
||||
:lazy="lazy"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@@ -49,7 +50,14 @@
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedNodeId: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
ancestorsOfSelectedNode: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
expanded: false,
|
||||
|
||||
81
app/imports/ui/components/tree/TreeSearchInput.vue
Normal file
81
app/imports/ui/components/tree/TreeSearchInput.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template lang="html">
|
||||
<v-combobox
|
||||
v-model="filterTerms"
|
||||
:items="filterOptions"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
hide-no-data
|
||||
hide-selected
|
||||
multiple
|
||||
clearable
|
||||
small-chips
|
||||
deletable-chips
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
filterTerms: [],
|
||||
filterOptions: [
|
||||
{text: 'Actions', value: 'action'},
|
||||
{text: 'Attacks', value: 'attack'},
|
||||
{text: 'Attributes', value: 'attribute'},
|
||||
{text: 'Buffs', value: 'buff'},
|
||||
{text: 'Class Levels', value: 'classLevel'},
|
||||
{text: 'Damage Multipliers', value: 'damageMultiplier'},
|
||||
{text: 'Effects', value: 'effect'},
|
||||
{text: 'Experiences', value: 'experience'},
|
||||
{text: 'Features', value: 'feature'},
|
||||
{text: 'Folders', value: 'folder'},
|
||||
{text: 'Notes', value: 'note'},
|
||||
{text: 'Proficiencies', value: 'proficiency'},
|
||||
{text: 'Rolls', value: 'roll'},
|
||||
{text: 'Saving Throws', value: 'savingThrow'},
|
||||
{text: 'Skills', value: 'skill'},
|
||||
{text: 'Spell Lists', value: 'spellList'},
|
||||
{text: 'Spells', value: 'spell'},
|
||||
{text: 'Containers', value: 'container'},
|
||||
{text: 'Items', value: 'item'},
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
filter(){
|
||||
if (!this.filterTerms.length) return;
|
||||
let typeFilters = [];
|
||||
let nameFilters = [];
|
||||
this.filterTerms.forEach(filter => {
|
||||
if (filter.value){
|
||||
typeFilters.push(filter.value);
|
||||
} else {
|
||||
// escape string
|
||||
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
|
||||
var reg = new RegExp( '.*' + term + '.*', 'i' );
|
||||
nameFilters.push(reg)
|
||||
}
|
||||
});
|
||||
let filter = {};
|
||||
if (typeFilters.length){
|
||||
filter.type = {$in: typeFilters};
|
||||
}
|
||||
if (nameFilters.length){
|
||||
filter.name = {$in: nameFilters};
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
},
|
||||
watch:{
|
||||
filter(value){
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -31,41 +31,22 @@
|
||||
:key="type"
|
||||
color="primary"
|
||||
:data-id="`insert-creature-property-type-${type}`"
|
||||
:label="'New ' + properties[type].name"
|
||||
:icon="properties[type].icon"
|
||||
:label="type ? 'New ' + properties[type].name : 'New Property'"
|
||||
:icon="type ? properties[type].icon : 'mdi-plus'"
|
||||
:disabled="!editPermission"
|
||||
@click="insertPropertyOfType(type)"
|
||||
@click="addProperty(type)"
|
||||
/>
|
||||
<template v-if="tabNumber === 5">
|
||||
<labeled-fab
|
||||
key="property"
|
||||
color="primary"
|
||||
data-id="insert-creature-property-btn"
|
||||
label="New Property"
|
||||
icon="mdi-pencil"
|
||||
:disabled="!editPermission"
|
||||
@click="insertTreeProperty"
|
||||
/>
|
||||
<labeled-fab
|
||||
key="property"
|
||||
color="primary"
|
||||
data-id="insert-creature-property-from-library-btn"
|
||||
label="Property From Library"
|
||||
icon="mdi-library-shelves"
|
||||
:disabled="!editPermission"
|
||||
@click="propertyFromLibrary"
|
||||
/>
|
||||
</template>
|
||||
</v-speed-dial>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
|
||||
import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
import insertProperty, { insertPropertyAsChildOfTag } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function getParentAndOrderFromSelectedTreeNode(creatureId){
|
||||
// find the parent based on the currently selected property
|
||||
@@ -149,93 +130,50 @@
|
||||
'inventory': ['item', 'container'],
|
||||
'spells': ['spellList', 'spell'],
|
||||
'character': ['note'],
|
||||
'tree': [],
|
||||
'tree': [null],
|
||||
};},
|
||||
properties(){
|
||||
return PROPERTIES;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
insertPropertyOfType(type){
|
||||
addProperty(forcedType){
|
||||
let creatureId = this.creatureId;
|
||||
let fab = hideFab();
|
||||
|
||||
// Open the dialog to insert the property
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
|
||||
let parent;
|
||||
try {
|
||||
parent = fetchDocByRef(parentRef);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-creation-dialog',
|
||||
elementId: 'insert-creature-property-type-' + type,
|
||||
component: 'add-creature-property-dialog',
|
||||
elementId: 'insert-creature-property-type-' + forcedType,
|
||||
data: {
|
||||
forcedType: type,
|
||||
parentDoc: forcedType ? undefined : parent,
|
||||
forcedType,
|
||||
},
|
||||
callback(creatureProperty){
|
||||
if (!creatureProperty) return 'insert-creature-property-fab';
|
||||
revealFab(fab);
|
||||
|
||||
// Insert the property
|
||||
creatureProperty.order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: creatureId
|
||||
}) + 1;
|
||||
|
||||
let tagDetails;
|
||||
switch (type){
|
||||
case 'item':
|
||||
tagDetails = {tag: 'carried', name: 'Carried'};
|
||||
break;
|
||||
case 'container':
|
||||
tagDetails = {tag: 'inventory', name: 'Inventory'};
|
||||
break;
|
||||
default:
|
||||
tagDetails = {tag: `${type}s`};
|
||||
break;
|
||||
callback(result){
|
||||
if (!result){
|
||||
return 'insert-creature-property-fab';
|
||||
}
|
||||
if (Array.isArray(result)){
|
||||
revealFab(fab);
|
||||
let nodeIds = result;
|
||||
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
|
||||
return forcedType ? id : `tree-node-${id}`;
|
||||
} else {
|
||||
revealFab(fab);
|
||||
let creatureProperty = result;
|
||||
// Get order and parent
|
||||
creatureProperty.order = order;
|
||||
// Insert the property
|
||||
let id = insertProperty.call({creatureProperty, parentRef});
|
||||
return forcedType ? id : `tree-node-${id}`;
|
||||
}
|
||||
let id = insertPropertyAsChildOfTag.call({
|
||||
creatureProperty,
|
||||
creatureId,
|
||||
tag: tagDetails.tag,
|
||||
tagDefaultName: tagDetails.name,
|
||||
});
|
||||
return id;
|
||||
}
|
||||
});
|
||||
},
|
||||
insertTreeProperty(){
|
||||
let creatureId = this.creatureId;
|
||||
let fab = hideFab();
|
||||
// Open the dialog to insert the property
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-creation-dialog',
|
||||
elementId: 'insert-creature-property-btn',
|
||||
callback(creatureProperty){
|
||||
if (!creatureProperty) return 'insert-creature-property-fab';
|
||||
revealFab(fab);
|
||||
|
||||
// Get order and parent
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
|
||||
creatureProperty.order = order;
|
||||
|
||||
// Insert the property
|
||||
let id = insertProperty.call({creatureProperty, parentRef});
|
||||
return `tree-node-${id}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
propertyFromLibrary(){
|
||||
let creatureId = this.creatureId;
|
||||
let fab = hideFab();
|
||||
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-from-library-dialog',
|
||||
elementId: 'insert-creature-property-from-library-btn',
|
||||
callback(libraryNode){
|
||||
if (!libraryNode) return 'insert-creature-property-fab';
|
||||
revealFab(fab);
|
||||
|
||||
let nodeId = libraryNode._id;
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
|
||||
|
||||
let id = insertPropertyFromLibraryNode.call({nodeId, parentRef, order});
|
||||
return `tree-node-${id}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-title>
|
||||
{{ creature.denormalizedStats.weightCarried || 0 }} lb
|
||||
{{ weightCarried }} lb
|
||||
</v-list-item-title>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
@@ -107,6 +107,7 @@ import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
|
||||
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
||||
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
||||
import CoinValue from '/imports/ui/components/CoinValue.vue';
|
||||
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -207,6 +208,11 @@ export default {
|
||||
containerIds(){
|
||||
return this.containers.map(container => container._id);
|
||||
},
|
||||
weightCarried(){
|
||||
return stripFloatingPointOddities(
|
||||
this.creature.denormalizedStats.weightCarried || 0
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id){
|
||||
|
||||
@@ -23,19 +23,11 @@
|
||||
:disabled="organizeDisabled"
|
||||
style="flex-grow: 0; height: 32px;"
|
||||
/>
|
||||
<v-combobox
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
slot="extension"
|
||||
v-model="filterString"
|
||||
:items="filterOptions"
|
||||
prepend-inner-icon="mdi-search"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
hide-no-data
|
||||
hide-selected
|
||||
multiple
|
||||
clearable
|
||||
small-chips
|
||||
deletable-chips
|
||||
/>
|
||||
</v-toolbar>
|
||||
<creature-properties-tree
|
||||
@@ -43,7 +35,7 @@
|
||||
style="overflow-y: auto;"
|
||||
:root="{collection: 'creatures', id: creatureId}"
|
||||
:organize="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
:filter="filter"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
@@ -51,9 +43,9 @@
|
||||
<template slot="detail">
|
||||
<creature-property-dialog
|
||||
embedded
|
||||
:_id="selected"
|
||||
@removed="selected = undefined"
|
||||
@duplicated="id => selected = id"
|
||||
:_id="selectedNodeId"
|
||||
@removed="selectedNodeId = undefined"
|
||||
@duplicated="id => selectedNodeId = id"
|
||||
/>
|
||||
</template>
|
||||
</tree-detail-layout>
|
||||
@@ -65,13 +57,14 @@
|
||||
import TreeDetailLayout from '/imports/ui/components/TreeDetailLayout.vue';
|
||||
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
|
||||
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
||||
|
||||
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeDetailLayout,
|
||||
TreeSearchInput,
|
||||
CreaturePropertiesTree,
|
||||
CreaturePropertyDialog,
|
||||
},
|
||||
@@ -87,52 +80,10 @@
|
||||
data(){ return {
|
||||
organize: false,
|
||||
organizeDisabled: false,
|
||||
selected: undefined,
|
||||
selectedNodeId: undefined,
|
||||
fab: false,
|
||||
filterString: '',
|
||||
filterOptions: [
|
||||
{text: 'Actions', value: 'action'},
|
||||
{text: 'Attacks', value: 'attack'},
|
||||
{text: 'Attributes', value: 'attribute'},
|
||||
{text: 'Buffs', value: 'buff'},
|
||||
{text: 'Class Levels', value: 'classLevel'},
|
||||
{text: 'Damage Multipliers', value: 'damageMultiplier'},
|
||||
{text: 'Effects', value: 'effect'},
|
||||
{text: 'Experiences', value: 'experience'},
|
||||
{text: 'Features', value: 'feature'},
|
||||
{text: 'Folders', value: 'folder'},
|
||||
{text: 'Notes', value: 'note'},
|
||||
{text: 'Proficiencies', value: 'proficiency'},
|
||||
{text: 'Rolls', value: 'roll'},
|
||||
{text: 'Saving Throws', value: 'savingThrow'},
|
||||
{text: 'Skills', value: 'skill'},
|
||||
{text: 'Spell Lists', value: 'spellList'},
|
||||
{text: 'Spells', value: 'spell'},
|
||||
{text: 'Containers', value: 'container'},
|
||||
{text: 'Items', value: 'item'},
|
||||
],
|
||||
filter: undefined,
|
||||
};},
|
||||
computed: {
|
||||
filter(){
|
||||
if (!this.filterString.length) return;
|
||||
let typeFilters = [];
|
||||
let nameFilters = [];
|
||||
this.filterString.forEach(filter => {
|
||||
if (filter.value){
|
||||
typeFilters.push(filter.value);
|
||||
} else {
|
||||
// escape string
|
||||
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
|
||||
var reg = new RegExp( '.*' + term + '.*', 'i' );
|
||||
nameFilters.push(reg)
|
||||
}
|
||||
});
|
||||
return {$or: [
|
||||
{type: {$in: typeFilters}},
|
||||
{name: {$in: nameFilters}},
|
||||
]};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filter(filter){
|
||||
if (filter) {
|
||||
@@ -144,14 +95,14 @@
|
||||
},
|
||||
'$vuetify.breakpoint.mdAndUp'(mdAndUp){
|
||||
if (!mdAndUp){
|
||||
this.selected = undefined;
|
||||
this.selectedNodeId = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickNode(id){
|
||||
if (this.$vuetify.breakpoint.mdAndUp){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
} else {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
@@ -167,7 +118,7 @@
|
||||
component: 'creature-property-dialog',
|
||||
elementId: 'selected-node-card',
|
||||
data: {
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
startInEditTab: true,
|
||||
},
|
||||
});
|
||||
@@ -175,9 +126,9 @@
|
||||
getPropertyName,
|
||||
},
|
||||
meteor: {
|
||||
selectedProperty(){
|
||||
selectedNode(){
|
||||
return CreatureProperties.findOne({
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
removed: {$ne: true}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title class="mr-4">
|
||||
<template v-if="tab === 2">
|
||||
New
|
||||
</template>{{ typeName }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-slide-x-reverse-transition hide-on-leave>
|
||||
<v-switch
|
||||
v-if="tab === 0"
|
||||
:input-value="showPropertyHelp"
|
||||
append-icon="mdi-help"
|
||||
hide-details
|
||||
flat
|
||||
@change="propertyHelpChanged"
|
||||
/>
|
||||
<text-field
|
||||
v-if="tab === 1"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
hide-details
|
||||
:value="searchValue"
|
||||
:debounce="400"
|
||||
@change="searchChanged"
|
||||
/>
|
||||
</v-slide-x-reverse-transition>
|
||||
</template>
|
||||
<v-tabs
|
||||
slot="toolbar-extension"
|
||||
v-model="tab"
|
||||
>
|
||||
<v-tab :disabled="!!forcedType">
|
||||
{{ typeName || 'Type' }}
|
||||
</v-tab>
|
||||
<v-tab :disabled="!type">
|
||||
Library
|
||||
</v-tab>
|
||||
<v-tab :disabled="!type">
|
||||
Create
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-items
|
||||
slot="unwrapped-content"
|
||||
v-model="tab"
|
||||
>
|
||||
<v-tab-item :disabled="!!forcedType">
|
||||
<property-selector
|
||||
no-library-only-props
|
||||
:parent-type="parentDoc && parentDoc.type"
|
||||
@select="e => type = e"
|
||||
/>
|
||||
</v-tab-item>
|
||||
<v-tab-item :disabled="!type">
|
||||
<v-expansion-panels
|
||||
multiple
|
||||
inset
|
||||
>
|
||||
<v-expansion-panel
|
||||
v-for="libraryNode in libraryNodes"
|
||||
:key="libraryNode._id"
|
||||
:model="libraryNode"
|
||||
:data-id="libraryNode._id"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<template #default="{ open }">
|
||||
<v-checkbox
|
||||
v-model="selectedNodeIds"
|
||||
class="my-0 py-0 mr-2 flex-grow-0"
|
||||
hide-details
|
||||
:value="libraryNode._id"
|
||||
:disabled="!selectedNodeIds.includes(libraryNode._id) &&
|
||||
selectedNodeIds.length >= 20"
|
||||
@click.stop
|
||||
/>
|
||||
<v-layout column>
|
||||
<tree-node-view :model="libraryNode" />
|
||||
<div class="text-caption">
|
||||
{{ libraryNames[libraryNode.ancestors[0].id ] }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<template v-if="open">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
class="flex-grow-0"
|
||||
@click.stop="openPropertyDetails(libraryNode._id)"
|
||||
>
|
||||
<v-icon>mdi-window-restore</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<library-node-expansion-content :model="libraryNode" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<v-layout
|
||||
justify-center
|
||||
>
|
||||
<v-fade-transition mode="out-in">
|
||||
<div
|
||||
v-if="currentLimit < countAll"
|
||||
class="layout justify-center align-stretch"
|
||||
>
|
||||
<v-btn
|
||||
v-if="currentLimit < countAll"
|
||||
key="load-more-btn"
|
||||
:loading="!$subReady.searchLibraryNodes"
|
||||
color="accent"
|
||||
class="ma-4"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</v-layout>
|
||||
</v-tab-item>
|
||||
<v-tab-item :disabled="!type">
|
||||
<v-card-text
|
||||
v-if="!$slots['unwrapped-content']"
|
||||
>
|
||||
<component
|
||||
:is="type"
|
||||
v-if="type"
|
||||
class="creature-property-form"
|
||||
:model="model"
|
||||
:errors="errors"
|
||||
@change="change"
|
||||
@push="push"
|
||||
@pull="pull"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
<template slot="actions">
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="tab === 2"
|
||||
text
|
||||
color="primary"
|
||||
:disabled="!valid"
|
||||
@click="$store.dispatch('popDialogStack', model)"
|
||||
>
|
||||
create
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else-if="tab === 1"
|
||||
text
|
||||
color="primary"
|
||||
:disabled="!selectedNodeIds.length"
|
||||
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
|
||||
>
|
||||
<template v-if="selectedNodeIds.length >= 15">
|
||||
{{ selectedNodeIds.length }}/20
|
||||
</template>
|
||||
Insert
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
const SKIP_LIBRARY_PROP_TYPES = ['note', 'damage', 'adjustment']
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertySelector,
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
LibraryNodeExpansionContent,
|
||||
...propertyFormIndex,
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
props: {
|
||||
forcedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedTypes: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
parentDoc: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['debounceTime'],
|
||||
},
|
||||
data(){return {
|
||||
selectedNodeIds: [],
|
||||
type: this.forcedType || this.suggestedType,
|
||||
model: {
|
||||
type: this.type,
|
||||
},
|
||||
searchValue: undefined,
|
||||
debounceTime: 0,
|
||||
tab: 0,
|
||||
};},
|
||||
computed: {
|
||||
typeName(){
|
||||
return getPropertyName(this.type) || 'Property';
|
||||
},
|
||||
toolbarColor(){
|
||||
return getThemeColor('secondary');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
type(newType){
|
||||
this.changeType(newType);
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
this.changeType(this.type);
|
||||
},
|
||||
methods: {
|
||||
|
||||
propertyHelpChanged(value){
|
||||
Meteor.users.setPreference.call({
|
||||
preference: 'hidePropertySelectDialogHelp',
|
||||
value: !value
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
},
|
||||
searchChanged(val, ack){
|
||||
this._subs.searchLibraryNodes.setData('searchTerm', val);
|
||||
this._subs.searchLibraryNodes.setData('limit', undefined);
|
||||
this.selectedNode = undefined;
|
||||
this.searchValue = val;
|
||||
setTimeout(ack, 200);
|
||||
},
|
||||
loadMore(){
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
|
||||
},
|
||||
insert(){
|
||||
if (!this.selectedNodeIds.length) return;
|
||||
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
|
||||
},
|
||||
changeType(type){
|
||||
this._subs.searchLibraryNodes.setData('type', type);
|
||||
if (!type) return;
|
||||
if (SKIP_LIBRARY_PROP_TYPES.includes(type)){
|
||||
this.tab = 2;
|
||||
} else {
|
||||
this.tab = 1;
|
||||
}
|
||||
this.schema = propertySchemasIndex[type];
|
||||
this.validationContext = this.schema.newContext();
|
||||
let model = this.schema.clean({});
|
||||
model.type = type;
|
||||
this.model = model;
|
||||
},
|
||||
openPropertyDetails(id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
'$subscribe':{
|
||||
'searchLibraryNodes': [],
|
||||
},
|
||||
showPropertyHelp(){
|
||||
let user = Meteor.user();
|
||||
return !(user?.preferences?.hidePropertySelectDialogHelp)
|
||||
},
|
||||
currentLimit(){
|
||||
return this._subs.searchLibraryNodes.data('limit') || 32;
|
||||
},
|
||||
countAll(){
|
||||
return this._subs.searchLibraryNodes.data('countAll');
|
||||
},
|
||||
libraryNodes(){
|
||||
return LibraryNodes.find({
|
||||
_searchResult: true
|
||||
},{
|
||||
sort: {
|
||||
'ancestors.0.id': 1,
|
||||
name: 1,
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
libraryNames(){
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
:children="children"
|
||||
:group="group"
|
||||
:organize="organize"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="reordered"
|
||||
@reorganized="reorganized"
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js'
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
|
||||
@@ -24,8 +24,14 @@
|
||||
props: {
|
||||
root: Object,
|
||||
organize: Boolean,
|
||||
selectedNodeId: String,
|
||||
filter: Object,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: 'creatureProperties'
|
||||
@@ -37,6 +43,8 @@
|
||||
collection: CreatureProperties,
|
||||
ancestorId: this.root.id,
|
||||
filter: this.filter,
|
||||
includeFilteredDocAncestors: true,
|
||||
includeFilteredDocDescendants: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -98,6 +98,7 @@ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
|
||||
let formIndex = {};
|
||||
for (let key in propertyFormIndex){
|
||||
@@ -243,28 +244,37 @@ export default {
|
||||
},
|
||||
addProperty(){
|
||||
let parentPropertyId = this.model._id;
|
||||
// Open the dialog to insert the property
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-creation-dialog',
|
||||
component: 'add-creature-property-dialog',
|
||||
elementId: 'insert-creature-property-btn',
|
||||
callback(creatureProperty){
|
||||
if (!creatureProperty) return;
|
||||
// Get order and parent
|
||||
data: {
|
||||
parentDoc: this.model,
|
||||
},
|
||||
callback(result){
|
||||
if (!result) return;
|
||||
let parentRef = {
|
||||
id: parentPropertyId,
|
||||
collection: 'creatureProperties',
|
||||
};
|
||||
creatureProperty.order = getHighestOrder({
|
||||
let order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: parentRef.id,
|
||||
}) + 0.5;
|
||||
|
||||
// Insert the property
|
||||
let id = insertProperty.call({creatureProperty, parentRef});
|
||||
return `tree-node-${id}`;
|
||||
if (Array.isArray(result)){
|
||||
let nodeIds = result;
|
||||
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
|
||||
return `tree-node-${id}`;
|
||||
} else {
|
||||
let creatureProperty = result;
|
||||
// Get order and parent
|
||||
creatureProperty.order = order;
|
||||
// Insert the property
|
||||
let id = insertProperty.call({creatureProperty, parentRef});
|
||||
return `tree-node-${id}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -46,44 +46,44 @@ import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
...propertyFormIndex,
|
||||
DialogBase,
|
||||
components: {
|
||||
...propertyFormIndex,
|
||||
DialogBase,
|
||||
ColorPicker,
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
props: {
|
||||
propertyName: String,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
props: {
|
||||
propertyName: String,
|
||||
type: String,
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['debounceTime'],
|
||||
},
|
||||
data(){return {
|
||||
model: {
|
||||
type: this.type,
|
||||
},
|
||||
schema: undefined,
|
||||
validationContext: undefined,
|
||||
data(){return {
|
||||
model: {
|
||||
type: this.type,
|
||||
},
|
||||
schema: undefined,
|
||||
validationContext: undefined,
|
||||
debounceTime: 0,
|
||||
};},
|
||||
watch: {
|
||||
type(newType){
|
||||
};},
|
||||
watch: {
|
||||
type(newType){
|
||||
this.changeType(newType);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
this.changeType(this.type);
|
||||
},
|
||||
methods:{
|
||||
changeType(type){
|
||||
if (!type) return;
|
||||
this.schema = propertySchemasIndex[type];
|
||||
this.validationContext = this.schema.newContext();
|
||||
let model = this.schema.clean({});
|
||||
model.type = type;
|
||||
this.model = model;
|
||||
this.schema = propertySchemasIndex[type];
|
||||
this.validationContext = this.schema.newContext();
|
||||
let model = this.schema.clean({});
|
||||
model.type = type;
|
||||
this.model = model;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<text-field
|
||||
prepend-inner-icon="mdi-search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
hide-details
|
||||
:value="searchValue"
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
callback(node){
|
||||
if(!node) return;
|
||||
let newPropertyId = insertPropertyFromLibraryNode.call({
|
||||
nodeId: node._id,
|
||||
nodeIds: [node._id],
|
||||
parentRef: {
|
||||
'id': slotId,
|
||||
'collection': 'creatureProperties',
|
||||
|
||||
@@ -21,19 +21,25 @@
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<slot name="toolbar" />
|
||||
<slot
|
||||
slot="extension"
|
||||
name="toolbar-extension"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</slot>
|
||||
<div
|
||||
v-if="$slots['unwrapped-content']"
|
||||
id="base-dialog-body"
|
||||
class="unwrapped-content"
|
||||
@scroll.passive="onScroll"
|
||||
>
|
||||
<slot name="unwrapped-content" />
|
||||
</div>
|
||||
<v-card-text
|
||||
v-if="!$slots['unwrapped-content']"
|
||||
v-else
|
||||
id="base-dialog-body"
|
||||
v-scroll:#base-dialog-body="onScroll"
|
||||
:class="{'dark-body': darkBody}"
|
||||
@scroll.passive="onScroll"
|
||||
>
|
||||
<slot />
|
||||
</v-card-text>
|
||||
@@ -90,7 +96,7 @@
|
||||
|
||||
<style scoped>
|
||||
.base-dialog-toolbar {
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
#base-dialog-body, .unwrapped-content {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AddCreaturePropertyDialog from '/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue';
|
||||
import ArchiveDialog from '/imports/ui/creature/archive/ArchiveDialog.vue';
|
||||
import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
|
||||
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
|
||||
@@ -23,6 +24,7 @@ import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
|
||||
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
|
||||
|
||||
export default {
|
||||
AddCreaturePropertyDialog,
|
||||
ArchiveDialog,
|
||||
CastSpellWithSlotDialog,
|
||||
CreatureFormDialog,
|
||||
|
||||
@@ -22,17 +22,17 @@ const dialogStackStore = {
|
||||
});
|
||||
updateHistory();
|
||||
},
|
||||
replaceDialog(state, {component, data, elementId, callback}){
|
||||
const _id = Random.id();
|
||||
replaceDialog(state, {component, data}){
|
||||
if (!state.dialogs.length){
|
||||
throw new Meteor.Error('can\'t replace dialog if no dialogs are open');
|
||||
}
|
||||
let currentDialog = state.dialogs[state.dialogs.length - 1]
|
||||
Vue.set(state.dialogs, state.dialogs.length - 1, {
|
||||
_id,
|
||||
_id: currentDialog._id,
|
||||
component,
|
||||
data,
|
||||
elementId,
|
||||
callback,
|
||||
elementId: currentDialog.elementId,
|
||||
callback: currentDialog.callback,
|
||||
});
|
||||
},
|
||||
popDialogStackMutation (state, result){
|
||||
|
||||
@@ -25,13 +25,20 @@
|
||||
class="mx-3"
|
||||
style="flex-grow: 0; height: 32px;"
|
||||
/>
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
slot="extension"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
/>
|
||||
<insert-library-node-button
|
||||
v-if="libraryId"
|
||||
style="bottom: -32px"
|
||||
v-if="libraryId && canEditLibrary"
|
||||
slot="extension"
|
||||
style="bottom: -24px"
|
||||
fab
|
||||
:library-id="libraryId"
|
||||
:selected-node-id="selected"
|
||||
@selected="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}"
|
||||
:selected-node-id="selectedNodeId"
|
||||
@selected="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<div
|
||||
@@ -41,8 +48,9 @@
|
||||
<library-contents-container
|
||||
:library-id="libraryId"
|
||||
:organize-mode="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
should-subscribe
|
||||
:filter="filter"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
</div>
|
||||
@@ -50,8 +58,9 @@
|
||||
v-else
|
||||
edit-mode
|
||||
:organize-mode="organize"
|
||||
:selected-node-id="selected"
|
||||
:selected-node="selectedNode"
|
||||
style="overflow-y: auto; padding: 12px;"
|
||||
:filter="filter"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
</div>
|
||||
@@ -61,10 +70,10 @@
|
||||
style="overflow: hidden;"
|
||||
>
|
||||
<library-node-dialog
|
||||
:_id="selected"
|
||||
:_id="selectedNodeId"
|
||||
embedded
|
||||
@removed="selected = undefined"
|
||||
@duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selected = id}"
|
||||
@removed="selectedNodeId = undefined"
|
||||
@duplicated="id => {if ($vuetify.breakpoint.mdAndUp) selectedNodeId = id}"
|
||||
/>
|
||||
</div>
|
||||
</tree-detail-layout>
|
||||
@@ -82,6 +91,7 @@ import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -90,6 +100,7 @@ export default {
|
||||
LibraryNodeDialog,
|
||||
LibraryContentsContainer,
|
||||
InsertLibraryNodeButton,
|
||||
TreeSearchInput,
|
||||
},
|
||||
props: {
|
||||
selection: Boolean,
|
||||
@@ -100,7 +111,8 @@ export default {
|
||||
},
|
||||
data(){ return {
|
||||
organize: false,
|
||||
selected: undefined,
|
||||
selectedNodeId: undefined,
|
||||
filter: undefined,
|
||||
};},
|
||||
computed: {
|
||||
isToolbarDark(){
|
||||
@@ -120,12 +132,12 @@ export default {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-edit-dialog',
|
||||
elementId: 'selected-node-card',
|
||||
data: {_id: this.selected},
|
||||
data: {_id: this.selectedNodeId},
|
||||
});
|
||||
},
|
||||
clickNode(id){
|
||||
if (this.$vuetify.breakpoint.mdAndUp){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
} else {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
@@ -136,7 +148,7 @@ export default {
|
||||
},
|
||||
callback: result => {
|
||||
if (result){
|
||||
this.selected = id;
|
||||
this.selectedNodeId = id;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -175,7 +187,7 @@ export default {
|
||||
},
|
||||
selectedNode(){
|
||||
return LibraryNodes.findOne({
|
||||
_id: this.selected,
|
||||
_id: this.selectedNodeId,
|
||||
removed: {$ne: true}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
class="ma-2"
|
||||
>
|
||||
<insert-library-node-button
|
||||
v-if="editPermission(library)"
|
||||
:library-id="library._id"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node-id="selectedNode && selectedNode._id"
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
<v-btn
|
||||
@@ -45,7 +46,8 @@
|
||||
:library-id="library._id"
|
||||
:organize-mode="organizeMode && editPermission(library)"
|
||||
:edit-mode="editMode"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
:filter="filter"
|
||||
should-subscribe
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
@@ -82,8 +84,12 @@ export default {
|
||||
props: {
|
||||
organizeMode: Boolean,
|
||||
editMode: Boolean,
|
||||
selectedNodeId: {
|
||||
type: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
group="library"
|
||||
:children="libraryChildren"
|
||||
:organize="organizeMode"
|
||||
:selected-node-id="selectedNodeId"
|
||||
:selected-node="selectedNode"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="reordered"
|
||||
@reorganized="reorganized"
|
||||
@@ -29,7 +29,7 @@
|
||||
<script lang="js">
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { nodesToTree } from '/imports/api/parenting/parenting.js'
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
|
||||
@@ -38,10 +38,20 @@
|
||||
TreeNodeList,
|
||||
},
|
||||
props: {
|
||||
libraryId: String,
|
||||
libraryId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
organizeMode: Boolean,
|
||||
selectedNodeId: String,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
shouldSubscribe: Boolean,
|
||||
filter: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
slowShouldSubscribe: this.shouldSubscribe,
|
||||
@@ -76,7 +86,13 @@
|
||||
},
|
||||
libraryChildren(){
|
||||
if (!this.library) return;
|
||||
return nodesToTree({collection: LibraryNodes, ancestorId: this.library._id});
|
||||
return nodesToTree({
|
||||
collection: LibraryNodes,
|
||||
ancestorId: this.library._id,
|
||||
filter: this.filter,
|
||||
includeFilteredDocAncestors: true,
|
||||
includeFilteredDocDescendants: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
61
app/imports/ui/library/LibraryNodeExpansionContent.vue
Normal file
61
app/imports/ui/library/LibraryNodeExpansionContent.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<component
|
||||
:is="model.type"
|
||||
:model="model"
|
||||
class="property-viewer"
|
||||
/>
|
||||
<tree-node-list
|
||||
group="library-node-expansion"
|
||||
:children="propertyChildren"
|
||||
@selected="clickChild"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeList,
|
||||
...propertyViewerIndex,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickChild(id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: `tree-node-${id}`,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
descendantLibraryNodes(){
|
||||
return [this.model._id];
|
||||
},
|
||||
},
|
||||
propertyChildren(){
|
||||
return nodesToTree({
|
||||
collection: LibraryNodes,
|
||||
ancestorId: this.model._id
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -53,15 +53,16 @@ export default {
|
||||
subscribed(){
|
||||
let libraryId = this.$route.params.id;
|
||||
let user = Meteor.user();
|
||||
if (!user) return false;
|
||||
let subs = user.subscribedLibraries;
|
||||
return subs && subs.includes(libraryId);
|
||||
return user?.subscribedLibraries?.includes(libraryId);
|
||||
},
|
||||
showSubscribeButton(){
|
||||
let userId = Meteor.userId();
|
||||
let user = Meteor.user();
|
||||
let library = this.library;
|
||||
if (!library) return;
|
||||
if (
|
||||
if (!user || !library) return;
|
||||
let userId = user._id;
|
||||
if (user.subscribedLibraries.includes(library._id)){
|
||||
return true
|
||||
} else if (
|
||||
library.readers.includes(userId) ||
|
||||
library.writers.includes(userId) ||
|
||||
library.owner === userId
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<v-menu
|
||||
v-if="context.creatureId"
|
||||
transition="slide-y-transition"
|
||||
lazy
|
||||
:disabled="!context.editPermission"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
>
|
||||
$vuetify.icons.weight
|
||||
</v-icon>
|
||||
{{ (model.contentsWeightless ? 0 : model.contentsWeight || 0) + (model.weight || 0) }}
|
||||
{{ weight }}
|
||||
</v-toolbar-title>
|
||||
<v-toolbar-title
|
||||
class="layout align-center"
|
||||
@@ -31,7 +31,7 @@
|
||||
$vuetify.icons.two_coins
|
||||
</v-icon>
|
||||
<coin-value
|
||||
:value="(model.contentsValue || 0) + (model.value || 0)"
|
||||
:value="value"
|
||||
/>
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
@@ -49,6 +49,7 @@ import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CoinValue from '/imports/ui/components/CoinValue.vue';
|
||||
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -62,6 +63,20 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
weight(){
|
||||
const contentWeight = this.model.contentsWeightless ?
|
||||
0 :
|
||||
this.model.contentsWeight || 0;
|
||||
const ownWeight = this.model.weight || 0;
|
||||
return stripFloatingPointOddities(contentWeight + ownWeight);
|
||||
},
|
||||
value(){
|
||||
const contentValue = this.model.contentsValue || 0;
|
||||
const ownValue = this.model.value || 0;
|
||||
return contentValue + ownValue;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickContainer(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
v-for="item in dataItems"
|
||||
:key="item._id"
|
||||
class="item"
|
||||
:data-id="`item-list-tile-${item._id}`"
|
||||
:data-id="item._id"
|
||||
:model="item"
|
||||
@click="clickProperty(item._id)"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
clickProperty(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `item-list-tile-${_id}`,
|
||||
elementId: _id,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
prepend-inner-icon="mdi-search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
hide-details
|
||||
:value="searchValue"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
label="Roll bonus"
|
||||
:value="model.rollBonus"
|
||||
:error-messages="errors.rollBonus"
|
||||
hint="A number (or calculation which returns a number) that is added to a d20 when making the attack roll"
|
||||
@change="change('rollBonus', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :errors="model.rollBonusErrors" />
|
||||
@@ -67,9 +68,11 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
<!--
|
||||
<smart-select
|
||||
label="Target"
|
||||
style="flex-basis: 300px;"
|
||||
@@ -79,6 +82,7 @@
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('target', ...arguments)"
|
||||
/>
|
||||
-->
|
||||
<div class="layout wrap">
|
||||
<text-field
|
||||
label="Uses"
|
||||
@@ -92,7 +96,7 @@
|
||||
<text-field
|
||||
label="Uses used"
|
||||
type="number"
|
||||
hint="How many times this action has already been used"
|
||||
hint="How many times this action has already been used: should be 0 in most cases"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.usesUsed"
|
||||
:error-messages="errors.uses"
|
||||
@@ -102,6 +106,7 @@
|
||||
<smart-select
|
||||
label="Reset"
|
||||
clearable
|
||||
hint="When number of uses used should be reset to zero"
|
||||
style="flex-basis: 300px;"
|
||||
:items="resetOptions"
|
||||
:value="model.reset"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
@change="change('stat', ...arguments)"
|
||||
/>
|
||||
<text-field
|
||||
label="Damage"
|
||||
hint="The amount of damage to apply to the selected stat, can be a calculation or roll"
|
||||
label="Amount"
|
||||
hint="The amount of damage to apply to the selected stat, can be a calculation or roll. Negative values will restore the selected from previous damage. If the operation is set, this is the final value of the stat instead."
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.amount"
|
||||
:error-messages="errors.amount"
|
||||
@@ -23,6 +23,7 @@
|
||||
label="Operation"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
hint="Should the attribute be damaged by the amount, or set to the amount"
|
||||
:items="adjustmentOps"
|
||||
:value="model.operation"
|
||||
:error-messages="errors.operation"
|
||||
@@ -43,6 +44,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
<text-field
|
||||
label="Quantity"
|
||||
hint="How much of the attribute will be consumed"
|
||||
hint="How much of the attribute will be consumed. If this amount is not available in the attribute, the action can't be taken"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.quantity"
|
||||
:error-messages="errors.quantity"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
ref="focusFirst"
|
||||
label="Base Value"
|
||||
class="base-value-field"
|
||||
hint="This is the value of the attribute before effects are applied"
|
||||
hint="This is the value of the attribute before effects are applied. Can be a number or a calculation"
|
||||
style="width: 332px;"
|
||||
:value="model.baseValueCalculation"
|
||||
:error-messages="errors.baseValueCalculation"
|
||||
@@ -24,7 +24,7 @@
|
||||
label="Variable name"
|
||||
:value="model.variableName"
|
||||
style="flex-basis: 300px;"
|
||||
hint="Use this name in formulae to reference this attribute"
|
||||
hint="Use this name in calculations to reference this attribute"
|
||||
:error-messages="errors.variableName"
|
||||
@change="change('variableName', ...arguments)"
|
||||
/>
|
||||
@@ -71,6 +71,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
@@ -92,7 +93,7 @@
|
||||
type="number"
|
||||
class="damage-field text-center"
|
||||
style="max-width: 300px;"
|
||||
hint="The attribute's final value is reduced by this amount"
|
||||
hint="The attribute's final value is reduced by this amount. Attribute damage can increase this value until it matches the attribute's computed value. Should mostly be left blank."
|
||||
:disabled="!context.isLibraryForm"
|
||||
:value="model.damage"
|
||||
:error-messages="errors.damage"
|
||||
@@ -106,6 +107,7 @@
|
||||
label="Reset"
|
||||
clearable
|
||||
style="flex-basis: 300px;"
|
||||
hint="When damage should be reset to zero"
|
||||
:items="resetOptions"
|
||||
:value="model.reset"
|
||||
:error-messages="errors.reset"
|
||||
@@ -123,14 +125,14 @@
|
||||
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
components: {
|
||||
FormSection,
|
||||
CalculationErrorList,
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
data(){
|
||||
let data = {
|
||||
attributeTypes: [
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
label="Class variable name"
|
||||
:value="model.variableName"
|
||||
style="flex-basis: 300px;"
|
||||
hint="This should be the same for each level in a class"
|
||||
hint="This should be the same for each level in a class, use `variablName.level` to reference the highest class level for a given class variable name in calculations"
|
||||
:error-messages="errors.variableName"
|
||||
@change="change('variableName', ...arguments)"
|
||||
/>
|
||||
@@ -50,6 +50,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
label="Variable name"
|
||||
:value="model.variableName"
|
||||
style="flex-basis: 300px;"
|
||||
hint="Use this name in formulae to reference this attribute"
|
||||
hint="Use this name in calculations to reference this attribute"
|
||||
:error-messages="errors.variableName"
|
||||
@change="change('variableName', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
<text-field
|
||||
label="Value"
|
||||
hint="Calculation of the constant value, use 'text' for a string value, [1,2,3] for a matrix, or 123 for a number"
|
||||
:value="model.calculation"
|
||||
:error-messages="errors.calculation"
|
||||
@change="change('calculation', ...arguments)"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<smart-switch
|
||||
label="Carried"
|
||||
class="mx-3"
|
||||
hint="Whether this container and its contents count towards the creature's weight carried"
|
||||
:value="model.carried"
|
||||
:error-messages="errors.carried"
|
||||
@change="change('carried', ...arguments)"
|
||||
@@ -62,6 +63,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
ref="focusFirst"
|
||||
label="Damage"
|
||||
style="flex-basis: 300px;"
|
||||
hint="A caculation including dice rolls of the damge to deal to the target when activated by an action"
|
||||
:value="model.amount"
|
||||
:error-messages="errors.amount"
|
||||
@change="change('amount', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :errors="model.amountErrors" />
|
||||
<smart-select
|
||||
label="Damage Type"
|
||||
style="flex-basis: 200px;"
|
||||
hint="Use the Healing type to restore hit points"
|
||||
:items="DAMAGE_TYPES"
|
||||
:value="model.damageType"
|
||||
:error-messages="errors.damageType"
|
||||
@@ -34,6 +37,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
@@ -44,8 +48,12 @@
|
||||
<script lang="js">
|
||||
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
|
||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalculationErrorList,
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
props: {
|
||||
parentTarget: {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
label="Operation"
|
||||
append-icon="mdi-menu-down"
|
||||
class="mx-2"
|
||||
:hint="operationHint"
|
||||
:error-messages="errors.operation"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:items="operations"
|
||||
@@ -42,6 +43,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Which stats will this effect apply to"
|
||||
:value="model.stats"
|
||||
:items="attributeList"
|
||||
:error-messages="errors.stats"
|
||||
@@ -50,13 +52,21 @@
|
||||
<text-field
|
||||
label="Value"
|
||||
class="mr-2"
|
||||
hint="Number or calculation to determine the value of this effect"
|
||||
:persistent-hint="needsValue"
|
||||
:value="needsValue ? (model.calculation) : ' '"
|
||||
:disabled="!needsValue"
|
||||
:error-messages="errors.calculation"
|
||||
:hint="!isFinite(model.calculation) && model.result ? model.result + '' : '' "
|
||||
@change="change('calculation', ...arguments)"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<text-field
|
||||
v-if="!isFinite(+model.calculation) && model.result !== undefined"
|
||||
disabled
|
||||
label="Result"
|
||||
:value="model.result"
|
||||
/>
|
||||
</v-expand-transition>
|
||||
<calculation-error-list :errors="model.errors" />
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
@@ -100,7 +110,7 @@
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
needsValue(){
|
||||
needsValue(){
|
||||
switch(this.model.operation) {
|
||||
case 'base': return true;
|
||||
case 'add': return true;
|
||||
@@ -117,6 +127,23 @@
|
||||
default: return true;
|
||||
}
|
||||
},
|
||||
operationHint(){
|
||||
switch(this.model.operation) {
|
||||
case 'base': return 'Stats take their largest base value, and then apply all other effects';
|
||||
case 'add': return 'Add this vaulue to the stat';
|
||||
case 'mul': return 'Multiply the stat by this value';
|
||||
case 'min': return 'The stat will be at least this value';
|
||||
case 'max': return 'The stat will not exceed this value';
|
||||
case 'set': return 'The stat will be set to this value';
|
||||
case 'advantage': return 'If this stat is the basis for a check, that check will be at advantage';
|
||||
case 'disadvantage': return 'If this stat is the basis for a check, that check will be at advantage';
|
||||
case 'passiveAdd': return 'This value will be added to the passive check';
|
||||
case 'fail': return 'Stat based on this attribute will always fail';
|
||||
case 'conditional': return 'Add a text note to this stat';
|
||||
case 'rollBonus': return 'Add this value to rolls based on this stat';
|
||||
default: return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'model.operation': {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
<text-field
|
||||
label="Quantity"
|
||||
hint="How much of the item will be consumed"
|
||||
hint="How many will be consumed"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.quantity"
|
||||
:error-messages="errors.quantity"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
label="Plural name"
|
||||
:value="model.plural"
|
||||
:error-messages="errors.plural"
|
||||
hint="The plural name of your item. If your item's name is 'sword' plural name would be 'swords'"
|
||||
@change="change('plural', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
@@ -55,6 +56,7 @@
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
prepend-inner-icon="$vuetify.icons.weight"
|
||||
hint="The weight of a single item in lbs. Can be a decimal value"
|
||||
:value="model.weight"
|
||||
:error-messages="errors.weight"
|
||||
@change="change('weight', ...arguments)"
|
||||
@@ -87,6 +89,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<text-area
|
||||
label="Summary"
|
||||
hint="This will appear in the action card in the character sheet"
|
||||
hint="This will appear in the card in the character sheet"
|
||||
:value="model.summary"
|
||||
:error-messages="errors.summary"
|
||||
@change="change('summary', ...arguments)"
|
||||
@@ -19,18 +19,19 @@
|
||||
|
||||
<text-area
|
||||
label="Description"
|
||||
hint="Text that does not fit in the summary"
|
||||
:value="model.description"
|
||||
:error-messages="errors.description"
|
||||
@change="change('description', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :calculations="model.descriptionCalculations" />
|
||||
|
||||
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Which skills does this proficiency apply to"
|
||||
:value="model.stats"
|
||||
:items="skillList"
|
||||
:error-messages="errors.stats"
|
||||
@@ -32,6 +33,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
<template lang="html">
|
||||
<div class="folder-form layout justify-start wrap">
|
||||
<v-text-field
|
||||
label="Linked Property"
|
||||
style="flex-basis: 300px;"
|
||||
readonly
|
||||
outlined
|
||||
persistent-hint
|
||||
:loading="linkLoading"
|
||||
:value="
|
||||
model.cache.node && model.cache.node.name ||
|
||||
model.ref && model.ref.id
|
||||
"
|
||||
:hint="model.cache.library && model.cache.library.name"
|
||||
:error-messages="model.cache.error || errors.ref"
|
||||
prepend-inner-icon="mdi-vector-link"
|
||||
append-icon="mdi-refresh"
|
||||
data-id="change-ref"
|
||||
@click="changeReference"
|
||||
@click:prepend-inner="changeReference"
|
||||
@click:append="updateReferenceNode"
|
||||
/>
|
||||
<div data-id="change-ref">
|
||||
<v-input
|
||||
:label="model.cache && model.cache.node ? '' : 'Linked Property'"
|
||||
style="flex-basis: 300px; cursor: pointer;"
|
||||
readonly
|
||||
outlined
|
||||
persistent-hint
|
||||
:loading="linkLoading"
|
||||
:value="
|
||||
model.cache.node && model.cache.node.name ||
|
||||
model.ref && model.ref.id
|
||||
"
|
||||
:hint="model.cache.library && model.cache.library.name"
|
||||
:error-messages="model.cache.error || errors.ref"
|
||||
prepend-icon="mdi-vector-link"
|
||||
append-icon="mdi-refresh"
|
||||
@click="changeReference"
|
||||
@click:prepend="changeReference"
|
||||
@click:append="updateReferenceNode"
|
||||
>
|
||||
<tree-node-view
|
||||
v-if="model && model.cache && model.cache.node"
|
||||
:model="model.cache.node"
|
||||
/>
|
||||
</v-input>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import updateReferenceNode from '/imports/api/library/methods/updateReferenceNode.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeView,
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
data(){return {
|
||||
linkLoading: false,
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
AttributesConsumedListForm,
|
||||
ItemsConsumedListForm,
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
mixins: [propertyFormMixin],
|
||||
props: {
|
||||
parentTarget: {
|
||||
type: String,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Roll"
|
||||
hint="The calculation that will be evaluated when the roll is triggered by an action. The result will be saved as the variable name in the context of the roll."
|
||||
:value="model.roll"
|
||||
:error-messages="errors.roll"
|
||||
@change="change('roll', ...arguments)"
|
||||
@@ -30,6 +31,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -8,15 +8,17 @@
|
||||
@change="change('name', ...arguments)"
|
||||
/>
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="DC"
|
||||
hint="A calculation of the DC that the target of an action needs to save against in order to succeed. If the saving throw is lower than the DC, the children of this property will be activated."
|
||||
:value="model.dc"
|
||||
:error-messages="errors.dc"
|
||||
@change="change('dc', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :errors="model.dcErrors" />
|
||||
|
||||
<smart-combobox
|
||||
label="Save"
|
||||
hint="Which save the saving throw targets"
|
||||
hint="Which stat the saving throw targets"
|
||||
:value="model.stat"
|
||||
:items="saveList"
|
||||
:error-messages="errors.stat"
|
||||
@@ -37,6 +39,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
:value="model.skillType"
|
||||
:error-messages="errors.skillType"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
:hint="skillTypeHints[model.skillType]"
|
||||
@change="change('skillType', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
@change="change('description', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :calculations="model.descriptionCalculations" />
|
||||
|
||||
|
||||
<form-section
|
||||
name="Advanced"
|
||||
standalone
|
||||
@@ -118,7 +119,17 @@
|
||||
text: 'Utility',
|
||||
value: 'utility',
|
||||
},
|
||||
]
|
||||
],
|
||||
skillTypeHints: {
|
||||
skill: 'A normal character sheet skill like Athletics, Deception, or Investigation',
|
||||
'save': 'A saving throw the character can make: Strength Save, etc.',
|
||||
'check': 'An ability check that might include a proficiency bonus later eg. Initiative',
|
||||
'tool': 'A tool proficiency. Be sure to add a base proficiency in the advanced section.',
|
||||
'weapon': 'A weapon proficiency. Be sure to add a base proficiency in the advanced section.',
|
||||
'armor': 'A armor proficiency. Be sure to add a base proficiency in the advanced section.',
|
||||
'language': 'A language proficiency. Be sure to add a base proficiency in the advanced section.',
|
||||
'utility': 'A skill that does not show up in the sheet, but can be used by other caclulations',
|
||||
}
|
||||
};},
|
||||
meteor: {
|
||||
abilityScoreList(){
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
label="Type"
|
||||
style="flex-basis: 300px;"
|
||||
clearable
|
||||
hint="The property type that this slot filler pretends to be when being searched for by a slot"
|
||||
:items="slotTypes"
|
||||
:value="model.slotFillerType"
|
||||
:error-messages="errors.slotFillerType"
|
||||
@@ -62,6 +63,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this slot filler in a library"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
label="Type"
|
||||
style="flex-basis: 300px;"
|
||||
clearable
|
||||
hint="What property type is needed to fill this slot"
|
||||
:items="slotTypes"
|
||||
:value="model.slotType"
|
||||
:error-messages="errors.slotType"
|
||||
@@ -33,6 +34,7 @@
|
||||
:error-messages="errors.quantityExpected"
|
||||
@change="change('quantityExpected', ...arguments)"
|
||||
/>
|
||||
<calculation-error-list :errors="model.quantityExpectedErrors" />
|
||||
<text-field
|
||||
label="Condition"
|
||||
hint="A caclulation to determine if this slot should be active"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
label="Level"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
hint="The spell level"
|
||||
:items="spellLevels"
|
||||
:value="model.level"
|
||||
:error-messages="errors.level"
|
||||
@@ -110,6 +111,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
:error-messages="errors.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
:error-messages="errors.name"
|
||||
@change="change('name', ...arguments)"
|
||||
/>
|
||||
<text-field
|
||||
label="Variable name"
|
||||
:value="model.variableName"
|
||||
style="flex-basis: 300px;"
|
||||
hint="This name is used by spells to reference which lists they appear on"
|
||||
:error-messages="errors.variableName"
|
||||
@change="change('variableName', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<text-area
|
||||
@@ -58,6 +50,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<text-field
|
||||
v-show="radioSelection === 'calculated'"
|
||||
label="Condition"
|
||||
hint="When this calculation returns a value that isn't false or zero the children will be active"
|
||||
:value="model.condition"
|
||||
:error-messages="errors.condition"
|
||||
@change="change('condition', ...arguments)"
|
||||
@@ -44,6 +45,7 @@
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
|
||||
57
app/imports/ui/properties/shared/PropertySelectCard.vue
Normal file
57
app/imports/ui/properties/shared/PropertySelectCard.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template lang="html">
|
||||
<v-card
|
||||
hover
|
||||
style="height: 100%; overflow: hidden;"
|
||||
@click="e => $emit('click', e)"
|
||||
>
|
||||
<v-card-title
|
||||
class="subtitle pb-3"
|
||||
style="text-align: center;"
|
||||
>
|
||||
<v-avatar tile>
|
||||
<v-icon x-large>
|
||||
{{ property.icon }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<span class="ml-3">
|
||||
{{ property.name }}
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-if="showPropertyHelp"
|
||||
class="mx-4"
|
||||
>
|
||||
{{ property.helpText }}
|
||||
<div style="height: 16px;" />
|
||||
<div
|
||||
v-if="property.examples"
|
||||
class="text-caption"
|
||||
>
|
||||
{{ property.examples }}
|
||||
<div style="height: 16px;" />
|
||||
</div>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
export default {
|
||||
props: {
|
||||
property: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
showPropertyHelp(){
|
||||
let user = Meteor.user();
|
||||
return !(user?.preferences?.hidePropertySelectDialogHelp)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -2,57 +2,115 @@
|
||||
<div class="card-raised-background">
|
||||
<v-container
|
||||
fluid
|
||||
grid-list-lg
|
||||
fill-height
|
||||
>
|
||||
<v-layout
|
||||
<v-row
|
||||
wrap
|
||||
fill-height
|
||||
dense
|
||||
justify="center"
|
||||
justify-sm="start"
|
||||
>
|
||||
<template v-for="(property, type) in PROPERTIES">
|
||||
<v-flex
|
||||
<template v-if="properties.suggested">
|
||||
<v-col
|
||||
cols="12"
|
||||
>
|
||||
<v-subheader>
|
||||
Suggested
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<template v-for="(property, type) in properties.suggested">
|
||||
<v-col
|
||||
v-if="!noLibraryOnlyProps || !property.libraryOnly"
|
||||
:key="type"
|
||||
md="4"
|
||||
sm="6"
|
||||
cols="10"
|
||||
>
|
||||
<property-select-card
|
||||
:property="property"
|
||||
@click="$emit('select', type)"
|
||||
/>
|
||||
</v-col>
|
||||
</template>
|
||||
</template>
|
||||
<v-col
|
||||
v-if="properties.suggested"
|
||||
cols="12"
|
||||
>
|
||||
<v-subheader>
|
||||
More
|
||||
</v-subheader>
|
||||
</v-col>
|
||||
<template v-for="(property, type) in properties.more">
|
||||
<v-col
|
||||
v-if="!noLibraryOnlyProps || !property.libraryOnly"
|
||||
:key="type"
|
||||
sm4
|
||||
xs6
|
||||
md="4"
|
||||
sm="6"
|
||||
cols="10"
|
||||
>
|
||||
<v-card
|
||||
hover
|
||||
style="height: 100%; overflow: hidden;"
|
||||
<property-select-card
|
||||
:property="property"
|
||||
@click="$emit('select', type)"
|
||||
>
|
||||
<div
|
||||
class="layout align-center justify-center"
|
||||
style="min-height: 70px;"
|
||||
>
|
||||
<v-icon x-large>
|
||||
{{ property.icon }}
|
||||
</v-icon>
|
||||
</div>
|
||||
<h3
|
||||
class="subtitle pb-3"
|
||||
style="text-align: center;"
|
||||
>
|
||||
{{ property.name }}
|
||||
</h3>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
/>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-layout>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
import PropertySelectCard from '/imports/ui/properties/shared/PropertySelectCard.vue';
|
||||
export default {
|
||||
components: {
|
||||
PropertySelectCard,
|
||||
},
|
||||
props: {
|
||||
noLibraryOnlyProps: Boolean,
|
||||
parentType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedTypes: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
PROPERTIES,
|
||||
};},
|
||||
computed:{
|
||||
properties(){
|
||||
let suggested;
|
||||
let more = {};
|
||||
if (this.suggestedTypes){
|
||||
for (const key in PROPERTIES){
|
||||
let prop = PROPERTIES[key];
|
||||
if (this.suggestedTypes.includes(prop.type)){
|
||||
if (!suggested) suggested = {};
|
||||
suggested[key] = prop;
|
||||
} else {
|
||||
more[key] = prop;
|
||||
}
|
||||
}
|
||||
return {suggested, more};
|
||||
} else if (this.parentType) {
|
||||
for (const key in PROPERTIES){
|
||||
let prop = PROPERTIES[key];
|
||||
if (prop.suggestedParents.includes(this.parentType)){
|
||||
if (!suggested) suggested = {};
|
||||
suggested[key] = prop;
|
||||
} else {
|
||||
more[key] = prop;
|
||||
}
|
||||
}
|
||||
return {suggested, more};
|
||||
} else {
|
||||
return {more: PROPERTIES};
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,12 +5,23 @@
|
||||
key="left"
|
||||
class="step-1"
|
||||
>
|
||||
<v-toolbar-title slot="toolbar">
|
||||
Property Type
|
||||
</v-toolbar-title>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Property Type
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
:input-value="showPropertyHelp"
|
||||
append-icon="mdi-help"
|
||||
hide-details
|
||||
flat
|
||||
@change="propertyHelpChanged"
|
||||
/>
|
||||
</template>
|
||||
<property-selector
|
||||
slot="unwrapped-content"
|
||||
:no-library-only-props="noLibraryOnlyProps"
|
||||
:parent-type="parentType"
|
||||
@select="type => $emit('input', type)"
|
||||
/>
|
||||
</dialog-base>
|
||||
@@ -28,6 +39,7 @@
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -38,8 +50,33 @@ export default {
|
||||
noLibraryOnlyProps: Boolean,
|
||||
value: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
parentType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
showPropertyHelp(){
|
||||
let user = Meteor.user();
|
||||
return !(user?.preferences?.hidePropertySelectDialogHelp)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
propertyHelpChanged(value){
|
||||
Meteor.users.setPreference.call({
|
||||
preference: 'hidePropertySelectDialogHelp',
|
||||
value: !value
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:value="model.value"
|
||||
/>
|
||||
<div class="text-no-wrap text-truncate">
|
||||
<template v-if="model.stats && model.stats.length">
|
||||
<template v-if="!model.name && model.stats && model.stats.length">
|
||||
{{ model.stats.join(', ') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
:color="model.color"
|
||||
:class="selected && 'primary--text'"
|
||||
/>
|
||||
<div class="text-no-wrap text-truncate">
|
||||
<tree-node-view
|
||||
v-if="model.cache && model.cache.node && model.cache.node.type !== 'reference'"
|
||||
:model="model.cache.node"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="text-no-wrap text-truncate"
|
||||
>
|
||||
{{ model.cache.node && model.cache.node.name || title }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,8 +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,
|
||||
},
|
||||
mixins: [treeNodeViewMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -134,6 +134,7 @@ import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyV
|
||||
import CoinValue from '/imports/ui/components/CoinValue.vue';
|
||||
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
||||
import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
|
||||
|
||||
export default {
|
||||
components:{
|
||||
@@ -146,10 +147,10 @@ export default {
|
||||
},
|
||||
computed:{
|
||||
totalValue(){
|
||||
return this.model.value * this.model.quantity;
|
||||
return stripFloatingPointOddities(this.model.value * this.model.quantity);
|
||||
},
|
||||
totalWeight(){
|
||||
return this.model.weight * this.model.quantity;
|
||||
return stripFloatingPointOddities(this.model.weight * this.model.quantity);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
name="Error"
|
||||
:value="model.cache.error"
|
||||
/>
|
||||
<property-field
|
||||
v-else-if="model.ref && model.ref.id"
|
||||
name="Linked Property"
|
||||
:value="model.cache.node && model.cache.node.name || model.ref.id"
|
||||
/>
|
||||
<template v-else-if="model.ref && model.ref.id">
|
||||
<div class="text-caption">
|
||||
Linked Property
|
||||
</div>
|
||||
<tree-node-view :model="model.cache.node" />
|
||||
</template>
|
||||
<property-field
|
||||
v-if="model.cache.library && model.cache.library.name"
|
||||
name="Library"
|
||||
@@ -20,8 +21,12 @@
|
||||
|
||||
<script lang="js">
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeView,
|
||||
},
|
||||
mixins: [propertyViewerMixin],
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template lang="html">
|
||||
<div class="spell-viewer">
|
||||
<property-name :value="model.name" />
|
||||
<p class="text-caption">
|
||||
{{ levelAndSchool }}
|
||||
</p>
|
||||
<property-field
|
||||
name="Casting time"
|
||||
:value="model.castingTime"
|
||||
@@ -10,12 +13,12 @@
|
||||
:value="model.range"
|
||||
/>
|
||||
<property-field
|
||||
name="Duration"
|
||||
:value="model.duration"
|
||||
name="Components"
|
||||
:value="spellComponents"
|
||||
/>
|
||||
<property-field
|
||||
name="Level"
|
||||
:value="`${model.level} ${model.school}`"
|
||||
name="Duration"
|
||||
:value="model.duration"
|
||||
/>
|
||||
<property-description
|
||||
:string="model.description"
|
||||
@@ -26,9 +29,33 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
||||
|
||||
const levelText = [
|
||||
'cantrip', '1st-level', '2nd-level', '3rd-level', '4th-level', '5th-level',
|
||||
'6th-level', '7th-level', '8th-level', '9th-level'
|
||||
];
|
||||
|
||||
export default {
|
||||
mixins: [propertyViewerMixin],
|
||||
computed:{
|
||||
levelAndSchool(){
|
||||
if (this.model.level == 0){
|
||||
return `${this.model.school} ${levelText[0]}`
|
||||
} else {
|
||||
return `${levelText[this.model.level]} ${this.model.school}`
|
||||
}
|
||||
},
|
||||
spellComponents(){
|
||||
let components = [];
|
||||
if (this.model.ritual) components.push('Ritual');
|
||||
if (this.model.concentration) components.push('Concentration');
|
||||
if (this.model.verbal) components.push('Verbal');
|
||||
if (this.model.somatic) components.push('Somatic');
|
||||
if (this.model.material) components.push(`Material (${this.model.material})`);
|
||||
return components.join(', ');
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
3
app/imports/ui/utility/stripFloatingPointOddities.js
Normal file
3
app/imports/ui/utility/stripFloatingPointOddities.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function stripFloatingPointOddities(num, precision = 12){
|
||||
return +parseFloat(num.toPrecision(precision))
|
||||
}
|
||||
1473
app/package-lock.json
generated
1473
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
"npm": "6.13.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"@chenfengyuan/vue-countdown": "^1.1.5",
|
||||
"@tozd/vue-observer-utils": "^0.5.0",
|
||||
"animejs": "^2.2.0",
|
||||
@@ -32,7 +32,7 @@
|
||||
"ignore-styles": "^5.0.1",
|
||||
"lodash": "^4.17.20",
|
||||
"marked": "^0.8.2",
|
||||
"meteor-node-stubs": "^1.0.3",
|
||||
"meteor-node-stubs": "^1.1.0",
|
||||
"moo": "^0.5.1",
|
||||
"nearley": "^2.19.1",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -46,13 +46,13 @@
|
||||
"vue-reactive-provide": "^0.3.0",
|
||||
"vue-router": "^3.5.2",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuetify": "^2.5.6",
|
||||
"vuetify": "^2.5.7",
|
||||
"vuetify-upload-button": "^2.0.2",
|
||||
"vuex": "^3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-vue": "^7.13.0",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-plugin-vue": "^7.14.0",
|
||||
"eslint-plugin-vuetify": "^1.0.1",
|
||||
"mem": "^6.1.1",
|
||||
"sass": "^1.35.2"
|
||||
@@ -88,10 +88,11 @@
|
||||
}
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"env": {
|
||||
"es6": true,
|
||||
"es2021": true,
|
||||
"es2020": true,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"meteor": true
|
||||
|
||||
95
app/public/sw.js
Normal file
95
app/public/sw.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const HTMLToCache = '/';
|
||||
const version = 'MSW V0.3';
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(caches.open(version).then((cache) => {
|
||||
cache.add(HTMLToCache).then(self.skipWaiting());
|
||||
}));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => Promise.all(cacheNames.map((cacheName) => {
|
||||
if (version !== cacheName) return caches.delete(cacheName);
|
||||
}))).then(self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// only processes http:// & https:// requests, prevents chrome-extention:// errors
|
||||
if (event.request.url.startsWith('http')){
|
||||
|
||||
const requestToFetch = event.request.clone();
|
||||
event.respondWith(
|
||||
caches.match(event.request.clone()).then((cached) => {
|
||||
// We don't return cached HTML (except if fetch failed)
|
||||
if (cached) {
|
||||
const resourceType = cached.headers.get('content-type');
|
||||
// We only return non css/js/html cached response e.g images
|
||||
if (!hasHash(event.request.url) && !/text\/html/.test(resourceType)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// If the CSS/JS didn't change since it's been cached, return the cached version
|
||||
if (hasHash(event.request.url) && hasSameHash(event.request.url, cached.url)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return fetch(requestToFetch).then((response) => {
|
||||
const clonedResponse = response.clone();
|
||||
const contentType = clonedResponse.headers.get('content-type');
|
||||
|
||||
if (!clonedResponse || clonedResponse.status !== 200 || clonedResponse.type !== 'basic'
|
||||
|| /\/sockjs\//.test(event.request.url)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (/html/.test(contentType)) {
|
||||
caches.open(version).then(cache => cache.put(HTMLToCache, clonedResponse));
|
||||
} else {
|
||||
// Delete old version of a file
|
||||
if (hasHash(event.request.url)) {
|
||||
caches.open(version).then(cache => cache.keys().then(keys => keys.forEach((asset) => {
|
||||
if (new RegExp(removeHash(event.request.url)).test(removeHash(asset.url))) {
|
||||
cache.delete(asset);
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
caches.open(version).then(cache => cache.put(event.request, clonedResponse));
|
||||
}
|
||||
return response;
|
||||
}).catch(() => {
|
||||
if (hasHash(event.request.url)) return caches.match(event.request.url);
|
||||
// If the request URL hasn't been served from cache and isn't sockjs we suppose it's HTML
|
||||
else if (!/\/sockjs\//.test(event.request.url)) return caches.match(HTMLToCache);
|
||||
// Only for sockjs
|
||||
return new Response('No connection to the server', {
|
||||
status: 503,
|
||||
statusText: 'No connection to the server',
|
||||
headers: new Headers({ 'Content-Type': 'text/plain' }),
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function removeHash(element) {
|
||||
if (typeof element === 'string') return element.split('?hash=')[0];
|
||||
}
|
||||
|
||||
function hasHash(element) {
|
||||
if (typeof element === 'string') return /\?hash=.*/.test(element);
|
||||
}
|
||||
|
||||
function hasSameHash(firstUrl, secondUrl) {
|
||||
if (typeof firstUrl === 'string' && typeof secondUrl === 'string') {
|
||||
return /\?hash=(.*)/.exec(firstUrl)[1] === /\?hash=(.*)/.exec(secondUrl)[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Service worker created by Ilan Schemoul alias NitroBAY as a specific Service Worker for Meteor
|
||||
// Please see https://github.com/NitroBAY/meteor-service-worker for the official project source
|
||||
@@ -1,7 +1,8 @@
|
||||
import '/imports/server/config/accountsEmailConfig.js';
|
||||
import '/imports/server/config/SimpleRestConfig.js';
|
||||
import '/imports/server/config/simpleSchemaDebug.js';
|
||||
import '/imports/server/config/SyncedCronConfig.js';
|
||||
import '/imports/server/publications/index.js';
|
||||
import '/imports/server/config/simpleSchemaDebug.js';
|
||||
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
||||
import '/imports/api/parenting/organizeMethods.js';
|
||||
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
||||
|
||||
Reference in New Issue
Block a user