Added library list UI
This commit is contained in:
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
53
app/imports/ui/library/LibraryCreationDialog.vue
Normal file
53
app/imports/ui/library/LibraryCreationDialog.vue
Normal 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>
|
||||||
71
app/imports/ui/pages/Libraries.vue
Normal file
71
app/imports/ui/pages/Libraries.vue
Normal 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>
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
14
app/imports/ui/styles/inheritBackgrounds.css
Normal file
14
app/imports/ui/styles/inheritBackgrounds.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
import "./speedDial.css";
|
import './speedDial.css';
|
||||||
|
import './inheritBackgrounds.css';
|
||||||
|
|||||||
Reference in New Issue
Block a user