Merge branch 'version-2-dev' into version-2-tabletop

This commit is contained in:
Stefan Zermatten
2022-04-23 15:18:03 +02:00
33 changed files with 727 additions and 156 deletions

View File

@@ -9,6 +9,7 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js'; import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
export function getArchiveObj(creatureId){ export function getArchiveObj(creatureId){
// Build the archive document // Build the archive document
@@ -31,7 +32,7 @@ export function getArchiveObj(creatureId){
return archiveCreature; return archiveCreature;
} }
export function archiveCreature(creatureId){ export function archiveCreature(creatureId, userId){
const archive = getArchiveObj(creatureId); const archive = getArchiveObj(creatureId);
const buffer = Buffer.from(JSON.stringify(archive, null, 2)); const buffer = Buffer.from(JSON.stringify(archive, null, 2));
ArchiveCreatureFiles.write(buffer, { ArchiveCreatureFiles.write(buffer, {
@@ -43,11 +44,12 @@ export function archiveCreature(creatureId){
creatureId: archive.creature._id, creatureId: archive.creature._id,
creatureName: archive.creature.name, creatureName: archive.creature.name,
}, },
}, (error) => { }, (error, file) => {
if (error){ if (error){
throw error; throw error;
} else { } else {
removeCreatureWork(creatureId); removeCreatureWork(creatureId);
incrementFileStorageUsed(userId, file.size);
} }
}, true); }, true);
} }
@@ -68,7 +70,7 @@ const archiveCreatureToFile = new ValidatedMethod({
async run({creatureId}) { async run({creatureId}) {
assertOwnership(creatureId, this.userId); assertOwnership(creatureId, this.userId);
if (Meteor.isServer){ if (Meteor.isServer){
archiveCreature(creatureId); archiveCreature(creatureId, this.userId);
} else { } else {
removeCreatureWork(creatureId); removeCreatureWork(creatureId);
} }

View File

@@ -2,3 +2,4 @@
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
import '/imports/api/creature/archive/methods/restoreCreatures.js'; import '/imports/api/creature/archive/methods/restoreCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js'; import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';

View File

@@ -0,0 +1,47 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
const removeArchiveCreature = new ValidatedMethod({
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
async run({ fileId }) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
if (!file) {
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId) {
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
},
});
export default removeArchiveCreature;

View File

@@ -8,6 +8,9 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js'; import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
let migrateArchive; let migrateArchive;
if (Meteor.isServer){ if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default; migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
@@ -69,13 +72,18 @@ const restoreCreaturefromFile = new ValidatedMethod({
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'You can only restore creatures you own'); 'You can only restore creatures you own');
} }
assertHasCharactersSlots(this.userId);
if (Meteor.isServer){ if (Meteor.isServer){
// Read the file data // Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file); const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive); restoreCreature(archive);
} }
//Remove the archive once the restore succeeded //Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({_id: fileId}); ArchiveCreatureFiles.remove({ _id: fileId });
// Update the user's file storage limits
incrementFileStorageUsed(userId, -file.size);
}, },
}); });

View File

@@ -0,0 +1,22 @@
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function assertHasCharactersSlots(userId) {
if (characterSlotsRemaining(userId) <= 0) {
throw new Meteor.Error('characterSlotLimit',
'No character slots left')
}
}
export function characterSlotsRemaining(userId) {
let tier = getUserTier(userId);
const currentCharacterCount = Creatures.find({
owner: userId,
}, {
fields: { _id: 1 },
}).count();
if (tier.characterSlots === -1) {
return Number.POSITIVE_INFINITY;
}
return tier.characterSlots - currentCharacterCount;
}

View File

