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({
|
let creatureFolderSchema = new SimpleSchema({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
defaultValue: 'New Party',
|
defaultValue: 'Folder',
|
||||||
trim: false,
|
trim: false,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
@@ -20,10 +20,11 @@ let creatureFolderSchema = new SimpleSchema({
|
|||||||
owner: {
|
owner: {
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
index: 1,
|
||||||
},
|
},
|
||||||
archived: {
|
archived: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
defaultValue: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -33,4 +34,5 @@ let creatureFolderSchema = new SimpleSchema({
|
|||||||
|
|
||||||
CreatureFolders.attachSchema(creatureFolderSchema);
|
CreatureFolders.attachSchema(creatureFolderSchema);
|
||||||
|
|
||||||
|
import '/imports/api/creature/creatureFolders/methods.js/index.js';
|
||||||
export default CreatureFolders;
|
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 }}
|
{{ character.name }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<!--
|
||||||
<v-list-group
|
<v-list-group
|
||||||
v-for="party in parties"
|
v-for="party in parties"
|
||||||
:key="party._id"
|
:key="party._id"
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
-->
|
||||||
</v-list>
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -140,31 +142,9 @@
|
|||||||
];
|
];
|
||||||
return links.filter(link => !link.requireLogin || isLoggedIn);
|
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() {
|
CreaturesWithNoParty() {
|
||||||
var userId = Meteor.userId();
|
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));
|
var partyChars = _.uniq(_.flatten(charArrays));
|
||||||
return Creatures.find(
|
return Creatures.find(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,16 +3,78 @@
|
|||||||
class="card-background pa-4"
|
class="card-background pa-4"
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card :class="{'mb-4': folders && folders.length}">
|
||||||
<v-list v-if="CreaturesWithNoParty.length">
|
<v-list v-if="CreaturesWithNoParty.length">
|
||||||
<creature-list-tile
|
<creature-list-tile
|
||||||
v-for="character in CreaturesWithNoParty"
|
v-for="creature in CreaturesWithNoParty"
|
||||||
:key="character._id"
|
:key="creature._id"
|
||||||
:to="character.url"
|
:to="creature.url"
|
||||||
:model="character"
|
:model="creature"
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</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
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
fab
|
fab
|
||||||
@@ -28,10 +90,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
|
import Vue from 'vue';
|
||||||
import Creatures, {insertCreature} from '/imports/api/creature/Creatures.js';
|
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 { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||||
import CreatureListTile from '/imports/ui/creature/CreatureListTile.vue';
|
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){
|
const characterTransform = function(char){
|
||||||
char.url = `/character/${char._id}/${char.urlName || '-'}`;
|
char.url = `/character/${char._id}/${char.urlName || '-'}`;
|
||||||
@@ -44,42 +111,54 @@
|
|||||||
},
|
},
|
||||||
data(){ return{
|
data(){ return{
|
||||||
fab: false,
|
fab: false,
|
||||||
|
loadingInsertFolder: false,
|
||||||
|
renamingFolder: undefined,
|
||||||
}},
|
}},
|
||||||
meteor: {
|
meteor: {
|
||||||
$subscribe: {
|
$subscribe: {
|
||||||
'characterList': [],
|
'characterList': [],
|
||||||
},
|
},
|
||||||
parties(){
|
folders(){
|
||||||
const userId = Meteor.userId();
|
const userId = Meteor.userId();
|
||||||
let parties = Parties.find(
|
let folders = CreatureFolders.find(
|
||||||
{owner: userId},
|
{owner: userId, archived: {$ne: true}},
|
||||||
{sort: {name: 1}},
|
{sort: {order: 1}},
|
||||||
).map(party => {
|
).map(folder => {
|
||||||
party.characterDocs = Creatures.find(
|
folder.creatures = Creatures.find(
|
||||||
{
|
{
|
||||||
_id: {$in: party.Creatures},
|
_id: {$in: folder.creatures || []},
|
||||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||||
}, {
|
}, {
|
||||||
sort: {name: 1},
|
sort: {name: 1},
|
||||||
}
|
}
|
||||||
).map(characterTransform);
|
).map(characterTransform);
|
||||||
return party;
|
return folder;
|
||||||
});
|
});
|
||||||
return parties;
|
return folders;
|
||||||
},
|
},
|
||||||
CreaturesWithNoParty() {
|
CreaturesWithNoParty() {
|
||||||
var userId = Meteor.userId();
|
var userId = Meteor.userId();
|
||||||
var charArrays = Parties.find({owner: userId}).map(p => p.Creatures);
|
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
|
||||||
var partyChars = _.uniq(_.flatten(charArrays));
|
var folderChars = _.uniq(_.flatten(charArrays));
|
||||||
return Creatures.find(
|
return Creatures.find(
|
||||||
{
|
{
|
||||||
_id: {$nin: partyChars},
|
_id: {$nin: folderChars},
|
||||||
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
|
||||||
},
|
},
|
||||||
{sort: {name: 1}}
|
{sort: {name: 1}}
|
||||||
).map(characterTransform);
|
).map(characterTransform);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch:{
|
||||||
|
renamingFolder(newId){
|
||||||
|
if(newId){
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
let input = this.$refs[`name-input-${newId}`];
|
||||||
|
input[0].focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
insertCharacter(){
|
insertCharacter(){
|
||||||
let tier = getUserTier(Meteor.userId());
|
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user