From 69f4bbf360e8f6a3458f1a1893e0fcb288f55669 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 20 Jun 2021 12:41:08 +0200 Subject: [PATCH] Added CRUD API and UI for creature folders --- .../creatureFolders/CreatureFolders.js | 6 +- .../creatureFolders/methods.js/index.js | 3 + .../methods.js/insertCreatureFolder.js | 45 ++++++ .../methods.js/removeCreatureFolder.js | 31 ++++ .../methods.js/reorderCreatureFolder.js | 43 ++++++ .../methods.js/updateCreatureFolderName.js | 31 ++++ app/imports/ui/layouts/Sidebar.vue | 26 +--- app/imports/ui/pages/CharacterList.vue | 142 +++++++++++++++--- 8 files changed, 282 insertions(+), 45 deletions(-) create mode 100644 app/imports/api/creature/creatureFolders/methods.js/index.js create mode 100644 app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js create mode 100644 app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js create mode 100644 app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js create mode 100644 app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js diff --git a/app/imports/api/creature/creatureFolders/CreatureFolders.js b/app/imports/api/creature/creatureFolders/CreatureFolders.js index 81d4779a..a61140c1 100644 --- a/app/imports/api/creature/creatureFolders/CreatureFolders.js +++ b/app/imports/api/creature/creatureFolders/CreatureFolders.js @@ -5,7 +5,7 @@ let CreatureFolders = new Mongo.Collection('creatureFolders'); let creatureFolderSchema = new SimpleSchema({ name: { type: String, - defaultValue: 'New Party', + defaultValue: 'Folder', trim: false, optional: true, }, @@ -20,10 +20,11 @@ let creatureFolderSchema = new SimpleSchema({ owner: { type: String, regEx: SimpleSchema.RegEx.Id, + index: 1, }, archived: { type: Boolean, - defaultValue: true, + optional: true, }, order: { type: Number, @@ -33,4 +34,5 @@ let creatureFolderSchema = new SimpleSchema({ CreatureFolders.attachSchema(creatureFolderSchema); +import '/imports/api/creature/creatureFolders/methods.js/index.js'; export default CreatureFolders; diff --git a/app/imports/api/creature/creatureFolders/methods.js/index.js b/app/imports/api/creature/creatureFolders/methods.js/index.js new file mode 100644 index 00000000..687f1d97 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/index.js @@ -0,0 +1,3 @@ +import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js'; +import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js'; +import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js'; \ No newline at end of file diff --git a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js new file mode 100644 index 00000000..7c5d4e15 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js @@ -0,0 +1,45 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const insertCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.insert', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run() { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.insert.denied', + 'You need to be logged in to insert a folder'); + } + // Limit folders to 50 per user + let existingFolders = CreatureFolders.find({ + owner: userId + }, { + fields: {order: 1}, + sort: {order :-1} + }); + if (existingFolders.count() >= 50){ + throw new Meteor.Error('creatureFolders.methods.insert.denied', + 'You can not have more than 50 folders'); + } + // Make the new folder the last in the order + let order = 0; + let lastFolder = existingFolders.fetch()[0]; + if (lastFolder){ + order = (lastFolder.order || 0) + 1; + } + // Insert + return CreatureFolders.insert({ + owner: userId, + order, + }); + }, +}); + +export default insertCreatureFolder; diff --git a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js new file mode 100644 index 00000000..71006cb7 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js @@ -0,0 +1,31 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const removeCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.remove', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'You need to be logged in to remove a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'This folder does not belong to you'); + } + // Remove + return CreatureFolders.remove(_id); + }, +}); + +export default removeCreatureFolder; \ No newline at end of file diff --git a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js new file mode 100644 index 00000000..92ecb418 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js @@ -0,0 +1,43 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const reorderCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.reorder', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, order}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.reorder.denied', + 'You need to be logged in to reorder a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.reorder.denied', + 'This folder does not belong to you'); + } + // First give it the new order, it should end in 0.5 putting it between two other docs + CreatureFolders.update(_id, {$set: {order}}); + this.unblock(); + // Reorder all the folders with integer numbers in this new order + CreatureFolders.find({ + owner: userId + }, { + fields: {order: 1,}, + sort: {order: -1} + }).forEach((folder, index) => { + if (folder.order !== index){ + CreatureFolders.update(_id, {$set: {order: index}}) + } + }); + }, +}); + +export default reorderCreatureFolder; diff --git a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js new file mode 100644 index 00000000..d798e59a --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js @@ -0,0 +1,31 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const updateCreatureFolderName = new ValidatedMethod({ + name: 'creatureFolders.methods.updateName', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, name}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'You need to be logged in to update a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'This folder does not belong to you'); + } + // Update + return CreatureFolders.update(_id, {$set: {name}}); + }, +}); + +export default updateCreatureFolderName; \ No newline at end of file diff --git a/app/imports/ui/layouts/Sidebar.vue b/app/imports/ui/layouts/Sidebar.vue index 5e4b8e9c..38d1bd3e 100644 --- a/app/imports/ui/layouts/Sidebar.vue +++ b/app/imports/ui/layouts/Sidebar.vue @@ -76,6 +76,7 @@ {{ character.name }} + @@ -140,31 +142,9 @@ ]; return links.filter(link => !link.requireLogin || isLoggedIn); }, - parties(){ - const userId = Meteor.userId(); - return Parties.find( - {owner: userId}, - {sort: {name: 1}}, - ).map(party => { - party.characterDocs = Creatures.find( - { - _id: {$in: party.Creatures}, - $or: [{readers: userId}, {writers: userId}, {owner: userId}], - }, { - sort: {name: 1}, - fields: {name: 1, urlName: 1}, - } - ).map(char => { - char.url = `/character/${char._id}/${char.urlName || '-'}`; - char.initial = char.name && char.name[0] || '?'; - return char; - }); - return party; - }); - }, CreaturesWithNoParty() { var userId = Meteor.userId(); - var charArrays = Parties.find({owner: userId}).map(p => p.Creatures); + var charArrays = Parties.find({owner: userId}).map(p => p.creatures); var partyChars = _.uniq(_.flatten(charArrays)); return Creatures.find( { diff --git a/app/imports/ui/pages/CharacterList.vue b/app/imports/ui/pages/CharacterList.vue index db02e7ca..c3b595dd 100644 --- a/app/imports/ui/pages/CharacterList.vue +++ b/app/imports/ui/pages/CharacterList.vue @@ -3,16 +3,78 @@ class="card-background pa-4" style="height: 100%" > - + + + + + + + + + + + + + + + add folder +