@@ -2,9 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js'; import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
const insertCreature = new ValidatedMethod({ const insertCreature = new ValidatedMethod({
@@ -23,21 +23,8 @@ const insertCreature = new ValidatedMethod({
throw new Meteor.Error('Creatures.methods.insert.denied', throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature'); 'You need to be logged in to insert a creature');
} }
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({ assertHasCharactersSlots(this.userId);
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
}
// Create the creature document // Create the creature document
let creatureId = Creatures.insert({ let creatureId = Creatures.insert({

View File

@@ -0,0 +1,18 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
const UserImages = createS3FilesCollection({
collectionName: 'userImages',
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
onBeforeUpload(file) {
// Allow upload files under 10MB
if (file.size > 10485760) {
return 'Please upload with size equal or less than 10MB';
}
// Allow common image extensions
if (/gif|png|jpe?g|webp/i.test(file.extension || '')) {
return 'Please upload an image file only';
}
}
});
export default UserImages;

View File

@@ -213,9 +213,6 @@ if (Meteor.isServer && Meteor.settings.useS3) {
return collection; return collection;
} }
} else { } else {
if (Meteor.isServer){
// console.log('No S3 details specified, files will be stored in the local filesystem');
}
createS3FilesCollection = function({ createS3FilesCollection = function({
collectionName, collectionName,
storagePath, storagePath,

View File

@@ -4,6 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/methods/deleteMyAccount.js'; import '/imports/api/users/methods/deleteMyAccount.js';
import '/imports/api/users/methods/addEmail.js'; import '/imports/api/users/methods/addEmail.js';
import '/imports/api/users/methods/removeEmail.js'; import '/imports/api/users/methods/removeEmail.js';
import '/imports/api/users/methods/updateFileStorageUsed.js';
import { some } from 'lodash'; import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
@@ -84,6 +85,10 @@ const userSchema = new SimpleSchema({
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
fileStorageUsed: {
type: Number,
optional: true,
},
profile: { profile: {
type: Object, type: Object,
blackbox: true, blackbox: true,

View File

@@ -0,0 +1,71 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import UserImages from '/imports/api/files/UserImages.js';
const fileCollections = [ArchiveCreatureFiles, UserImages];
const updateFileStorageUsed = new ValidatedMethod({
name: 'users.recalculateFileStorageUsed',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
const userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged in to recalculate your file use');
const user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('noUser', 'User not found');
}
updateFileStorageUsedWork(userId);
}
});
export default updateFileStorageUsed;
export function updateFileStorageUsedWork(userId) {
if (!userId) {
throw new Meteor.Error('idRequired',
'No user ID was provided to update file storage used')
}
let sum = 0;
fileCollections.forEach(collection => {
collection.find({ userId }, { fields: { size: 1 } }).forEach(file => {
sum += file.size;
});
});
Meteor.users.update(userId, {
$set: {
fileStorageUsed: sum,
}
});
}
export function incrementFileStorageUsed(userId, amount) {
if (!userId) {
throw new Meteor.Error('idRequired',
'No user ID was provided to update file storage used')
}
const user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('noUser', 'User not found');
}
if (user.fileStorageUsed === undefined) {
// The user doesn't have a current value for storage used, calculate it
// from scratch
updateFileStorageUsedWork(userId);
} else {
Meteor.users.update(userId, {
$inc: {
fileStorageUsed: amount,
}
});
}
}

View File

@@ -9,18 +9,21 @@ const TIERS = Object.freeze([
minimumEntitledCents: 0, minimumEntitledCents: 0,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Dreamer', name: 'Dreamer',
minimumEntitledCents: 100, minimumEntitledCents: 100,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Wanderer', name: 'Wanderer',
minimumEntitledCents: 300, minimumEntitledCents: 300,
invites: 0, invites: 0,
characterSlots: 5, characterSlots: 5,
fileStorage: 50,
paidBenefits: false, paidBenefits: false,
}, { }, {
//cost per user $5 //cost per user $5
@@ -28,6 +31,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 500, minimumEntitledCents: 500,
invites: 0, invites: 0,
characterSlots: 20, characterSlots: 20,
fileStorage: 200,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.33 //cost per user $3.33
@@ -35,6 +39,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 1000, minimumEntitledCents: 1000,
invites: 2, invites: 2,
characterSlots: 50, characterSlots: 50,
fileStorage: 500,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.333 //cost per user $3.333
@@ -42,6 +47,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 2000, minimumEntitledCents: 2000,
invites: 5, invites: 5,
characterSlots: 120, characterSlots: 120,
fileStorage: 1000,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.125 //cost per user $3.125
@@ -49,6 +55,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 5000, minimumEntitledCents: 5000,
invites: 15, invites: 15,
characterSlots: -1, // Unlimited characters characterSlots: -1, // Unlimited characters
fileStorage: 2000,
paidBenefits: true, paidBenefits: true,
}, },
]); ]);
@@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({
guest: true, guest: true,
invites: 0, invites: 0,
characterSlots: 20, characterSlots: 20,
fileStorage: 200,
paidBenefits: true, paidBenefits: true,
}); });
@@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
name: 'Outlander', name: 'Outlander',
invites: 0, invites: 0,
characterSlots: -1, // Can have infinitely many characters characterSlots: -1, // Can have infinitely many characters
fileStorage: 1000000, // 1TB file storage
paidBenefits: true, paidBenefits: true,
}); });

View File

@@ -1,4 +1,4 @@
// Generated automatically by nearley, version 2.20.1 // Generated automatically by nearley, version 2.16.0
// http://github.com/Hardmath123/nearley // http://github.com/Hardmath123/nearley
function id(x) { return x[0]; } function id(x) { return x[0]; }
@@ -85,26 +85,24 @@ let ParserRules = [
d => node.call.create({functionName: d[0].name, args: d[2]}) d => node.call.create({functionName: d[0].name, args: d[2]})
}, },
{"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id}, {"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id},
{"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arguments$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arguments$ebnf$1", "symbols": ["arguments$ebnf$1$subexpression$1"], "postprocess": id}, {"name": "arguments$ebnf$1", "symbols": []},
{"name": "arguments$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]},
{"name": "arguments$ebnf$2", "symbols": []}, {"name": "arguments$ebnf$1", "symbols": ["arguments$ebnf$1", "arguments$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, {"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$subexpression$1", "arguments$ebnf$1", "_", {"literal":")"}], "postprocess":
{"name": "arguments$ebnf$2", "symbols": ["arguments$ebnf$2", "arguments$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$ebnf$1", "arguments$ebnf$2", "_", {"literal":")"}], "postprocess":
d => [d[2], ...d[3]] d => [d[2], ...d[3]]
}, },
{"name": "arguments", "symbols": [{"literal":"("}, "_", {"literal":")"}], "postprocess": d => []},
{"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})}, {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})},
{"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id}, {"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id},
{"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arrayExpression$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1$subexpression$1"], "postprocess": id}, {"name": "arrayExpression$ebnf$1", "symbols": []},
{"name": "arrayExpression$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}}, {"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]},
{"name": "arrayExpression$ebnf$2", "symbols": []}, {"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1", "arrayExpression$ebnf$1$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "arrayExpression$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, {"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$subexpression$1", "arrayExpression$ebnf$1", "_", {"literal":"]"}], "postprocess":
{"name": "arrayExpression$ebnf$2", "symbols": ["arrayExpression$ebnf$2", "arrayExpression$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, d => node.array.create({ values: [d[2], ...d[3]] })
{"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$ebnf$1", "arrayExpression$ebnf$2", "_", {"literal":"]"}], "postprocess":
d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []})
}, },
{"name": "arrayExpression", "symbols": [{"literal":"["}, "_", {"literal":"]"}], "postprocess": d => node.array.create({ values: [] })},
{"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, {"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id},
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})}, {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})},
{"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id}, {"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id},

