Added library list UI

This commit is contained in:
Stefan Zermatten
2019-05-10 13:05:21 +02:00
parent de183297fc
commit 9a194a20cb
11 changed files with 217 additions and 35 deletions

View File

@@ -1,5 +1,6 @@
import schema from '/imports/api/schema.js'; import schema from '/imports/api/schema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js'; import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
/** /**
* Libraries are trees of library nodes where each node represents a character * Libraries are trees of library nodes where each node represents a character
@@ -23,4 +24,17 @@ LibrarySchema.extend(SharingSchema);
Libraries.attachSchema(LibrarySchema); Libraries.attachSchema(LibrarySchema);
export default Libraries; export default Libraries;
export { LibrarySchema };
const insertLibrary = new ValidatedMethod({
name: 'Libraries.methods.insert',
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner'),
run(library) {
library.owner = this.userId;
return Libraries.insert(library);
},
});
export { LibrarySchema, insertLibrary };

View File

@@ -10,21 +10,21 @@ const userSchema = schema({
type: Array, type: Array,
optional: true, optional: true,
}, },
"emails.$": { 'emails.$': {
type: Object, type: Object,
}, },
"emails.$.address": { 'emails.$.address': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Email, regEx: SimpleSchema.RegEx.Email,
}, },
"emails.$.verified": { 'emails.$.verified': {
type: Boolean, type: Boolean,
}, },
registered_emails: { registered_emails: {
type: Array, type: Array,
optional: true, optional: true,
}, },
"registered_emails.$": { 'registered_emails.$': {
type: Object, type: Object,
blackbox: true, blackbox: true,
}, },
@@ -40,7 +40,7 @@ const userSchema = schema({
type: Array, type: Array,
optional: true, optional: true,
}, },
"roles.$": { 'roles.$': {
type: String type: String
}, },
// In order to avoid an 'Exception in setInterval callback' from Meteor // In order to avoid an 'Exception in setInterval callback' from Meteor
@@ -57,12 +57,20 @@ const userSchema = schema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
subscribedLibraries: {
type: Array,
defaultValue: [],
max: 100,
},
'subscribedLibraries.$': {
type: String,
},
}); });
Meteor.users.attachSchema(userSchema); Meteor.users.attachSchema(userSchema);
Meteor.users.generateApiKey = new ValidatedMethod({ Meteor.users.generateApiKey = new ValidatedMethod({
name: "Users.methods.generateApiKey", name: 'Users.methods.generateApiKey',
validate: null, validate: null,
run(){ run(){
if(Meteor.isClient) return; if(Meteor.isClient) return;
@@ -75,7 +83,7 @@ Meteor.users.generateApiKey = new ValidatedMethod({
}); });
Meteor.users.setDarkMode = new ValidatedMethod({ Meteor.users.setDarkMode = new ValidatedMethod({
name: "Users.methods.setDarkMode", name: 'Users.methods.setDarkMode',
validate: new SimpleSchema({ validate: new SimpleSchema({
darkMode: { type: Boolean }, darkMode: { type: Boolean },
}).validator(), }).validator(),
@@ -86,7 +94,7 @@ Meteor.users.setDarkMode = new ValidatedMethod({
}); });
Meteor.users.sendVerificationEmail = new ValidatedMethod({ Meteor.users.sendVerificationEmail = new ValidatedMethod({
name: "Users.methods.sendVerificationEmail", name: 'Users.methods.sendVerificationEmail',
validate: schema({ validate: schema({
userId:{ userId:{
type: String, type: String,
@@ -100,12 +108,12 @@ Meteor.users.sendVerificationEmail = new ValidatedMethod({
userId = this.userId || userId; userId = this.userId || userId;
let user = Meteor.users.findOne(); let user = Meteor.users.findOne();
if (!user) { if (!user) {
throw new Meteor.Error("User not found", throw new Meteor.Error('User not found',
"Can't send a validation email to a user that does not exist"); 'Can\'t send a validation email to a user that does not exist');
} }
if (!_.some(user.emails, email => email.address === address)) { if (!_.some(user.emails, email => email.address === address)) {
throw new Meteor.Error("Email address not found", throw new Meteor.Error('Email address not found',
"The specified email address wasn't found on this user account"); 'The specified email address wasn\'t found on this user account');
} }
Accounts.sendVerificationEmail(this.userId, address); Accounts.sendVerificationEmail(this.userId, address);
} }

View File

@@ -1,23 +1,23 @@
import Libraries from "/imports/api/library/Libraries.js" import Libraries from '/imports/api/library/Libraries.js';
const standardLibraryIds = [ const standardLibraryIds = [
"SRDLibraryGA3XWsd", 'SRDLibraryGA3XWsd',
]; ];
Meteor.publish("standardLibraries", function(){ Meteor.publish('standardLibraries', function(){
return Libraries.find({_id: {$in: standardLibraryIds}}); return Libraries.find({_id: {$in: standardLibraryIds}});
}); });
Meteor.publish("standardLibraryItems", function(categoryKey){ Meteor.publish('standardLibraryItems', function(categoryKey){
return LibraryItems.find({ return LibraryItems.find({
library: {$in: standardLibraryIds}, library: {$in: standardLibraryIds},
"settings.category": categoryKey, 'settings.category': categoryKey,
}, { }, {
sort: {name: 1}, sort: {name: 1},
}); });
}); });
Meteor.publish("standardLibrarySpells", function(level){ Meteor.publish('standardLibrarySpells', function(level){
return LibrarySpells.find({ return LibrarySpells.find({
library: {$in: standardLibraryIds}, library: {$in: standardLibraryIds},
level, level,
@@ -25,3 +25,17 @@ Meteor.publish("standardLibrarySpells", function(level){
sort: {name: 1}, sort: {name: 1},
}); });
}); });
Meteor.publish('libraries', function(){
const user = Meteor.user();
const userId = user && user._id;
const subs = user && user.subscribedLibraries || [];
return Libraries.find({
$or: [
{owner: userId},
{writers: userId},
{readers: userId},
{_id: {$in: subs}},
]
});
});

View File

@@ -5,7 +5,7 @@
{{userName}} {{userName}}
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-tooltip bottom> <v-tooltip bottom>
<v-btn flat icon slot="activator"><v-icon>settings</v-icon></v-btn> <v-btn flat icon slot="activator" to="/account"><v-icon>settings</v-icon></v-btn>
<span>Account Settings</span> <span>Account Settings</span>
</v-tooltip> </v-tooltip>
</v-layout> </v-layout>
@@ -82,6 +82,7 @@
return [ return [
{title: "Home", icon: "home", to: "/"}, {title: "Home", icon: "home", to: "/"},
{title: "Creatures", icon: "group", to: "/characterList", vif: Meteor.userId()}, {title: "Creatures", icon: "group", to: "/characterList", vif: Meteor.userId()},
{title: "Libraries", icon: "book", to: "/library", vif: Meteor.userId()},
{title: "Send Feedback", icon: "bug_report", to: "/feedback"}, {title: "Send Feedback", icon: "bug_report", to: "/feedback"},
{title: "Patreon", icon: "", href: "https://www.patreon.com/dicecloud"}, {title: "Patreon", icon: "", href: "https://www.patreon.com/dicecloud"},
{title: "Github", icon: "", href: "https://github.com/ThaumRystra/DiceCloud1"}, {title: "Github", icon: "", href: "https://github.com/ThaumRystra/DiceCloud1"},

View File

@@ -18,7 +18,7 @@ export default {
dirty: false, dirty: false,
safeValue: this.value, safeValue: this.value,
inputValue: this.value, inputValue: this.value,
}}, };},
props: { props: {
value: [String, Number], value: [String, Number],
debounceTime: { debounceTime: {

View File

@@ -3,6 +3,7 @@ import AttributeDialogContainer from '/imports/ui/creature/properties/attributes
import AttributeCreationDialog from '/imports/ui/creature/properties/attributes/AttributeCreationDialog.vue'; import AttributeCreationDialog from '/imports/ui/creature/properties/attributes/AttributeCreationDialog.vue';
import FeatureCreationDialog from '/imports/ui/creature/properties/features/FeatureCreationDialog.vue'; import FeatureCreationDialog from '/imports/ui/creature/properties/features/FeatureCreationDialog.vue';
import FeatureDialogContainer from '/imports/ui/creature/properties/features/FeatureDialogContainer.vue'; import FeatureDialogContainer from '/imports/ui/creature/properties/features/FeatureDialogContainer.vue';
import LibraryCreationDialog from '/imports/ui/library/LibraryCreationDialog.vue';
import SkillDialogContainer from '/imports/ui/creature/properties/skills/SkillDialogContainer.vue'; import SkillDialogContainer from '/imports/ui/creature/properties/skills/SkillDialogContainer.vue';
export default { export default {
@@ -11,5 +12,6 @@ export default {
AttributeCreationDialog, AttributeCreationDialog,
FeatureCreationDialog, FeatureCreationDialog,
FeatureDialogContainer, FeatureDialogContainer,
LibraryCreationDialog,
SkillDialogContainer, SkillDialogContainer,
}; };

View File

@@ -0,0 +1,53 @@
<template lang="html">
<dialog-base>
<template slot="toolbar">
<div>
New Library
</div>
</template>
<template>
<text-field label="name" :value="library.name" @change="nameChanged" :debounce-time="0"/>
</template>
<template slot="actions">
<v-spacer/>
<v-btn
flat
:disabled="!valid"
@click="$store.dispatch('popDialogStack', library)"
>
Insert Library
</v-btn>
</template>
</dialog-base>
</template>
<script>
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
export default {
data(){ return {
library: {
name: 'New Library',
},
valid: true,
}},
components: {
DialogBase,
},
methods: {
nameChanged(val, ack){
if (val){
this.library.name = val;
this.valid = true,
ack();
} else {
this.valid = false;
ack('Name is required')
}
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,71 @@
<template lang="html">
<toolbar-layout>
<span slot="toolbar">Libraries</span>
<v-card class="ma-4">
<v-list v-if="libraries.length">
<v-list-tile
v-for="library in libraries"
:key="library._id"
:to="library.url"
:data-id="library._id"
>
<v-list-tile-content>
<v-list-tile-title>
{{library.name}}
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-card-text v-else-if="$subReady.libraries">
You aren't subscribed to any libraries :O
</v-card-text>
<v-card-text v-if="!$subReady.libraries" class="layout row justify-center">
<v-progress-circular indeterminate/>
</v-card-text>
</v-card>
<v-btn fixed fab bottom right
color="primary"
@click="insertLibrary"
data-id="insert-library-fab"
>
<v-icon>add</v-icon>
</v-btn>
</toolbar-layout>
</template>
<script>
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
import Libraries, {insertLibrary} from '/imports/api/library/Libraries.js';
export default {
components: {
ToolbarLayout,
},
methods: {
insertLibrary(){
this.$store.commit('pushDialogStack', {
component: 'library-creation-dialog',
elementId: 'insert-library-fab',
callback(library){
if (!library) return;
let libraryId = insertLibrary.call(library);
return libraryId;
}
});
},
},
meteor: {
$subscribe: {
'libraries': [],
},
libraries(){
return Libraries.find({}, {
sort: {name: 1}
}).fetch();
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,13 +1,14 @@
import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2'; import { RouterFactory, nativeScrollBehavior } from 'meteor/akryum:vue-router2';
import Vue from "vue"; import Vue from 'vue';
// Components // Components
import Home from '/imports/ui/pages/Home.vue'; import Home from '/imports/ui/pages/Home.vue';
import CharacterList from "/imports/ui/pages/CharacterList.vue"; import CharacterList from '/imports/ui/pages/CharacterList.vue';
import CharacterSheetPage from "/imports/ui/pages/CharacterSheetPage.vue"; import Libraries from '/imports/ui/pages/Libraries.vue';
import SignIn from "/imports/ui/pages/SignIn.vue" ; import CharacterSheetPage from '/imports/ui/pages/CharacterSheetPage.vue';
import Register from "/imports/ui/pages/Register.vue" ; import SignIn from '/imports/ui/pages/SignIn.vue' ;
import Account from "/imports/ui/pages/Account.vue" ; import Register from '/imports/ui/pages/Register.vue' ;
import Account from '/imports/ui/pages/Account.vue' ;
// Not found // Not found
import NotFound from '/imports/ui/pages/NotFound.vue'; import NotFound from '/imports/ui/pages/NotFound.vue';
@@ -27,22 +28,25 @@ RouterFactory.configure(factory => {
name: 'home', name: 'home',
component: Home, component: Home,
},{ },{
path: "/characterList", path: '/characterList',
component: CharacterList, component: CharacterList,
},{ },{
path: "/character/:id/:urlName", path: '/library',
component: Libraries,
},{
path: '/character/:id/:urlName',
component: CharacterSheetPage, component: CharacterSheetPage,
},{ },{
path: "/character/:id", path: '/character/:id',
component: CharacterSheetPage, component: CharacterSheetPage,
},{ },{
path: "/sign-in", path: '/sign-in',
component: SignIn, component: SignIn,
},{ },{
path: "/register", path: '/register',
component: Register, component: Register,
},{ },{
path: "/account", path: '/account',
component: Account, component: Account,
}, },
]); ]);
@@ -61,7 +65,7 @@ RouterFactory.configure(factory => {
component: StoryBook, component: StoryBook,
}, { }, {
path: '/icon-admin', path: '/icon-admin',
name: "iconAdmin", name: 'iconAdmin',
component: IconAdmin, component: IconAdmin,
}, },
]); ]);

View File

@@ -0,0 +1,14 @@
/**
* Some componets are used as targets for dialog animations often, and thus
* require a background color. Inheriting the color from the nearest ancestor
* that is known to have a background ensures that the background is
* consistent for both light and dark themes
**/
.v-list > div {
background-color: inherit;
}
.v-list > div > .v-list__tile {
background-color: inherit;
}

View File

@@ -1 +1,2 @@
import "./speedDial.css"; import './speedDial.css';
import './inheritBackgrounds.css';