Library Collections UI built
This commit is contained in:
@@ -24,6 +24,11 @@ let LibrarySchema = new SimpleSchema({
|
|||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.name,
|
max: STORAGE_LIMITS.name,
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.summary,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
LibrarySchema.extend(SharingSchema);
|
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({
|
const removeLibrary = new ValidatedMethod({
|
||||||
name: 'libraries.remove',
|
name: 'libraries.remove',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -102,4 +130,4 @@ export function removeLibaryWork(libraryId){
|
|||||||
LibraryNodes.remove({'ancestors.id': libraryId});
|
LibraryNodes.remove({'ancestors.id': libraryId});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };
|
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
|||||||
/**
|
/**
|
||||||
* LibraryCollections are groups of libraries that are subscribed together at once
|
* 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: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
@@ -71,7 +71,14 @@ const updateLibraryCollection = new ValidatedMethod({
|
|||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
type: LibraryCollectionSchema.pick('name', 'description', 'libraries')
|
type: LibraryCollectionSchema
|
||||||
|
.pick('name', 'description', 'libraries')
|
||||||
|
.extend({ //make libraries optional
|
||||||
|
libraries: {
|
||||||
|
optional: true,
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import Libraries from '/imports/api/library/Libraries.js';
|
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 LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
import { union } from 'lodash';
|
||||||
|
|
||||||
const LIBRARY_NODE_TREE_FIELDS = {
|
const LIBRARY_NODE_TREE_FIELDS = {
|
||||||
_id: 1,
|
_id: 1,
|
||||||
@@ -41,25 +43,89 @@ const LIBRARY_NODE_TREE_FIELDS = {
|
|||||||
|
|
||||||
export { LIBRARY_NODE_TREE_FIELDS };
|
export { LIBRARY_NODE_TREE_FIELDS };
|
||||||
|
|
||||||
Meteor.publish('libraries', function(){
|
Meteor.publish('libraryCollection', function (libraryCollectionId) {
|
||||||
this.autorun(function (){
|
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 () {
|
||||||
let userId = this.userId;
|
let userId = this.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const user = Meteor.users.findOne(userId, {
|
const user = Meteor.users.findOne(userId, {
|
||||||
fields: {subscribedLibraries: 1}
|
fields: { subscribedLibraries: 1, subscribedLibraryCollections: 1 }
|
||||||
});
|
});
|
||||||
const subs = user && user.subscribedLibraries || [];
|
|
||||||
return Libraries.find({
|
this.autorun(function () {
|
||||||
$or: [
|
// Get the collections the user is subscribed to
|
||||||
{owner: this.userId},
|
const subCollections = user && user.subscribedLibraryCollections || [];
|
||||||
{writers: this.userId},
|
const libraryCollectionsCursor = LibraryCollections.find({
|
||||||
{readers: this.userId},
|
$or: [
|
||||||
{ _id: {$in: subs}, public: true },
|
{ owner: userId },
|
||||||
]
|
{ writers: userId },
|
||||||
}, {
|
{ readers: userId },
|
||||||
sort: {name: 1}
|
{ _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];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
|
|||||||
apiKey: 1,
|
apiKey: 1,
|
||||||
darkMode: 1,
|
darkMode: 1,
|
||||||
subscribedLibraries: 1,
|
subscribedLibraries: 1,
|
||||||
|
subscribedLibraryCollections: 1,
|
||||||
fileStorageUsed: 1,
|
fileStorageUsed: 1,
|
||||||
profile: 1,
|
profile: 1,
|
||||||
preferences: 1,
|
preferences: 1,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="accessRights === 'reader' || accessRights === 'writer'"
|
v-if="accessRights === 'reader' || accessRights === 'writer' || accessRights === 'public'"
|
||||||
bottom
|
bottom
|
||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
style="opacity: 0.4"
|
style="opacity: 0.4"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
{{ accessRights === 'reader' ? 'mdi-file-eye' : 'mdi-file-edit' }}
|
{{ accessIcon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ accessText }}</span>
|
<span>{{ accessText }}</span>
|
||||||
@@ -32,13 +32,24 @@ export default {
|
|||||||
else if (this.model.public) return 'public';
|
else if (this.model.public) return 'public';
|
||||||
else return 'denied'
|
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(){
|
accessText(){
|
||||||
switch (this.accessRights){
|
switch (this.accessRights){
|
||||||
case 'writer': return 'Shared with edit permission';
|
case 'writer': return 'Shared with edit permission';
|
||||||
case 'reader': return 'Shared as view-only';
|
case 'reader': return 'Shared as view-only';
|
||||||
case 'public': return 'Shared as publicly viewable';
|
case 'public': return 'Shared publically';
|
||||||
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const ExperienceInsertDialog = () => import( '/imports/ui/creature/experiences/E
|
|||||||
const ExperienceListDialog = () => import( '/imports/ui/creature/experiences/ExperienceListDialog.vue');
|
const ExperienceListDialog = () => import( '/imports/ui/creature/experiences/ExperienceListDialog.vue');
|
||||||
const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue');
|
const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue');
|
||||||
const LevelUpDialog = () => import('/imports/ui/creature/slots/LevelUpDialog.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 LibraryCreationDialog = () => import('/imports/ui/library/LibraryCreationDialog.vue');
|
||||||
const LibraryEditDialog = () => import('/imports/ui/library/LibraryEditDialog.vue');
|
const LibraryEditDialog = () => import('/imports/ui/library/LibraryEditDialog.vue');
|
||||||
const LibraryNodeCreationDialog = () => import('/imports/ui/library/LibraryNodeCreationDialog.vue');
|
const LibraryNodeCreationDialog = () => import('/imports/ui/library/LibraryNodeCreationDialog.vue');
|
||||||
@@ -40,6 +42,8 @@ export default {
|
|||||||
ExperienceListDialog,
|
ExperienceListDialog,
|
||||||
InviteDialog,
|
InviteDialog,
|
||||||
LevelUpDialog,
|
LevelUpDialog,
|
||||||
|
LibraryCollectionCreationDialog,
|
||||||
|
LibraryCollectionEditDialog,
|
||||||
LibraryCreationDialog,
|
LibraryCreationDialog,
|
||||||
LibraryEditDialog,
|
LibraryEditDialog,
|
||||||
LibraryNodeCreationDialog,
|
LibraryNodeCreationDialog,
|
||||||
|
|||||||
107
app/imports/ui/library/LibraryCollectionCreationDialog.vue
Normal file
107
app/imports/ui/library/LibraryCollectionCreationDialog.vue
Normal 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>
|
||||||
147
app/imports/ui/library/LibraryCollectionEditDialog.vue
Normal file
147
app/imports/ui/library/LibraryCollectionEditDialog.vue
Normal 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>
|
||||||
86
app/imports/ui/library/LibraryCollectionHeader.vue
Normal file
86
app/imports/ui/library/LibraryCollectionHeader.vue
Normal 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>
|
||||||
109
app/imports/ui/library/LibraryCollectionToolbar.vue
Normal file
109
app/imports/ui/library/LibraryCollectionToolbar.vue
Normal 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>
|
||||||
@@ -7,11 +7,17 @@
|
|||||||
</template>
|
</template>
|
||||||
<template>
|
<template>
|
||||||
<text-field
|
<text-field
|
||||||
label="name"
|
label="Name"
|
||||||
:value="library.name"
|
:value="library.name"
|
||||||
:debounce-time="0"
|
:debounce-time="0"
|
||||||
@change="nameChanged"
|
@change="nameChanged"
|
||||||
/>
|
/>
|
||||||
|
<text-area
|
||||||
|
label="Description"
|
||||||
|
:value="library.description"
|
||||||
|
:debounce-time="0"
|
||||||
|
@change="descriptionChanged"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
@@ -35,7 +41,8 @@
|
|||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
library: {
|
library: {
|
||||||
name: 'New Library',
|
name: 'New Library',
|
||||||
|
description: undefined,
|
||||||
},
|
},
|
||||||
valid: true,
|
valid: true,
|
||||||
}},
|
}},
|
||||||
@@ -49,6 +56,10 @@
|
|||||||
this.valid = false;
|
this.valid = false;
|
||||||
ack('Name is required')
|
ack('Name is required')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
descriptionChanged(val, ack){
|
||||||
|
this.library.description = val;
|
||||||
|
ack();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
:value="model.name"
|
:value="model.name"
|
||||||
@change="updateName"
|
@change="updateName"
|
||||||
/>
|
/>
|
||||||
|
<text-area
|
||||||
|
label="Description"
|
||||||
|
:value="model.description"
|
||||||
|
@change="updateDescription"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="removedDocs.length">
|
<template v-if="removedDocs.length">
|
||||||
<h3>Recently Deleted Properties</h3>
|
<h3>Recently Deleted Properties</h3>
|
||||||
@@ -71,9 +76,10 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
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 LibraryNodes, { restoreLibraryNode } from '/imports/api/library/LibraryNodes.js';
|
||||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||||
|
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -88,7 +94,12 @@ export default {
|
|||||||
updateLibraryName.call({_id: this._id, name: value}, (error) =>{
|
updateLibraryName.call({_id: this._id, name: value}, (error) =>{
|
||||||
ack(error && error.reason || error);
|
ack(error && error.reason || error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateDescription(value, ack){
|
||||||
|
updateLibraryDescription.call({_id: this._id, description: value}, (error) =>{
|
||||||
|
ack(error && error.reason || error);
|
||||||
|
});
|
||||||
|
},
|
||||||
remove(){
|
remove(){
|
||||||
let that = this;
|
let that = this;
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
@@ -101,10 +112,14 @@ export default {
|
|||||||
callback(confirmation){
|
callback(confirmation){
|
||||||
if(!confirmation) return;
|
if(!confirmation) return;
|
||||||
removeLibrary.call({_id: that._id}, (error) => {
|
removeLibrary.call({_id: that._id}, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
snackbar({
|
||||||
|
text: error.reason,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
that.$store.dispatch('popDialogStack')
|
that.$router.push({ name: 'library', replace: true });
|
||||||
|
that.$store.dispatch('popDialogStack');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
37
app/imports/ui/library/LibraryListTile.vue
Normal file
37
app/imports/ui/library/LibraryListTile.vue
Normal 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>
|
||||||
@@ -1,18 +1,216 @@
|
|||||||
<template lang="html">
|
<template>
|
||||||
<single-card-layout>
|
<div
|
||||||
<library-and-node
|
class="card-background"
|
||||||
:library-id="$route.params.id"
|
style="height: 100%"
|
||||||
/>
|
>
|
||||||
</single-card-layout>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import SingleCardLayout from '/imports/ui/layouts/SingleCardLayout.vue';
|
import { union } from 'lodash';
|
||||||
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
|
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||||
export default {
|
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
components: {
|
import LibraryCollections, { insertLibraryCollection } from '/imports/api/library/LibraryCollections.js';
|
||||||
SingleCardLayout,
|
import Libraries, { insertLibrary } from '/imports/api/library/Libraries.js';
|
||||||
LibraryAndNode,
|
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>
|
</script>
|
||||||
|
|||||||
72
app/imports/ui/pages/LibraryCollection.vue
Normal file
72
app/imports/ui/pages/LibraryCollection.vue
Normal 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>
|
||||||
18
app/imports/ui/pages/SingleLibrary.vue
Normal file
18
app/imports/ui/pages/SingleLibrary.vue
Normal 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>
|
||||||
@@ -7,7 +7,8 @@ const About = () => import('/imports/ui/pages/About.vue');
|
|||||||
const CharacterList = () => import('/imports/ui/pages/CharacterList.vue');
|
const CharacterList = () => import('/imports/ui/pages/CharacterList.vue');
|
||||||
const CharacterListToolbarItems = () => import('/imports/ui/creature/creatureList/CharacterListToolbarItems.vue');
|
const CharacterListToolbarItems = () => import('/imports/ui/creature/creatureList/CharacterListToolbarItems.vue');
|
||||||
const Library = () => import('/imports/ui/pages/Library.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 CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue');
|
||||||
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
|
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
|
||||||
const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.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 ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue' );
|
||||||
const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue');
|
const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue');
|
||||||
const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.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 Tabletops = () => import('/imports/ui/pages/Tabletops.vue');
|
||||||
const Tabletop = () => import('/imports/ui/pages/Tabletop.vue');
|
const Tabletop = () => import('/imports/ui/pages/Tabletop.vue');
|
||||||
const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue');
|
const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue');
|
||||||
@@ -120,7 +123,8 @@ RouterFactory.configure(router => {
|
|||||||
title: 'Home',
|
title: 'Home',
|
||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
path: '/characterList',
|
path: '/character-list',
|
||||||
|
alias: '/characterList',
|
||||||
components: {
|
components: {
|
||||||
default: CharacterList,
|
default: CharacterList,
|
||||||
toolbarItems: CharacterListToolbarItems,
|
toolbarItems: CharacterListToolbarItems,
|
||||||
@@ -129,7 +133,8 @@ RouterFactory.configure(router => {
|
|||||||
title: 'Character List',
|
title: 'Character List',
|
||||||
},
|
},
|
||||||
beforeEnter: ensureLoggedIn,
|
beforeEnter: ensureLoggedIn,
|
||||||
},{
|
}, {
|
||||||
|
name: 'library',
|
||||||
path: '/library',
|
path: '/library',
|
||||||
components: {
|
components: {
|
||||||
default: Library,
|
default: Library,
|
||||||
@@ -142,12 +147,22 @@ RouterFactory.configure(router => {
|
|||||||
name: 'singleLibrary',
|
name: 'singleLibrary',
|
||||||
path: '/library/:id',
|
path: '/library/:id',
|
||||||
components: {
|
components: {
|
||||||
default: Library,
|
default: SingleLibrary,
|
||||||
toolbar: SingleLibraryToolbar,
|
toolbar: SingleLibraryToolbar,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Library',
|
title: 'Library',
|
||||||
},
|
},
|
||||||
|
},{
|
||||||
|
name: 'libraryCollection',
|
||||||
|
path: '/library-collection/:id',
|
||||||
|
components: {
|
||||||
|
default: LibraryCollection,
|
||||||
|
toolbar: LibraryCollectionToolbar,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: 'Library Collection',
|
||||||
|
},
|
||||||
},{
|
},{
|
||||||
path: '/character/:id',
|
path: '/character/:id',
|
||||||
alias: '/character/:id/:urlName',
|
alias: '/character/:id/:urlName',
|
||||||
|
|||||||
3
app/imports/ui/styles/cardTitles.css
Normal file
3
app/imports/ui/styles/cardTitles.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.v-card__title {
|
||||||
|
word-break: normal !important;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import './cardColors.css';
|
import './cardColors.css';
|
||||||
|
import './cardTitles.css';
|
||||||
import './centeredInputs.css';
|
import './centeredInputs.css';
|
||||||
import './denseLists.css';
|
import './denseLists.css';
|
||||||
import './fitAvatars.css';
|
import './fitAvatars.css';
|
||||||
|
|||||||
Reference in New Issue
Block a user