Library Collections UI built
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
|
||||
apiKey: 1,
|
||||
darkMode: 1,
|
||||
subscribedLibraries: 1,
|
||||
subscribedLibraryCollections: 1,
|
||||
fileStorageUsed: 1,
|
||||
profile: 1,
|
||||
preferences: 1,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
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>
|
||||
<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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
||||
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 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',
|
||||
|
||||
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 './cardTitles.css';
|
||||
import './centeredInputs.css';
|
||||
import './denseLists.css';
|
||||
import './fitAvatars.css';
|
||||
|
||||
Reference in New Issue
Block a user