diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js
index 358489ea..95444bc3 100644
--- a/app/imports/api/library/Libraries.js
+++ b/app/imports/api/library/Libraries.js
@@ -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 };
diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js
index 9a31e99e..aec4d351 100644
--- a/app/imports/api/library/LibraryCollections.js
+++ b/app/imports/api/library/LibraryCollections.js
@@ -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: {
diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js
index ebfdc936..808327ee 100644
--- a/app/imports/server/publications/library.js
+++ b/app/imports/server/publications/library.js
@@ -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];
+ });
});
});
});
diff --git a/app/imports/server/publications/users.js b/app/imports/server/publications/users.js
index 289f60dc..f096ccd2 100644
--- a/app/imports/server/publications/users.js
+++ b/app/imports/server/publications/users.js
@@ -10,6 +10,7 @@ Meteor.publish('user', function(){
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
+ subscribedLibraryCollections: 1,
fileStorageUsed: 1,
profile: 1,
preferences: 1,
diff --git a/app/imports/ui/components/SharedIcon.vue b/app/imports/ui/components/SharedIcon.vue
index 568bd91c..936f01d0 100644
--- a/app/imports/ui/components/SharedIcon.vue
+++ b/app/imports/ui/components/SharedIcon.vue
@@ -1,6 +1,6 @@
@@ -8,7 +8,7 @@
style="opacity: 0.4"
v-on="on"
>
- {{ accessRights === 'reader' ? 'mdi-file-eye' : 'mdi-file-edit' }}
+ {{ accessIcon }}
{{ accessText }}
@@ -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 '';
}
- }
+ },
}
}
diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js
index c3ccea03..cd9e7322 100644
--- a/app/imports/ui/dialogStack/DialogComponentIndex.js
+++ b/app/imports/ui/dialogStack/DialogComponentIndex.js
@@ -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,
diff --git a/app/imports/ui/library/LibraryCollectionCreationDialog.vue b/app/imports/ui/library/LibraryCollectionCreationDialog.vue
new file mode 100644
index 00000000..ed764930
--- /dev/null
+++ b/app/imports/ui/library/LibraryCollectionCreationDialog.vue
@@ -0,0 +1,107 @@
+
+
+
+
+ New Collection
+
+
+
+
+
+
+
+
+
+
+ Insert Collection
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/library/LibraryCollectionEditDialog.vue b/app/imports/ui/library/LibraryCollectionEditDialog.vue
new file mode 100644
index 00000000..8cf2e70c
--- /dev/null
+++ b/app/imports/ui/library/LibraryCollectionEditDialog.vue
@@ -0,0 +1,147 @@
+
+
+
+
+ {{ model && model.name }}
+
+
+
+ mdi-share-variant
+
+
+ mdi-delete
+
+
+
+ updateLibraryCollection({name}, ack)"
+ />
+ updateLibraryCollection({description}, ack)"
+ />
+ updateLibraryCollection({libraries}, ack)"
+ />
+
+
+
+
+ Done
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/library/LibraryCollectionHeader.vue b/app/imports/ui/library/LibraryCollectionHeader.vue
new file mode 100644
index 00000000..c31bc9fe
--- /dev/null
+++ b/app/imports/ui/library/LibraryCollectionHeader.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+ {{ model.name }}
+
+
+
+
+
+ mdi-pencil
+
+
+
+
+ mdi-forward
+
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/library/LibraryCollectionToolbar.vue b/app/imports/ui/library/LibraryCollectionToolbar.vue
new file mode 100644
index 00000000..4147836e
--- /dev/null
+++ b/app/imports/ui/library/LibraryCollectionToolbar.vue
@@ -0,0 +1,109 @@
+
+
+
+
+ mdi-arrow-left
+
+
+ {{ libraryCollection && libraryCollection.name }}
+
+
+
+ {{ subscribed ? 'Unsubscribe' : 'Subscribe' }}
+
+
+ mdi-cog
+
+
+
+
+
+
+
diff --git a/app/imports/ui/library/LibraryCreationDialog.vue b/app/imports/ui/library/LibraryCreationDialog.vue
index a68deee1..55f6894d 100644
--- a/app/imports/ui/library/LibraryCreationDialog.vue
+++ b/app/imports/ui/library/LibraryCreationDialog.vue
@@ -7,11 +7,17 @@
+
@@ -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();
},
},
};
diff --git a/app/imports/ui/library/LibraryEditDialog.vue b/app/imports/ui/library/LibraryEditDialog.vue
index 145799da..2b08a9ae 100644
--- a/app/imports/ui/library/LibraryEditDialog.vue
+++ b/app/imports/ui/library/LibraryEditDialog.vue
@@ -26,6 +26,11 @@
:value="model.name"
@change="updateName"
/>
+
Recently Deleted Properties
@@ -71,9 +76,10 @@
diff --git a/app/imports/ui/pages/Library.vue b/app/imports/ui/pages/Library.vue
index d564dee1..a1f1866a 100644
--- a/app/imports/ui/pages/Library.vue
+++ b/app/imports/ui/pages/Library.vue
@@ -1,18 +1,216 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Collection
+
+
+
+ mdi-plus
+
+
+
+
+
diff --git a/app/imports/ui/pages/LibraryCollection.vue b/app/imports/ui/pages/LibraryCollection.vue
new file mode 100644
index 00000000..414e2320
--- /dev/null
+++ b/app/imports/ui/pages/LibraryCollection.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ library.name }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/imports/ui/pages/SingleLibrary.vue b/app/imports/ui/pages/SingleLibrary.vue
new file mode 100644
index 00000000..d564dee1
--- /dev/null
+++ b/app/imports/ui/pages/SingleLibrary.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js
index 4295e83a..3108b505 100644
--- a/app/imports/ui/router.js
+++ b/app/imports/ui/router.js
@@ -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',
diff --git a/app/imports/ui/styles/cardTitles.css b/app/imports/ui/styles/cardTitles.css
new file mode 100644
index 00000000..b45ce973
--- /dev/null
+++ b/app/imports/ui/styles/cardTitles.css
@@ -0,0 +1,3 @@
+.v-card__title {
+ word-break: normal !important;
+}
\ No newline at end of file
diff --git a/app/imports/ui/styles/stylesIndex.js b/app/imports/ui/styles/stylesIndex.js
index a49f5cd0..383664b1 100644
--- a/app/imports/ui/styles/stylesIndex.js
+++ b/app/imports/ui/styles/stylesIndex.js
@@ -1,4 +1,5 @@
import './cardColors.css';
+import './cardTitles.css';
import './centeredInputs.css';
import './denseLists.css';
import './fitAvatars.css';