Library Collections UI built

This commit is contained in:
Stefan Zermatten
2022-07-17 22:48:48 +02:00
parent ee89a052bc
commit bf9639ae59
19 changed files with 982 additions and 46 deletions

View File

@@ -24,6 +24,11 @@ let LibrarySchema = new SimpleSchema({
type: String,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.summary,
},
});
LibrarySchema.extend(SharingSchema);
@@ -76,6 +81,29 @@ const updateLibraryName = new ValidatedMethod({
},
});
const updateLibraryDescription = new ValidatedMethod({
name: 'libraries.updateDescription',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
description: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, description}){
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {description}});
},
});
const removeLibrary = new ValidatedMethod({
name: 'libraries.remove',
validate: new SimpleSchema({
@@ -102,4 +130,4 @@ export function removeLibaryWork(libraryId){
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };

View File

@@ -10,9 +10,9 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/**
* LibraryCollections are groups of libraries that are subscribed together at once
*/
let LibraryCollections = new Mongo.Collection('librarycollections');
const LibraryCollections = new Mongo.Collection('libraryCollections');
let LibraryCollectionSchema = new SimpleSchema({
const LibraryCollectionSchema = new SimpleSchema({
name: {
type: String,
optional: true,
@@ -71,7 +71,14 @@ const updateLibraryCollection = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id,
},
update: {
type: LibraryCollectionSchema.pick('name', 'description', 'libraries')
type: LibraryCollectionSchema
.pick('name', 'description', 'libraries')
.extend({ //make libraries optional
libraries: {
optional: true,
defaultValue: undefined,
},
}),
}
},
rateLimit: {

View File

@@ -1,7 +1,9 @@
import SimpleSchema from 'simpl-schema';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
import { union } from 'lodash';
const LIBRARY_NODE_TREE_FIELDS = {
_id: 1,
@@ -40,26 +42,90 @@ const LIBRARY_NODE_TREE_FIELDS = {
}
export { LIBRARY_NODE_TREE_FIELDS };
Meteor.publish('libraryCollection', function (libraryCollectionId) {
this.autorun(function () {
let userId = this.userId;
if (!userId) return [];
this.autorun(function () {
const libraryCollectionCursor = LibraryCollections.find({
_id: libraryCollectionId,
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
});
const libraryCollection = libraryCollectionCursor.fetch()[0];
if (!libraryCollection) return [ libraryCollectionCursor ];
this.autorun(function () {
const libraryCursor = Libraries.find({
_id: {$in: libraryCollection.libraries},
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
}, {
sort: { name: 1 }
});
return [ libraryCollectionCursor, libraryCursor ];
});
});
})
});
Meteor.publish('libraries', function(){
this.autorun(function (){
Meteor.publish('libraries', function () {
this.autorun(function () {
let userId = this.userId;
if (!userId) {
return [];
}
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
fields: { subscribedLibraries: 1, subscribedLibraryCollections: 1 }
});
const subs = user && user.subscribedLibraries || [];
return Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{ _id: {$in: subs}, public: true },
]
}, {
sort: {name: 1}
this.autorun(function () {
// Get the collections the user is subscribed to
const subCollections = user && user.subscribedLibraryCollections || [];
const libraryCollectionsCursor = LibraryCollections.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: subCollections }, public: true },
]
}, {
sort: { name: 1 }
});
// Collate all the libraryIds in those collections
let collectionLibIds = [];
libraryCollectionsCursor.forEach(libCollection => {
collectionLibIds = union(collectionLibIds, libCollection.libraries);
});
// Get the libraries the user is subscribed to directly
const subs = user && user.subscribedLibraries || [];
// Combine all the library Ids
const libIds = union(collectionLibIds, subs);
this.autorun(function () {
const librariesCursor = Libraries.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: libIds }, public: true },
]
}, {
sort: { name: 1 }
});
return [librariesCursor, libraryCollectionsCursor];
});
});
});
});

View File

@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
subscribedLibraryCollections: 1,
fileStorageUsed: 1,
profile: 1,
preferences: 1,

View File

