Added CRUD API and UI for creature folders
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user