Added CRUD API and UI for creature folders

This commit is contained in:
Stefan Zermatten
2021-06-20 12:41:08 +02:00
parent 6b2d74a165
commit 69f4bbf360
8 changed files with 282 additions and 45 deletions

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -76,6 +76,7 @@
{{ character.name }}
</v-list-item-title>
</v-list-item>
<!--
<v-list-group
v-for="party in parties"
:key="party._id"
@@ -105,6 +106,7 @@
</v-list-item-title>
</v-list-item>
</v-list-group>
-->
</v-list>
</div>
</template>
@@ -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(
{

View File

@@ -3,16 +3,78 @@
class="card-background pa-4"
style="height: 100%"
>
<v-card>
<v-card :class="{'mb-4': folders && folders.length}">
<v-list v-if="CreaturesWithNoParty.length">
<creature-list-tile
v-for="character in CreaturesWithNoParty"
:key="character._id"
:to="character.url"
:model="character"
v-for="creature in CreaturesWithNoParty"
:key="creature._id"
:to="creature.url"
:model="creature"
/>
</v-list>
</v-card>
<v-expansion-panels v-if="folders && folders.length">
<v-expansion-panel
v-for="folder in folders"
:key="folder._id"
>
<v-expansion-panel-header>
<template #default="{ open }">
<div v-if="renamingFolder !== folder._id">
{{ folder.name }}
</div>
<text-field
v-else
:ref="`name-input-${folder._id}`"
regular
hide-details
dense
:value="folder.name"
@change="(value, ack) => renameFolder(folder._id, value, ack)"
@click.native.stop="()=>{}"
/>
<v-btn
v-if="renamingFolder === folder._id || open"
icon
style="flex-grow: 0"
@click.stop="renamingFolder !== folder._id ? renamingFolder = folder._id : renamingFolder = undefined"
>
<v-icon v-if="renamingFolder !== folder._id">
mdi-pencil
</v-icon>
<v-icon v-else>
mdi-check
</v-icon>
</v-btn>
<v-btn
v-if="open"
icon
style="flex-grow: 0"
@click.stop="removeFolder(folder._id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-list v-if="folder.creatures">
<creature-list-tile
v-for="creature in folder.creatures"
:key="creature._id"
:to="creature.url"
:model="creature"
/>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-btn
text
:loading="loadingInsertFolder"
@click="insertFolder"
>
add folder
</v-btn>
<v-btn
color="accent"
fab
@@ -28,11 +90,16 @@
</template>
<script lang="js">
import Vue from 'vue';
import Creatures, {insertCreature} from '/imports/api/creature/Creatures.js';
import Parties from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import CreatureListTile from '/imports/ui/creature/CreatureListTile.vue';
import insertCreatureFolder from '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js';
import updateCreatureFolderName from '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js';
import removeCreatureFolder from '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`;
char.initial = char.name && char.name[0] || '?';
@@ -44,42 +111,54 @@
},
data(){ return{
fab: false,
loadingInsertFolder: false,
renamingFolder: undefined,
}},
meteor: {
$subscribe: {
'characterList': [],
},
parties(){
folders(){
const userId = Meteor.userId();
let parties = Parties.find(
{owner: userId},
{sort: {name: 1}},
).map(party => {
party.characterDocs = Creatures.find(
let folders = CreatureFolders.find(
{owner: userId, archived: {$ne: true}},
{sort: {order: 1}},
).map(folder => {
folder.creatures = Creatures.find(
{
_id: {$in: party.Creatures},
_id: {$in: folder.creatures || []},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
}, {
sort: {name: 1},
}
).map(characterTransform);
return party;
return folder;
});
return parties;
return folders;
},
CreaturesWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.Creatures);
var partyChars = _.uniq(_.flatten(charArrays));
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
var folderChars = _.uniq(_.flatten(charArrays));
return Creatures.find(
{
_id: {$nin: partyChars},
_id: {$nin: folderChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
).map(characterTransform);
},
},
watch:{
renamingFolder(newId){
if(newId){
Vue.nextTick(() => {
let input = this.$refs[`name-input-${newId}`];
input[0].focus();
});
}
}
},
methods: {
insertCharacter(){
let tier = getUserTier(Meteor.userId());
@@ -103,6 +182,29 @@
});
}
},
}
insertFolder(){
loadingInsertFolder = true;
insertCreatureFolder.call(error => {
loadingInsertFolder = false;
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
renameFolder(_id, name, ack){
updateCreatureFolderName.call({_id, name}, ack);
},
removeFolder(_id){
removeCreatureFolder.call({_id}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
},
};
</script>