View File

@@ -119,18 +119,20 @@ callExpression ->
| indexExpression {% id %} | indexExpression {% id %}
arguments -> arguments ->
"(" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ ")" {% "(" _ (expression {% d => d[0] %}) ( _ %separator _ expression {% d => d[3] %} ):* _ ")" {%
d => [d[2], ...d[3]] d => [d[2], ...d[3]]
%} %}
| "(" _ ")" {% d => [] %}
indexExpression -> indexExpression ->
arrayExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %} arrayExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %}
| arrayExpression {% id %} | arrayExpression {% id %}
arrayExpression -> arrayExpression ->
"[" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {% "[" _ (expression {% d => d[0] %}) ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {%
d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []}) d => node.array.create({ values: [d[2], ...d[3]] })
%} %}
| "[" _ "]" {% d => node.array.create({ values: [] }) %}
| parenthesizedExpression {% id %} | parenthesizedExpression {% id %}
parenthesizedExpression -> parenthesizedExpression ->

View File

@@ -33,6 +33,7 @@ const call = {
// Check that the arguments match what is expected // Check that the arguments match what is expected
let checkFailed = call.checkArugments({ let checkFailed = call.checkArugments({
node,
fn, fn,
resolvedArgs, resolvedArgs,
argumentsExpected: func.arguments, argumentsExpected: func.arguments,

View File

@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
apiKey: 1, apiKey: 1,
darkMode: 1, darkMode: 1,
subscribedLibraries: 1, subscribedLibraries: 1,
fileStorageUsed: 1,
profile: 1, profile: 1,
preferences: 1, preferences: 1,
'services.patreon.id': 1, 'services.patreon.id': 1,

View File

@@ -1,5 +1,5 @@
<template lang="html"> <template lang="html">
<dialog-base :color="model.color"> <dialog-base v-if="model" :color="model.color">
<template slot="toolbar"> <template slot="toolbar">
<v-toolbar-title> <v-toolbar-title>
Character Details Character Details
@@ -43,7 +43,10 @@ export default {
ColorPicker, ColorPicker,
}, },
props: { props: {
_id: String, _id: {
type: String,
required: true,
},
startInEditTab: Boolean, startInEditTab: Boolean,
}, },
meteor: { meteor: {

View File

@@ -35,16 +35,15 @@
slot="actions" slot="actions"
text text
:loading="archiveActionLoading" :loading="archiveActionLoading"
:disabled="!numSelected" :disabled="!numSelected || (mode === 'restore' && characterSlots <= 0)"
color="primary" color="primary"
@click="archiveAction" @click="archiveAction"
> >
{{ mode === 'archive' ? 'Archive' : 'Restore' }} <template v-if="mode === 'restore' && characterSlots <= 0">
<template v-if="numSelected > 1"> No Character Slots Left
{{ numSelected }} characters
</template> </template>
<template v-else-if="numSelected === 1"> <template v-else>
character {{ mode === 'archive' ? 'Archive' : 'Restore' }}
</template> </template>
</v-btn> </v-btn>
<v-btn <v-btn
@@ -65,8 +64,9 @@ import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolder
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import archiveCreatureToFile from '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; import archiveCreatureToFile from '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js'; import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import { uniq, flatten } from 'lodash'; import { uniq, flatten } from 'lodash';
import { characterSlotsRemaining } from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
const characterTransform = function(char){ const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`; char.url = `/character/${char._id}/${char.urlName || '-'}`;
@@ -146,6 +146,10 @@ export default {
$subscribe: { $subscribe: {
'archivedCreatures': [], 'archivedCreatures': [],
'archiveCreatureFiles': [], 'archiveCreatureFiles': [],
'characterList': [],
},
characterSlots(){
return characterSlotsRemaining(Meteor.userId());
}, },
folders(){ folders(){
const userId = Meteor.userId(); const userId = Meteor.userId();

View File

@@ -63,6 +63,9 @@
<v-tab-item> <v-tab-item>
<character-tab :creature-id="creatureId" /> <character-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item>
<build-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item <v-tab-item
v-if="creature.settings.showTreeTab" v-if="creature.settings.showTreeTab"
> >
@@ -82,7 +85,8 @@
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue'; import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue'; import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue'; import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue'; import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue';
import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue';
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue'; import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
@@ -95,6 +99,7 @@
InventoryTab, InventoryTab,
SpellsTab, SpellsTab,
CharacterTab, CharacterTab,
BuildTab,
TreeTab, TreeTab,
}, },
props: { props: {

View File

@@ -110,7 +110,10 @@
Spells Spells
</v-tab> </v-tab>
<v-tab> <v-tab>
Character Journal
</v-tab>
<v-tab>
Build
</v-tab> </v-tab>
<v-tab v-if="creature.settings.showTreeTab"> <v-tab v-if="creature.settings.showTreeTab">
Tree Tree

View File

@@ -0,0 +1,56 @@
<template>
<v-card
hover
data-id="creature-summary"
@mouseover="hover = true"
@mouseleave="hover = false"
@click="showCharacterForm"
>
<v-img
v-if="creature.picture"
:src="creature.picture"
/>
<v-card-title class="text-h6">
{{ creature.name }}
</v-card-title>
<v-card-text>
{{ creature.alignment }}<br>
{{ creature.gender }}
</v-card-text>
<card-highlight :active="hover" />
</v-card>
</template>
<script lang="js">
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
export default {
components: {
CardHighlight,
},
props: {
creature: {
type: Object,
required: true,
},
},
data(){ return {
hover: false,
}},
methods: {
showCharacterForm(){
this.$store.commit('pushDialogStack', {
component: 'creature-form-dialog',
elementId: 'creature-summary',
data: {
_id: this.creature._id,
},
});
},
}
}
</script>
<style>
</style>

View File

@@ -1,53 +1,6 @@
<template lang="html"> <template lang="html">
<div class="inventory"> <div class="build">
<column-layout wide-columns> <column-layout wide-columns>
<div>
<v-card
hover
data-id="creature-summary"
@mouseover="summaryHover = true"
@mouseleave="summaryHover = false"
@click="showCharacterForm"
>
<v-img
v-if="creature.picture"
:src="creature.picture"
/>
<v-card-title class="text-h6">
{{ creature.name }}
</v-card-title>
<v-card-text>
{{ creature.alignment }}<br>
{{ creature.gender }}
</v-card-text>
<card-highlight :active="summaryHover" />
</v-card>
</div>
<div>
<toolbar-card
data-id="slot-card"
@toolbarclick="showSlotDialog"
>
<template slot="toolbar">
<v-toolbar-title>
Build
</v-toolbar-title>
<v-spacer />
<v-toolbar-title>
<v-icon
small
style="width: 16px;"
class="mr-1"
>
mdi-pencil
</v-icon>
</v-toolbar-title>
</template>
<v-card-text style="background-color: inherit;">
<slots :creature-id="creatureId" />
</v-card-text>
</toolbar-card>
</div>
<div> <div>
<v-card class="class-details"> <v-card class="class-details">
<v-card-title <v-card-title
@@ -117,13 +70,30 @@
</v-list> </v-list>
</v-card> </v-card>
</div> </div>
<div <div>
v-for="note in notes" <toolbar-card
:key="note._id" data-id="slot-card"
> @toolbarclick="showSlotDialog"
<note-card >
:model="note" <template slot="toolbar">
/> <v-toolbar-title>
Build
</v-toolbar-title>
<v-spacer />
<v-toolbar-title>
<v-icon
small
style="width: 16px;"
class="mr-1"
>
mdi-pencil
</v-icon>
</v-toolbar-title>
</template>
<v-card-text style="background-color: inherit;">
<slots :creature-id="creatureId" />
</v-card-text>
</toolbar-card>
</div> </div>
</column-layout> </column-layout>
</div> </div>
@@ -133,18 +103,16 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
import Slots from '/imports/ui/creature/slots/Slots.vue'; import Slots from '/imports/ui/creature/slots/Slots.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue'; import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import CardHighlight from '/imports/ui/components/CardHighlight.vue'; import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
export default { export default {
components: { components: {
ColumnLayout, ColumnLayout,
NoteCard,
Slots, Slots,
ToolbarCard, ToolbarCard,
CardHighlight, CreatureSummary,
}, },
props: { props: {
creatureId: { creatureId: {
@@ -152,9 +120,6 @@ export default {
required: true, required: true,
}, },
}, },
data(){return {
summaryHover: false,
}},
computed: { computed: {
highestClassLevels(){ highestClassLevels(){
let highestLevels = {}; let highestLevels = {};
@@ -182,16 +147,6 @@ export default {
} }
}, },
meteor: { meteor: {
notes(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'note',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
creature(){ creature(){
return Creatures.findOne(this.creatureId); return Creatures.findOne(this.creatureId);
}, },
@@ -207,15 +162,6 @@ export default {
}, },
}, },
methods: { methods: {
showCharacterForm(){
this.$store.commit('pushDialogStack', {
component: 'creature-form-dialog',
elementId: 'creature-summary',
data: {
_id: this.creatureId,
},
});
},
addExperience(){ addExperience(){
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'experience-insert-dialog', component: 'experience-insert-dialog',

View File

@@ -0,0 +1,60 @@
<template>
<div class="build-tab">
<column-layout wide-columns>
<div>
<creature-summary :creature="creature" />
</div>
<div
v-for="note in notes"
:key="note._id"
>
<note-card
:model="note"
/>
</div>
</column-layout>
</div>
</template>
<script lang="js">
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Slots from '/imports/ui/creature/slots/Slots.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default {
components: {
ColumnLayout,
CreatureSummary,
NoteCard,
},
props: {
creatureId: {
type: String,
required: true,
},
},
meteor: {
notes(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'note',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
creature(){
return Creatures.findOne(this.creatureId);
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,33 +1,19 @@
<template lang="html"> <template lang="html">
<div> <div>
{{ creatureCount }} / <creature-storage-stats />
<v-icon v-if="characterSlots === -1">
mdi-infinity
</v-icon>
<template v-else>
{{ characterSlots }}
</template>
<archive-button /> <archive-button />
</div> </div>
</template> </template>
<script lang="js"> <script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureStorageStats from '/imports/ui/creature/creatureList/CreatureStorageStats.vue';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue'; import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
export default { export default {
components: { components: {
CreatureStorageStats,
ArchiveButton, ArchiveButton,
}, },
meteor: {
creatureCount(){
return Creatures.find({owner: Meteor.userId()}).count();
},
characterSlots(){
return getUserTier(Meteor.userId()).characterSlots;
}
},
} }
</script> </script>

View File

@@ -0,0 +1,34 @@
<template>
<div
class="creature-storage-stats"
style="display: inline-block;"
>
{{ creatureCount }} /
<v-icon v-if="characterSlots === -1">
mdi-infinity
</v-icon>
<template v-else>
{{ characterSlots }}
</template>
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
export default {
meteor: {
creatureCount(){
return Creatures.find({owner: Meteor.userId()}).count();
},
characterSlots(){
return getUserTier(Meteor.userId()).characterSlots;
}
},
}
</script>
<style>
</style>

View File

@@ -4,8 +4,11 @@
Delete {{ typeName }} Delete {{ typeName }}
</v-toolbar-title> </v-toolbar-title>
<div> <div>
<v-alert type="warning" outlined>
This can't be undone
</v-alert>
<p v-if="name"> <p v-if="name">
Type "{{ name }}" to permanenetly delete Type "{{ name }}" to permanenetly delete.
</p> </p>
<v-text-field <v-text-field
v-if="name" v-if="name"

View File

@@ -0,0 +1,87 @@
<template>
<v-card :data-id="`${model._id}-archive-card`">
<v-card-title>
{{ model.meta.creatureName }}
</v-card-title>
<v-card-subtitle>
{{ model.size }}
</v-card-subtitle>
<v-card-actions>
<v-btn
v-if="characterSlots > 0"
text
@click="restore(model._id)"
>
Restore
</v-btn>
<v-flex />
<v-btn
icon
@click="removeArchiveCharacter"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<v-btn
icon
:href="`${model.link}?download=true`"
>
<v-icon>mdi-download</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script lang="js">
import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import { characterSlotsRemaining } from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
import removeArchiveCreature from '/imports/api/creature/archive/methods/removeArchiveCreature.js';
export default {
props: {
model: {
type: Object,
required: true,
},
},
data(){return {
restoreLoading: false,
removeLoading: false,
}},
meteor: {
characterSlots(){
return characterSlotsRemaining(Meteor.userId());
},
},
methods: {
restore(){
this.restoreLoading = true;
restoreCreatureFromFile.call({
fileId: this.model._id,
}, error => {
this.restoreLoading = false;
if (!error) return;
console.error(error);
snackbar({text: error.reason});
});
},
removeArchiveCharacter(){
let that = this;
this.$store.commit('pushDialogStack', {
component: 'delete-confirmation-dialog',
elementId: `${that.model._id}-archive-card`,
data: {
name: this.model.meta.creatureName,
typeName: 'Character Archive'
},
callback(confirmation){
if(!confirmation) return;
removeArchiveCreature.call({fileId: that.model._id}, (error) => {
if (error) console.error(error);
});
}
});
},
},
}
</script>

View File

@@ -0,0 +1,66 @@
<template>
<v-col
cols="12"
md="4"
lg="3"
class="layout column justify-center align-center"
>
<v-progress-circular
:rotate="-90"
:size="100"
:width="15"
:value="percentFileStorageUsed"
:buffer-value="50"
color="accent"
>
{{ percentFileStorageUsed }}%
</v-progress-circular>
<div class="ma-2 mt-4">
{{ prettyBytes(storageUsed) }} / {{ prettyBytes(storageAllowed) }}
<v-btn
icon
@click="updateStorageUsed"
>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</div>
</v-col>
</template>
<script lang="js">
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import updateFileStorageUsed from '/imports/api/users/methods/updateFileStorageUsed.js';
import prettyBytes from 'pretty-bytes';
export default {
meteor: {
storageUsed(){
return Meteor.user().fileStorageUsed || 0;
},
storageAllowed(){
return getUserTier(Meteor.userId()).fileStorage * 1000000;
},
percentFileStorageUsed(){
return Math.round((this.storageUsed / this.storageAllowed) * 100);
},
},
methods: {
prettyBytes(input){
return prettyBytes(input)
},
updateStorageUsed(){
this.updateStorageUsedLoading = true;
updateFileStorageUsed.call(error => {
this.updateStorageUsedLoading = false;
if (!error) return;
snackbar({text: error.reason});
});
},
},
}
</script>
<style>
</style>

View File

@@ -96,6 +96,7 @@
{title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true}, {title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true},
//{title: 'Tabletops', icon: 'api', to: '/tabletops', requireLogin: true}, //{title: 'Tabletops', icon: 'api', to: '/tabletops', requireLogin: true},
//{title: 'Friends', icon: 'people', to: '/friends', requireLogin: true}, //{title: 'Friends', icon: 'people', to: '/friends', requireLogin: true},
{title: 'Files', icon: 'mdi-file-multiple', to: '/my-files'},
{title: 'Feedback', icon: 'mdi-bug', to: '/feedback'}, {title: 'Feedback', icon: 'mdi-bug', to: '/feedback'},
{title: 'About', icon: 'mdi-sign-text', to: '/about'}, {title: 'About', icon: 'mdi-sign-text', to: '/about'},
{title: 'Patreon', icon: 'mdi-patreon', href: 'https://www.patreon.com/dicecloud'}, {title: 'Patreon', icon: 'mdi-patreon', href: 'https://www.patreon.com/dicecloud'},

View File

@@ -8,6 +8,18 @@
style="flex-basis: 900px" style="flex-basis: 900px"
> >
<v-list> <v-list>
<v-subheader>
File storage used
</v-subheader>
<file-storage-stats />
<v-subheader>
Character storage used
</v-subheader>
<v-list-item>
<v-list-item-title>
<creature-storage-stats />
</v-list-item-title>
</v-list-item>
<v-subheader> <v-subheader>
Preferences Preferences
</v-subheader> </v-subheader>
@@ -221,8 +233,14 @@
import { getUserTier } from '/imports/api/users/patreon/tiers.js'; import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import addEmail from '/imports/api/users/methods/addEmail.js'; import addEmail from '/imports/api/users/methods/addEmail.js';
import removeEmail from '/imports/api/users/methods/removeEmail.js'; import removeEmail from '/imports/api/users/methods/removeEmail.js';
import CreatureStorageStats from '/imports/ui/creature/creatureList/CreatureStorageStats.vue';
import FileStorageStats from '/imports/ui/files/FileStorageStats.vue';
export default { export default {
components: {
CreatureStorageStats,
FileStorageStats,
},
meteor: { meteor: {
$subscribe: { $subscribe: {
'userPublicProfiles'(){ 'userPublicProfiles'(){

View File

@@ -0,0 +1,114 @@
<template>
<v-container>
<v-row
justify="center"
class="mt-2"
>
<file-storage-stats />
</v-row>
<v-row dense>
<template v-if="archiveFiles && archiveFiles.length">
<v-col cols="12">
<v-subheader> Archived Characters </v-subheader>
</v-col>
<v-col
v-for="file in archiveFiles"
:key="file._id"
cols="12"
md="4"
lg="3"
>
<archive-file-card :model="file" />
</v-col>
<v-col
key="upload"
cols="12"
md="4"
lg="3"
class="layout column justify-center"
>
<input
ref="archiveFileInput"
type="file"
accept=".json"
style="display: none;"
@input="inputArchiveFile"
>
<v-btn
outlined
style="height: 100%; width: 100%;"
:color="archiveFileError ? 'error' : undefined"
@click="$refs.archiveFileInput.click()"
>
<v-icon left>
mdi-file-upload-outline
</v-icon>
<template v-if="archiveFileError">
{{ archiveFileError }}
</template>
<template v-else>
Upload archive
</template>
</v-btn>
</v-col>
</template>
</v-row>
</v-container>
</template>
<script lang="js">
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
import prettyBytes from 'pretty-bytes';
import ArchiveFileCard from '/imports/ui/files/ArchiveFileCard.vue';
import FileStorageStats from '/imports/ui/files/FileStorageStats.vue';
export default {
components: {
ArchiveFileCard,
FileStorageStats,
},
data(){ return {
updateStorageUsedLoading: false,
archiveFileError: undefined,
archiveFile: undefined,
}},
meteor: {
$subscribe: {
'archiveCreatureFiles': [],
'characterList': [],
},
archiveFiles() {
var userId = Meteor.userId();
return ArchiveCreatureFiles.find(
{
userId,
}, {
sort: {'size': -1},
}
).map(f => {
f.size = prettyBytes(f.size);
f.link = ArchiveCreatureFiles.link(f);
return f;
});
},
},
methods: {
inputArchiveFile(){
this.archiveFile = undefined;
this.archiveFileError = undefined;
const file = this.$refs.archiveFileInput.files[0];
if (!file) return;
if (file.type !== 'application/json'){
this.archiveFileError = 'File must be .json';
return;
}
if (file.size > 10000000){
this.archiveFileError = 'File too large';
return;
}
this.archiveFile = file;
console.log(this.archiveFile);
}
},
}
</script>

View File

@@ -30,6 +30,7 @@ const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue')
const TabletopRightDrawer = () => import('/imports/ui/tabletop/TabletopRightDrawer.vue'); const TabletopRightDrawer = () => import('/imports/ui/tabletop/TabletopRightDrawer.vue');
const Admin = () => import('/imports/ui/pages/Admin.vue'); const Admin = () => import('/imports/ui/pages/Admin.vue');
const Maintenance = () => import('/imports/ui/pages/Maintenance.vue'); const Maintenance = () => import('/imports/ui/pages/Maintenance.vue');
const Files = () => import('/imports/ui/pages/Files.vue');
// Not found // Not found
const NotFound = () => import('/imports/ui/pages/NotFound.vue'); const NotFound = () => import('/imports/ui/pages/NotFound.vue');
@@ -199,8 +200,8 @@ RouterFactory.configure(router => {
meta: { meta: {
title: 'Register', title: 'Register',
}, },
},{ }, {
path: '/account', path: '/account',
components: { components: {
default: Account, default: Account,
}, },
@@ -208,7 +209,16 @@ RouterFactory.configure(router => {
title: 'Account', title: 'Account',
}, },
beforeEnter: ensureLoggedIn, beforeEnter: ensureLoggedIn,
},{ }, {
path: '/my-files',
components: {
default: Files,
},
meta: {
title: 'Files',
},
beforeEnter: ensureLoggedIn,
}, {
path: '/feedback', path: '/feedback',
components: { components: {
default: Feedback, default: Feedback,

7
app/package-lock.json generated
View File

@@ -2564,6 +2564,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true "dev": true
}, },
"pretty-bytes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz",
"integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg=="
},
"prism-media": { "prism-media": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz",
@@ -2788,7 +2793,7 @@
}, },
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "", "resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
}, },
"simpl-schema": { "simpl-schema": {

View File

@@ -9,7 +9,7 @@
}, },
"author": "Stefan Zermatten", "author": "Stefan Zermatten",
"scripts": { "scripts": {
"run": "meteor --once", "run": "meteor",
"test": "meteor test --driver-package meteortesting:mocha --port 3001" "test": "meteor test --driver-package meteortesting:mocha --port 3001"
}, },
"engines": { "engines": {
@@ -40,6 +40,7 @@
"nearley": "^2.19.1", "nearley": "^2.19.1",
"ngraph.graph": "^19.1.0", "ngraph.graph": "^19.1.0",
"ngraph.path": "^1.4.0", "ngraph.path": "^1.4.0",
"pretty-bytes": "^6.0.0",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"request": "^2.88.2", "request": "^2.88.2",
"simpl-schema": "^1.12.0", "simpl-schema": "^1.12.0",