Continued implementing sharing

This commit is contained in:
Thaum Rystra
2020-05-12 12:27:24 +02:00
parent dd213feb0a
commit 47206ccfc4
9 changed files with 465 additions and 83 deletions

View File

@@ -3,18 +3,64 @@ import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
const setPublic = new ValidatedMethod({
name: 'sharing.methods.setPublic',
validate: new SimpleSchema({
docRef: RefSchema,
public: { type: Boolean },
isPublic: { type: Boolean },
}).validator(),
run({docRef, public}){
run({docRef, isPublic}){
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
getCollectionByName(docRef.collection).update(docRef.id, {$set: {public}});
return getCollectionByName(docRef.collection).update(docRef.id, {
$set: {public: isPublic},
});
},
});
export { setPublic };
const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.methods.updateUserSharePermissions',
validate: new SimpleSchema({
docRef: RefSchema,
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
role: {
type: String,
allowedValues: ['reader', 'writer', 'none'],
},
}).validator(),
run({docRef, userId, role}){
let doc = fetchDocByRef(docRef);
if (role === 'none'){
// only asser ownership if you aren't removing yourself
if (this.userId !== userId){
assertOwnership(doc, this.userId);
}
return getCollectionByName(docRef.collection).update(docRef.id, {
$pullAll: { readers: userId, writers: userId },
});
}
if (doc.owner === userId){
throw new Meteor.Error('Sharing update failed',
'User is already the owner of this document');
}
assertOwnership(doc, this.userId);
if (role === 'reader'){
return getCollectionByName(docRef.collection).update(docRef.id, {
$addToSet: { readers: userId },
$pullAll: { writers: userId },
});
} else if (role === 'writer'){
return getCollectionByName(docRef.collection).update(docRef.id, {
$addToSet: { writers: userId },
$pullAll: { readers: userId },
});
}
},
});
export { setPublic, updateUserSharePermissions };

View File