@@ -1,6 +1,6 @@
<template lang="html">
<v-tooltip
v-if="accessRights === 'reader' || accessRights === 'writer'"
v-if="accessRights === 'reader' || accessRights === 'writer' || accessRights === 'public'"
bottom
>
<template #activator="{ on }">
@@ -8,7 +8,7 @@
style="opacity: 0.4"
v-on="on"
>
{{ accessRights === 'reader' ? 'mdi-file-eye' : 'mdi-file-edit' }}
{{ accessIcon }}
</v-icon>
</template>
<span>{{ accessText }}</span>
@@ -32,13 +32,24 @@ export default {
else if (this.model.public) return 'public';
else return 'denied'
},
},
computed: {
accessIcon() {
switch (this.accessRights){
case 'writer': return 'mdi-file-edit';
case 'reader': return 'mdi-file-eye';
case 'public': return 'mdi-cloud';
default: return '';
}
},
accessText(){
switch (this.accessRights){
case 'writer': return 'Shared with edit permission';
case 'reader': return 'Shared as view-only';
case 'public': return 'Shared as publicly viewable';
case 'public': return 'Shared publically';
default: return '';
}
}
},
}
}
</script>

View File

@@ -12,6 +12,8 @@ const ExperienceInsertDialog = () => import( '/imports/ui/creature/experiences/E
const ExperienceListDialog = () => import( '/imports/ui/creature/experiences/ExperienceListDialog.vue');
const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue');
const LevelUpDialog = () => import('/imports/ui/creature/slots/LevelUpDialog.vue');
const LibraryCollectionCreationDialog = () => import('/imports/ui/library/LibraryCollectionCreationDialog.vue');
const LibraryCollectionEditDialog = () => import('/imports/ui/library/LibraryCollectionEditDialog.vue');
const LibraryCreationDialog = () => import('/imports/ui/library/LibraryCreationDialog.vue');
const LibraryEditDialog = () => import('/imports/ui/library/LibraryEditDialog.vue');
const LibraryNodeCreationDialog = () => import('/imports/ui/library/LibraryNodeCreationDialog.vue');
@@ -40,6 +42,8 @@ export default {
ExperienceListDialog,
InviteDialog,
LevelUpDialog,
LibraryCollectionCreationDialog,
LibraryCollectionEditDialog,
LibraryCreationDialog,
LibraryEditDialog,
LibraryNodeCreationDialog,

View File

@@ -0,0 +1,107 @@
<template lang="html">
<dialog-base>
<template slot="toolbar">
<v-toolbar-title>
New Collection
</v-toolbar-title>
</template>
<template>
<text-field
label="Name"
:value="libraryCollection.name"
:debounce-time="0"
@change="nameChanged"
/>
<text-area
label="Description"
:value="libraryCollection.description"
:debounce-time="0"
@change="descriptionChanged"
/>
<smart-select
label="Libraries"
:items="libraryOptions"
:value="libraryCollection.libraries"
:debounce-time="0"
multiple
chips
deletable-chips
no-data-text="No libraries found"
@change="librariesChanged"
/>
</template>
<template slot="actions">
<v-spacer />
<v-btn
text
:disabled="!valid"
@click="$store.dispatch('popDialogStack', libraryCollection)"
>
Insert Collection
</v-btn>
</template>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Libraries from '/imports/api/library/Libraries.js';
export default {
components: {
DialogBase,
},
data(){ return {
libraryCollection: {
name: 'New Collection',
description: undefined,
libraries: [],
},
valid: true,
}},
meteor: {
libraryOptions() {
const userId = Meteor.userId();
return Libraries.find(
{
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
},
{sort: {name: 1}}
).map(library => {
return {
text: library.name,
value: library._id,
};
});
}
},
methods: {
nameChanged(val, ack){
if (val){
this.libraryCollection.name = val;
this.valid = true,
ack();
} else {
this.valid = false;
ack('Name is required')
}
},
descriptionChanged(val, ack){
this.libraryCollection.description = val;
ack();
},
librariesChanged(val, ack){
this.libraryCollection.libraries = val;
ack();
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,147 @@
<template lang="html">
<dialog-base>
<template slot="toolbar">
<v-toolbar-title>
{{ model && model.name }}
</v-toolbar-title>
<v-spacer />
<v-btn
icon
data-id="share-library-button"
@click="share"
>
<v-icon>mdi-share-variant</v-icon>
</v-btn>
<v-btn
icon
data-id="delete-library-button"
@click="remove"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template v-if="model">
<text-field
label="name"
:value="model.name"
@change="(name, ack) => updateLibraryCollection({name}, ack)"
/>
<text-area
label="Description"
:value="model.description"
@change="(description, ack) => updateLibraryCollection({description}, ack)"
/>
<smart-select
label="Libraries"
:items="libraryOptions"
:value="model.libraries"
:debounce-time="0"
multiple
chips
deletable-chips
no-data-text="No libraries found"
@change="(libraries, ack) => updateLibraryCollection({libraries}, ack)"
/>
</template>
<template slot="actions">
<v-spacer />
<v-btn
text
data-id="delete-library-button"
@click="$store.dispatch('popDialogStack')"
>
Done
</v-btn>
</template>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import LibraryCollections, { updateLibraryCollection, removeLibraryCollection } from '/imports/api/library/LibraryCollections.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import Libraries from '/imports/api/library/Libraries.js';
export default {
components: {
DialogBase,
},
props: {
_id: String,
},
methods: {
updateLibraryCollection(update, ack){
updateLibraryCollection.call({_id: this._id, update}, (error) =>{
ack(error && error.reason || error);
});
},
remove(){
let that = this;
this.$store.commit('pushDialogStack', {
component: 'delete-confirmation-dialog',
elementId: 'delete-library-button',
data: {
name: this.model.name,
typeName: 'Collection'
},
callback(confirmation){
if(!confirmation) return;
removeLibraryCollection.call({_id: that._id}, (error) => {
if (error) {
console.error(error);
snackbar({
text: error.reason,
});
} else {
that.$router.push({ name: 'library', replace: true });
that.$store.dispatch('popDialogStack');
}
});
}
});
},
share(){
this.$store.commit('pushDialogStack', {
component: 'share-dialog',
elementId: 'share-library-button',
data: {
docRef: {
id: this._id,
collection: 'libraryCollections',
}
},
});
},
},
meteor: {
'$subscribe':{
libraries: [],
},
model(){
return LibraryCollections.findOne(this._id);
},
libraryOptions() {
const userId = Meteor.userId();
return Libraries.find(
{
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
},
{sort: {name: 1}}
).map(library => {
return {
text: library.name,
value: library._id,
};
});
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,86 @@
<template lang="html">
<v-list-item
style="min-height: 60px; min-width: 0;"
class="px-0 font-weight-bold"
>
<v-list-item-avatar>
<shared-icon :model="model" />
</v-list-item-avatar>
<v-list-item-title class="d-flex align-center">
<div
class="text-truncate text-no-wrap"
style="opacity: 0.7"
>
{{ model.name }}
</div>
<template v-if="!selection && !dense">
<v-spacer />
<v-btn
v-if="canEdit"
icon
style="flex-grow: 0"
@click.stop="editLibraryCollection"
>
<v-icon>
mdi-pencil
</v-icon>
</v-btn>
<v-btn
icon
style="flex-grow: 0"
:to="{name: 'libraryCollection', params: {id: model._id}}"
@click.stop
>
<v-icon>
mdi-forward
</v-icon>
</v-btn>
</template>
</v-list-item-title>
</v-list-item>
</template>
<script lang="js">
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default {
components: {
SharedIcon,
},
props: {
model: {
type: Object,
required: true,
},
open: Boolean,
selection: Boolean,
dense: Boolean,
},
data(){return {
renaming: false,
}},
meteor: {
canEdit(){
try {
assertDocEditPermission(this.model, Meteor.userId());
return true
} catch (e) {
return false;
}
}
},
methods: {
editLibraryCollection() {
this.$store.commit('pushDialogStack', {
data: { _id: this.model._id},
component: 'library-collection-edit-dialog',
elementId: `library-collection-${this.model._id}`,
});
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,109 @@
<template lang="html">
<v-app-bar
app
color="secondary"
dark
tabs
extended
dense
>
<v-app-bar-nav-icon @click="toggleDrawer" />
<v-btn
icon
@click="$router.push('/library')"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>
{{ libraryCollection && libraryCollection.name }}
</v-toolbar-title>
<v-spacer />
<v-btn
v-if="showSubscribeButton"
text
:loading="loading"
@click="subscribe(!subscribed)"
>
{{ subscribed ? 'Unsubscribe' : 'Subscribe' }}
</v-btn>
<v-btn
v-if="canEdit"
icon
data-id="library-collection-edit-button"
@click="editLibraryCollection"
>
<v-icon>mdi-cog</v-icon>
</v-btn>
</v-app-bar>
</template>
<script lang="js">
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { mapMutations } from 'vuex';
export default {
data(){ return {
loading: false,
}},
meteor: {
libraryCollection(){
return LibraryCollections.findOne(this.$route.params.id);
},
subscribed(){
const libraryCollectionId = this.$route.params.id;
const user = Meteor.user();
return user?.subscribedLibraryCollections?.includes(libraryCollectionId);
},
showSubscribeButton(){
let user = Meteor.user();
let libraryCollection = this.libraryCollection;
if (!user || !libraryCollection) return;
let userId = user._id;
if (user.subscribedLibraryCollections?.includes(libraryCollection._id)){
return true
} else if (
libraryCollection.readers.includes(userId) ||
libraryCollection.writers.includes(userId) ||
libraryCollection.owner === userId
) {
return false
} else {
return true;
}
},
canEdit(){
try {
assertDocEditPermission(this.libraryCollection, Meteor.userId());
return true
} catch (e) {
return false;
}
}
},
methods: {
...mapMutations([
'toggleDrawer',
]),
subscribe(value){
this.loading = true;
Meteor.users.subscribeToLibraryCollection.call({
libraryCollectionId: this.$route.params.id,
subscribe: value,
}, () => {
this.loading = false;
});
},
editLibraryCollection(){
this.$store.commit('pushDialogStack', {
component: 'library-collection-edit-dialog',
elementId: 'library-collection-edit-button',
data: {_id: this.$route.params.id},
});
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -7,11 +7,17 @@
</template>
<template>
<text-field
label="name"
label="Name"
:value="library.name"
:debounce-time="0"
@change="nameChanged"
/>
<text-area
label="Description"
:value="library.description"
:debounce-time="0"
@change="descriptionChanged"
/>
</template>
<template slot="actions">
<v-spacer />
@@ -35,7 +41,8 @@
},
data(){ return {
library: {
name: 'New Library',
name: 'New Library',
description: undefined,
},
valid: true,
}},
@@ -49,6 +56,10 @@
this.valid = false;
ack('Name is required')
}
},
descriptionChanged(val, ack){
this.library.description = val;
ack();
},
},
};

View File

@@ -26,6 +26,11 @@
:value="model.name"
@change="updateName"
/>
<text-area
label="Description"
:value="model.description"
@change="updateDescription"
/>
</template>
<template v-if="removedDocs.length">
<h3>Recently Deleted Properties</h3>
@@ -71,9 +76,10 @@
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Libraries, { updateLibraryName, removeLibrary } from '/imports/api/library/Libraries.js';
import Libraries, { updateLibraryName, updateLibraryDescription, removeLibrary } from '/imports/api/library/Libraries.js';
import LibraryNodes, { restoreLibraryNode } from '/imports/api/library/LibraryNodes.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
@@ -88,7 +94,12 @@ export default {
updateLibraryName.call({_id: this._id, name: value}, (error) =>{
ack(error && error.reason || error);
});
},
},
updateDescription(value, ack){
updateLibraryDescription.call({_id: this._id, description: value}, (error) =>{
ack(error && error.reason || error);
});
},
remove(){
let that = this;
this.$store.commit('pushDialogStack', {
@@ -101,10 +112,14 @@ export default {
callback(confirmation){
if(!confirmation) return;
removeLibrary.call({_id: that._id}, (error) => {
if (error) {
console.error(error);
if (error) {
console.error(error);
snackbar({
text: error.reason,
});
} else {
that.$store.dispatch('popDialogStack')
that.$router.push({ name: 'library', replace: true });
that.$store.dispatch('popDialogStack');
}
});
}

View File

@@ -0,0 +1,37 @@
<template
lang="html"
functional
>
<v-list-item
v-bind="$attrs"
:class="isSelected && 'primary--text v-list-item--active'"
>
<v-list-item-avatar>
<shared-icon :model="model" />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
<script lang="js" functional>
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default {
components: {
SharedIcon,
},
props: {
model: {
type: Object,
required: true,
},
selection: Boolean,
isSelected: Boolean,
dense: Boolean,
}
}
</script>

View File

@@ -1,18 +1,216 @@
<template lang="html">
<single-card-layout>
<library-and-node
:library-id="$route.params.id"
/>
</single-card-layout>
<template>
<div
class="card-background"
style="height: 100%"
>
<v-container>
<v-row justify="center">
<v-col
cols="12"
xl="8"
>
<v-card :class="{'mb-4': libraryCollections && libraryCollections.length}">
<v-list
expand
class="library-folder-list"
>
<library-list-tile
v-for="library in librariesWithoutCollection"
:key="library._id"
:model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}"
/>
<v-list-group
v-for="libraryCollection in libraryCollections"
:key="libraryCollection._id"
v-model="openCollections[libraryCollection._id]"
group="library-collection"
:data-id="`library-collection-${libraryCollection._id}`"
>
<template #activator>
<library-collection-header
:open="openCollections[libraryCollection._id]"
:model="libraryCollection"
/>
</template>
<library-list-tile
v-for="library in libraryCollection.libraryDocuments"
:key="library._id"
:model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}"
class="ml-4"
/>
</v-list-group>
</v-list>
<v-expand-transition>
<v-row
v-if="!$subReady.libraries"
align="center"
justify="center"
class="pa-4"
>
<v-progress-circular
indeterminate
color="primary"
size="32"
/>
</v-row>
</v-expand-transition>
</v-card>
<div class="layout justify-end mt-2">
<v-btn
v-if="paidBenefits"
text
data-id="insert-library-collection-button"
:loading="loadingInsertLibraryCollection"
@click="insertLibraryCollection"
>
Add Collection
</v-btn>
</div>
<v-btn
color="accent"
fab
fixed
bottom
right
data-id="insert-library-button"
:disabled="!paidBenefits"
@click="insertLibrary"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script lang="js">
import SingleCardLayout from '/imports/ui/layouts/SingleCardLayout.vue';
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
export default {
components: {
SingleCardLayout,
LibraryAndNode,
},
};
import { union } from 'lodash';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import LibraryCollections, { insertLibraryCollection } from '/imports/api/library/LibraryCollections.js';
import Libraries, { insertLibrary } from '/imports/api/library/Libraries.js';
import LibraryListTile from '/imports/ui/library/LibraryListTile.vue'
import LibraryCollectionHeader from '/imports/ui/library/LibraryCollectionHeader.vue';
export default {
components: {
LibraryListTile,
LibraryCollectionHeader,
},
data(){ return{
loadingInsertLibraryCollection: false,
openCollections: [],
}},
meteor: {
$subscribe: {
'libraries': [],
},
paidBenefits(){
let tier = getUserTier(Meteor.userId());
return tier && tier.paidBenefits;
},
libraryCollections(){
const userId = Meteor.userId();
if (!userId) return;
const subCollections = Meteor.user().subscribedLibraryCollections || [];
return LibraryCollections.find({
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ _id: { $in: subCollections }, public: true },
]
}, {
sort: { name: 1 }
}).map(libCollection => {
libCollection.libraryDocuments = Libraries.find({
_id: {$in: libCollection.libraries},
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
}, {
sort: { name: 1 }
}).fetch();
return libCollection;
});
},
librariesWithoutCollection() {
const userId = Meteor.userId();
if (!this.libraryCollections) return;
// Collate the IDs of all the libraries in collections
let collectedLibraries = [];
this.libraryCollections.forEach(libCollection => {
collectedLibraries = union(collectedLibraries, libCollection.libraries);
});
// return the libraries with IDs not in that list
return Libraries.find(
{
_id: {$nin: collectedLibraries},
$or: [
{ owner: userId },
{ writers: userId },
{ readers: userId },
{ public: true },
]
},
{sort: {name: 1}}
);
},
},
methods: {
insertLibrary() {
const self = this;
if (this.paidBenefits){
this.$store.commit('pushDialogStack', {
component: 'library-creation-dialog',
elementId: 'insert-library-button',
callback(library){
if (!library) return;
return insertLibrary.call(library, (error, libraryId) => {
if (error){
console.error(error);
snackbar({
text: error.reason,
});
} else {
self.$router.push({
name: 'singleLibrary',
params: { id: libraryId }
});
}
});
}
});
} else {
this.$store.commit('pushDialogStack', {
component: 'tier-too-low-dialog',
elementId: 'insert-library-button',
});
}
},
insertLibraryCollection() {
this.$store.commit('pushDialogStack', {
component: 'library-collection-creation-dialog',
elementId: 'insert-library-collection-button',
callback(libraryCollection){
if (!libraryCollection) return;
const id = insertLibraryCollection.call(libraryCollection, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
return `library-collection-${id}`
}
});
},
},
};
</script>

View File

@@ -0,0 +1,72 @@
<template lang="html">
<v-container class="pa-6">
<v-row
v-if="collection && collection.description"
justify="center"
align="stretch"
>
<v-col
cols="12"
>
<v-card>
<v-card-text>
<markdown-text :markdown="collection.description" />
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row
align="stretch"
>
<v-col
v-for="library in libraries"
:key="library._id"
cols="12"
sm="6"
md="4"
lg="3"
xl="2"
>
<v-card
style="height: 100%;"
:to="{name: 'singleLibrary', params: {id: library._id}}"
>
<v-card-title>
{{ library.name }}
</v-card-title>
<v-card-text>
<markdown-text :markdown="library.description" />
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script lang="js">
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import Libraries from '/imports/api/library/Libraries.js';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
export default {
components: {
MarkdownText,
},
meteor: {
$subscribe: {
'libraryCollection'() {
return [this.$route.params.id];
},
},
collection() {
return LibraryCollections.findOne(this.$route.params.id);
},
libraries() {
if (!this.collection) return;
return Libraries.find({
_id: { $in: this.collection.libraries },
});
}
}
}
</script>

View File

@@ -0,0 +1,18 @@
<template lang="html">
<single-card-layout>
<library-and-node
:library-id="$route.params.id"
/>
</single-card-layout>
</template>
<script lang="js">
import SingleCardLayout from '/imports/ui/layouts/SingleCardLayout.vue';
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
export default {
components: {
SingleCardLayout,
LibraryAndNode,
},
};
</script>

View File

@@ -7,7 +7,8 @@ const About = () => import('/imports/ui/pages/About.vue');
const CharacterList = () => import('/imports/ui/pages/CharacterList.vue');
const CharacterListToolbarItems = () => import('/imports/ui/creature/creatureList/CharacterListToolbarItems.vue');
const Library = () => import('/imports/ui/pages/Library.vue');
const SingleLibraryToolbar = () => import('/imports/ui/library/SingleLibraryToolbar.vue');
const LibraryCollection = () => import('/imports/ui/pages/LibraryCollection.vue');
const LibraryCollectionToolbar = () => import('/imports/ui/library/LibraryCollectionToolbar.vue');
const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue');
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.vue');
@@ -24,6 +25,8 @@ const EmailVerificationError = () => import('/imports/ui/pages/EmailVerification
const ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue' );
const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue');
const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.vue');
const SingleLibrary = () => import('/imports/ui/pages/SingleLibrary.vue');
const SingleLibraryToolbar = () => import('/imports/ui/library/SingleLibraryToolbar.vue');
const Tabletops = () => import('/imports/ui/pages/Tabletops.vue');
const Tabletop = () => import('/imports/ui/pages/Tabletop.vue');
const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue');
@@ -120,7 +123,8 @@ RouterFactory.configure(router => {
title: 'Home',
},
},{
path: '/characterList',
path: '/character-list',
alias: '/characterList',
components: {
default: CharacterList,
toolbarItems: CharacterListToolbarItems,
@@ -129,7 +133,8 @@ RouterFactory.configure(router => {
title: 'Character List',
},
beforeEnter: ensureLoggedIn,
},{
}, {
name: 'library',
path: '/library',
components: {
default: Library,
@@ -142,12 +147,22 @@ RouterFactory.configure(router => {
name: 'singleLibrary',
path: '/library/:id',
components: {
default: Library,
default: SingleLibrary,
toolbar: SingleLibraryToolbar,
},
meta: {
title: 'Library',
},
},{
name: 'libraryCollection',
path: '/library-collection/:id',
components: {
default: LibraryCollection,
toolbar: LibraryCollectionToolbar,
},
meta: {
title: 'Library Collection',
},
},{
path: '/character/:id',
alias: '/character/:id/:urlName',

View File

@@ -0,0 +1,3 @@
.v-card__title {
word-break: normal !important;
}

View File

@@ -1,4 +1,5 @@
import './cardColors.css';
import './cardTitles.css';
import './centeredInputs.css';
import './denseLists.css';
import './fitAvatars.css';