@@ -66,6 +66,16 @@ const userSchema = new SimpleSchema({
},
'subscribedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedCharacters: {
type: Array,
defaultValue: [],
max: 100,
},
'subscribedCharacters.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
@@ -151,6 +161,31 @@ Meteor.users.setUsername = new ValidatedMethod({
}
});
Meteor.users.subscribeToLibrary = new ValidatedMethod({
name: 'Users.methods.subscribeToLibrary',
validate: new SimpleSchema({
libraryId:{
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribe: {
type: Boolean,
},
}).validator(),
run({libraryId, subscribe}){
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe){
return Meteor.users.update(this.userId, {
$addToSet: {subscribedLibraries: libraryId},
});
} else {
return Meteor.users.update(this.userId, {
$pullAll: {subscribedLibraries: libraryId},
});
}
}
});
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
name: 'Users.methods.findUserByUsernameOrEmail',
validate: new SimpleSchema({

View File

@@ -8,6 +8,7 @@ Meteor.publish('user', function(){
username: 1,
apiKey: 1,
darkMode: 1,
subscribedLibraries: 1,
'services.patreon.id': 1,
'services.patreon.entitledCents': 1,
'services.patreon.entitledCentsOverride': 1,

View File

@@ -1,33 +1,33 @@
<template lang="html">
<!--use value for immutable, list for auto-updating children -->
<draggable
class="drag-area"
:value="children"
:group="group"
:animation="200"
:move="move"
@change="change"
ghost-class="ghost"
draggable=".item"
handle=".handle"
>
<tree-node
class="item"
v-for="child in children"
:node="child.node"
:children="child.children"
:group="group"
:key="child.node._id"
:selected-node-id="selectedNodeId"
:selected="selectedNodeId === child.node._id"
:organize="organize"
:lazy="lazy"
@selected="e => $emit('selected', e)"
@reordered="e => $emit('reordered', e)"
@reorganized="e => $emit('reorganized', e)"
@dragstart.native="e => e.dataTransfer.setData('cow', child.node && child.node.name)"
/>
</draggable>
<!--use value for immutable, list for auto-updating children -->
<draggable
class="drag-area"
:value="children"
:group="group"
:animation="200"
:move="move"
ghost-class="ghost"
draggable=".item"
handle=".handle"
@change="change"
>
<tree-node
v-for="child in children"
:key="child.node._id"
class="item"
:node="child.node"
:children="child.children"
:group="group"
:selected-node-id="selectedNodeId"
:selected="selectedNodeId === child.node._id"
:organize="organize"
:lazy="lazy"
@selected="e => $emit('selected', e)"
@reordered="e => $emit('reordered', e)"
@reorganized="e => $emit('reorganized', e)"
@dragstart.native="e => e.dataTransfer.setData('cow', child.node && child.node.name)"
/>
</draggable>
</template>
<script>
@@ -40,9 +40,6 @@
draggable,
TreeNode,
},
data(){ return {
expanded: false,
}},
props: {
node: Object,
group: String,
@@ -50,10 +47,13 @@
lazy: Boolean,
children: {
type: Array,
required: true,
default: () => [],
},
selectedNodeId: String,
},
data(){ return {
expanded: false,
}},
computed: {
hasChildren(){
return this.children && this.children.length;
@@ -63,7 +63,7 @@
},
},
methods: {
change({added, moved, removed}){
change({added, moved}){
let event = moved || added;
if (event){
let doc = event.element.node;
@@ -71,7 +71,7 @@
if (event.newIndex === 0){
newIndex = 0;
} else {
childBeforeNewIndex = this.children[event.newIndex - 1];
let childBeforeNewIndex = this.children[event.newIndex - 1];
newIndex = childBeforeNewIndex.node.order + 1;
}
if (moved){

View File

@@ -0,0 +1,121 @@
<template lang="html">
<div
class="layout row"
style="background-color: inherit;"
>
<div
class="layout column"
style="
background-color: inherit;
width: initial;
max-width: 100%;
min-width: 320px;
"
>
<v-toolbar
dense
flat
>
<v-spacer />
<v-switch
v-model="organize"
label="Organize"
class="mx-3"
style="flex-grow: 0; height: 32px;"
/>
</v-toolbar>
<library-contents-container
:library-id="$route.params.id"
:organize-mode="organize"
:selected-node-id="selected"
@selected="e => selected = e"
/>
</div>
<v-divider vertical />
<div
style="width: 100%; background-color: inherit;"
data-id="selected-node-card"
>
<v-toolbar
dense
flat
>
<property-icon
:type="selectedNode && selectedNode.type"
class="mr-2"
/>
<div class="title">
{{ getPropertyName(selectedNode && selectedNode.type) }}
</div>
<v-spacer />
<v-btn
v-if="selectedNode"
flat
icon
@click="editLibraryNode"
>
<v-icon>create</v-icon>
</v-btn>
</v-toolbar>
<v-card-text style="overflow-y: auto;">
<property-viewer :model="selectedNode" />
</v-card-text>
</div>
</div>
</template>
<script>
import PropertyViewer from '/imports/ui/properties/shared/PropertyViewer.vue';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import Libraries from '/imports/api/library/Libraries.js';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import LibraryContentsContainer from '/imports/ui/library/LibraryContentsContainer.vue';
export default {
components: {
LibraryContentsContainer,
PropertyViewer,
PropertyIcon,
},
data(){ return {
organize: false,
selected: undefined,
};},
watch:{
selectedNode(val){
this.$emit('selected', val)
},
'library.name'(value){
this.$store.commit('setPageTitle', value || 'Library');
},
},
methods: {
editLibraryNode(){
this.$store.commit('pushDialogStack', {
component: 'library-node-edit-dialog',
elementId: 'selected-node-card',
data: {_id: this.selected},
});
},
getPropertyName,
},
meteor: {
$subscribe: {
'libraries': [],
},
library(){
return Libraries.findOne(this.$route.params.id);
},
selectedNode(){
return LibraryNodes.findOne({
_id: this.selected,
removed: {$ne: true}
});
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,88 @@
<template lang="html">
<v-toolbar-items>
<v-btn
v-if="showSubscribeButton"
flat
:loading="loading"
@click="subscribe(!subscribed)"
>
{{ subscribed ? 'Unsubscribe' : 'Subscribe' }}
</v-btn>
<v-btn
v-if="canEdit"
flat
icon
data-id="library-edit-button"
@click="editLibrary(library._id)"
>
<v-icon>create</v-icon>
</v-btn>
</v-toolbar-items>
</template>
<script>
import Libraries from '/imports/api/library/Libraries.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
export default {
data(){ return {
loading: false,
}},
meteor: {
library(){
console.log(this.$route);
return Libraries.findOne(this.$route.params.id);
},
subscribed(){
let libraryId = this.$route.params.id;
let subs = Meteor.user().subscribedLibraries;
return subs.includes(libraryId);
},
showSubscribeButton(){
let userId = Meteor.userId();
let library = this.library;
if (!library) return;
console.log({library, userId});
if (
library.readers.includes(userId) ||
library.writers.includes(userId) ||
library.owner === userId
) {
return false
} else {
return true;
}
},
canEdit(){
try {
assertDocEditPermission(this.library, Meteor.userId());
console.log('can edit');
return true
} catch (e) {
console.log(e);
return false;
}
}
},
methods: {
subscribe(value){
this.loading = true;
Meteor.users.subscribeToLibrary.call({
libraryId: this.$route.params.id,
subscribe: value,
}, () => {
this.loading = false;
});
},
editLibrary(){
this.$store.commit('pushDialogStack', {
component: 'library-edit-dialog',
elementId: 'library-edit-button',
data: {_id: this.$route.params.id},
});
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,16 @@
<template lang="html">
<div>
<v-card class="ma-4">
<single-library />
</v-card>
</div>
</template>
<script>
import SingleLibrary from '/imports/ui/library/SingleLibrary.vue';
export default {
components: {
SingleLibrary,
},
};
</script>

View File

@@ -6,6 +6,8 @@ import LAUNCH_DATE from '/imports/constants/LAUNCH_DATE.js';
import Home from '/imports/ui/pages/Home.vue';
import CharacterList from '/imports/ui/pages/CharacterList.vue';
import Library from '/imports/ui/pages/Library.vue';
import SingleLibraryPage from '/imports/ui/pages/SingleLibraryPage.vue'
import SingleLibraryToolbarItems from '/imports/ui/library/SingleLibraryToolbarItems.vue'
import CharacterSheetPage from '/imports/ui/pages/CharacterSheetPage.vue';
import CharacterSheetToolbarItems from '/imports/ui/creature/character/CharacterSheetToolbarItems.vue';
import CharacterSheetToolbarExtension from '/imports/ui/creature/character/CharacterSheetToolbarExtension.vue';
@@ -100,6 +102,17 @@ RouterFactory.configure(factory => {
title: 'Library',
},
beforeEnter: ensurePatronTier5,
},{
name: 'singleLibrary',
path: '/library/:id',
components: {
default: SingleLibraryPage,
toolbarItems: SingleLibraryToolbarItems,
},
meta: {
title: 'Library',
},
beforeEnter: ensurePatronTier5,
},{
path: '/character/:id/:urlName',
components: {

View File

@@ -13,6 +13,15 @@
:value="!!model.public + ''"
@change="(value, ack) => setSheetPublic({value, ack})"
/>
<text-field
v-if="model.public && docRef.collection === 'libraries'"
disabled
label="Link"
:value="'https://beta.dicecloud.com' + this.$router.resolve({
name: 'singleLibrary',
params: { id: model._id },
}).href"
/>
<div class="layout row">
<text-field
label="Username or email"
@@ -20,42 +29,78 @@
:debounce-time="300"
@change="(value, ack) => getUser({value, ack})"
/>
<v-btn :disabled="userFoundState !== 'found'">
<v-btn
:disabled="userFoundState !== 'found'"
@click="updateSharing(userId, 'reader')"
>
Share
</v-btn>
</div>
<div class="sharedWith">
<h3 v-if="writers.length">
Can Edit
</h3>
<div
v-for="user in writers"
<v-list
two-lines
class="sharedWith"
>
<v-list-tile
v-for="user in sharedUsers"
:key="user._id"
>
{{ user }}
<v-btn
flat
icon
>
<v-icon>delete</v-icon>
</v-btn>
</div>
<h3 v-if="readers.length">
Can View
</h3>
<div
v-for="user in readers"
:key="user._id"
>
{{ user }}
<v-btn
flat
icon
>
<v-icon>delete</v-icon>
</v-btn>
</div>
</div>
<v-list-tile-content>
<v-list-tile-title>
{{ user.username || user._id }}
</v-list-tile-title>
<v-list-tile-sub-title>
{{ user.permission === 'writer' ? 'Can edit' : 'Can view' }}
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-menu
bottom
left
>
<template #activator="{ on }">
<v-btn
icon
v-on="on"
>
<v-icon>more_vert</v-icon>
</v-btn>
</template>
<v-list>
<v-list-tile
v-if="user.permission === 'reader'"
@click="updateSharing(user._id, 'writer')"
>
<v-list-tile-action>
<v-icon>create</v-icon>
</v-list-tile-action>
<v-list-tile-title>Can edit</v-list-tile-title>
</v-list-tile>
<v-list-tile
v-if="user.permission === 'writer'"
@click="updateSharing(user._id, 'reader')"
>
<v-list-tile-action>
<v-icon>remove_red_eye</v-icon>
</v-list-tile-action>
<v-list-tile-title>View only</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="updateSharing(user._id, 'none')">
<v-list-tile-action>
<v-icon>delete</v-icon>
</v-list-tile-action>
<v-list-tile-title>Remove</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</v-list-tile-action>
</v-list-tile>
</v-list>
<v-fade-transition>
<v-progress-circular
v-if="!$subReady.userPublicProfiles"
indeterminate
/>
</v-fade-transition>
</div>
<v-spacer slot="actions" />
<v-btn
@@ -69,7 +114,10 @@
</template>
<script>
import { setPublic } from '/imports/api/sharing/sharing.js';
import {
setPublic,
updateUserSharePermissions
} from '/imports/api/sharing/sharing.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
@@ -92,7 +140,7 @@ export default {
setSheetPublic({value, ack}){
setPublic.call({
docRef: this.docRef,
public: value === 'true',
isPublic: value === 'true',
}, (error) => {
ack(error && error.reason || error);
});
@@ -127,6 +175,13 @@ export default {
}
});
},
updateSharing(userId, role){
updateUserSharePermissions.call({
docRef: this.docRef,
userId,
role,
});
},
},
meteor: {
model(){
@@ -134,21 +189,28 @@ export default {
let model = fetchDocByRef(this.docRef);
return model;
},
readers(){
if (this.model){
return Meteor.users.find({_id: {$in: this.model.readers}})
}
},
writers(){
if (this.model){
return Meteor.users.find({_id: {$in: this.model.writers}})
}
},
sharedUsers(){
let users = [];
Meteor.users.find({_id: {$in: this.model.readers}}).forEach(user => {
user.permission = 'reader';
users.push(user);
});
Meteor.users.find({_id: {$in: this.model.writers}}).forEach(user => {
user.permission = 'writer';
users.push(user);
});
users.sort(function(a, b){
if (a.username < b.username) return -1;
if (a.username > b.username) return 1;
return 0;
});
return users;
},
$subscribe: {
'userPublicProfiles'(){
let model = this.model;
if (!model) return false;
return [model.owner, ...model.writers, ...model.readers];
return [[model.owner, ...model.writers, ...model.readers]];
},
},
},