Compare commits

..

4 Commits

Author SHA1 Message Date
Stefan Zermatten
bf4ce4f9f7 Hotfix: Adding properties to the tree, type selection fixed 2021-02-25 19:43:17 +02:00
Stefan Zermatten
2a983b0a94 User accounts can now be deleted with some UI to prevent accidental deletion 2021-02-25 14:28:51 +02:00
Stefan Zermatten
a5460bba0b Added floating action button to add properties directly to the sheet 2021-02-25 12:37:32 +02:00
Stefan Zermatten
df361236f5 Hotfix: Containers total weight now showing correctly on inventory tab 2021-02-24 15:18:32 +02:00
18 changed files with 439 additions and 30 deletions

View File

@@ -28,10 +28,14 @@ const removeCreature = new ValidatedMethod({
},
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -118,10 +118,14 @@ const removeLibrary = new ValidatedMethod({
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
Libraries.remove(_id);
this.unblock();
LibraryNodes.remove({'ancestors.id': _id});
this.unblock();
removeLibaryWork(_id)
}
})
});
export function removeLibaryWork(libraryId){
Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js';
const userSchema = new SimpleSchema({
username: {

View File

@@ -0,0 +1,61 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run(){
let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged into to delete your account');
// Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch();
creatures.forEach(creature => removeCreatureWork(creature._id));
// Remove permissions from all creatures
Creatures.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// Delete all libraries
let libraries = Libraries.find({owner: userId}, {fields: {_id: 1}}).fetch();
libraries.forEach(library => removeLibaryWork(library._id));
// Remove permissions from all creatures
Libraries.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// delete the account
Meteor.users.remove(userId);
}
});

View File

@@ -7,3 +7,4 @@ import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'

View File

@@ -0,0 +1,15 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
return [
Creatures.find({owner: userId}),
Libraries.find({owner: userId}),
]
});
});

View File

@@ -3,6 +3,8 @@
fab
small
v-bind="$attrs"
:disabled="disabled"
:style="disabled ? 'background-color: #616161 !important;' : ''"
@click="$emit('click')"
>
<v-icon>{{ icon }}</v-icon>
@@ -18,7 +20,7 @@
* component creates a v-btn with a label.
*/
export default {
props: ['icon', 'label'],
props: ['icon', 'label', 'disabled'],
}
</script>

View File

@@ -0,0 +1,104 @@
<template lang="html">
<v-speed-dial
v-if="speedDials && speedDials.length"
v-model="fab"
direction="bottom"
>
<template #activator>
<v-btn
v-model="fab"
color="primary"
fab
data-id="insert-creature-property-fab"
small
>
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
</template>
<labeled-fab
v-for="type in speedDials"
:key="type"
color="primary"
:data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name"
:icon="properties[type].icon"
:disabled="!editPermission"
@click="insertPropertyOfType(type)"
/>
</v-speed-dial>
</template>
<script>
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
const tabs = [
'stats',
'features',
'inventory',
'spells',
'character',
'tree',
];
export default {
components: {
LabeledFab,
},
props: {
editPermission: Boolean,
},
data(){return {
fab: false,
};},
computed: {
creatureId(){
return this.$route.params.id;
},
tabNumber(){
return this.$store.getters.tabById(this.creatureId);
},
speedDials(){
return this.speedDialsByTab[tabs[this.tabNumber]];
},
speedDialsByTab() { return {
'stats': ['attribute', 'skill'],
'features': ['feature'],
'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'],
'character': ['note'],
};},
properties(){
return PROPERTIES;
},
},
methods: {
insertPropertyOfType(type){
let that = this;
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-type-' + type,
data: {
forcedType: type,
},
callback(creatureProperty){
if (!creatureProperty) return;
creatureProperty.parent = {collection: 'creatures', id: that.creatureId};
creatureProperty.ancestors = [ {collection: 'creatures', id: that.creatureId}];
setDocToLastOrder({collection: CreatureProperties, doc: creatureProperty});
let id = insertProperty.call({creatureProperty});
return id;
}
});
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -5,6 +5,7 @@
:color="toolbarColor"
:dark="isDark"
:light="!isDark"
extended
tabs
dense
>
@@ -73,11 +74,12 @@
>
<div
:key="$route.meta.title"
style="width: 100%"
class="layout row"
>
<v-tabs
v-if="creature"
slot="extension"
class="flex"
style="min-width: 0"
centered
grow
max="100px"
@@ -106,6 +108,11 @@
Tree
</v-tab>
</v-tabs>
<v-spacer />
<character-sheet-fab
class="character-sheet-fab"
:edit-permission="editPermission"
/>
</div>
</v-fade-transition>
</v-toolbar>
@@ -119,8 +126,15 @@ import { theme } from '/imports/ui/theme.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue';
export default {
inject: {
context: { default: {} }
},
components: {
CharacterSheetFab,
},
data(){return {
theme,
}},
@@ -231,4 +245,8 @@ export default {
.character-sheet-toolbar .v-tabs__bar {
background: none !important;
}
.character-sheet-fab {
bottom: -24px;
margin-right: -8px;
}
</style>

View File

@@ -1,11 +1,14 @@
<template lang="html">
<selectable-property-dialog v-model="type">
<creature-property-insert-form
:type="type"
:property-name="getPropertyName(type)"
@back="type = undefined"
/>
</selectable-property-dialog>
<selectable-property-dialog
:value="forcedType || type"
@input="e => type = e"
>
<creature-property-insert-form
:type="forcedType || type"
:property-name="getPropertyName(forcedType || type)"
@back="back"
/>
</selectable-property-dialog>
</template>
<script>
@@ -14,15 +17,28 @@ import CreaturePropertyInsertForm from '/imports/ui/creature/creatureProperties/
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default {
data() { return {
type: undefined,
};},
components: {
SelectablePropertyDialog,
CreaturePropertyInsertForm,
},
props: {
forcedType: {
type: String,
default: undefined,
},
},
data() { return {
type: undefined,
};},
methods: {
getPropertyName,
back(){
if (this.forcedType){
this.$store.dispatch('popDialogStack');
} else {
this.type = undefined;
}
},
},
};
</script>

View File

@@ -70,14 +70,22 @@ export default {
};},
watch: {
type(newType){
if (!newType) return;
this.schema = propertySchemasIndex[newType];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = newType;
this.model = model;
this.changeType(newType);
},
},
mounted(){
this.changeType(this.type);
},
methods:{
changeType(type){
if (!type) return;
this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = type;
this.model = model;
}
},
}
</script>

View File

@@ -4,6 +4,7 @@ import CreaturePropertyCreationDialog from '/imports/ui/creature/creaturePropert
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue';
import DeleteUserAccountDialog from '/imports/ui/user/DeleteUserAccountDialog.vue';
import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue';
import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue';
import InviteDialog from '/imports/ui/user/InviteDialog.vue';
@@ -26,6 +27,7 @@ export default {
CreaturePropertyDialog,
CreaturePropertyFromLibraryDialog,
DeleteConfirmationDialog,
DeleteUserAccountDialog,
ExperienceInsertDialog,
ExperienceListDialog,
InviteDialog,

View File

@@ -13,7 +13,7 @@
name="toolbar"
/>
<v-toolbar
v-if="!$route.matched[0].components.toolbar"
v-if="!$route.matched[0] || !$route.matched[0].components.toolbar"
app
color="secondary"
dark

View File

@@ -141,6 +141,19 @@
</template>
</v-list>
</template>
<v-layout
row
justify-end
class="mt-3"
>
<v-btn
color="error"
data-id="delete-account-btn"
@click="deleteAccount"
>
Delete Account
</v-btn>
</v-layout>
</v-card>
</div>
</template>
@@ -270,6 +283,12 @@
this.updatePatreonLoading = false;
if (error) this.updatePatreonError = error;
});
},
deleteAccount(){
this.$store.commit('pushDialogStack', {
component: 'delete-user-account-dialog',
elementId: 'delete-account-btn',
});
}
},
}

View File

@@ -17,7 +17,7 @@
>
$vuetify.icons.weight
</v-icon>
{{ (model.contentsWeight ? 0 : model.contentsWeight || 0) + (model.weight || 0) }}
{{ (model.contentsWeightless ? 0 : model.contentsWeight || 0) + (model.weight || 0) }}
</v-toolbar-title>
<v-toolbar-title
class="layout row align-center"

View File

@@ -8,7 +8,7 @@
<v-toolbar-title slot="toolbar">
Add Library Content
</v-toolbar-title>
<property-selector @select="property => $emit('input', property)" />
<property-selector @select="type => $emit('input', type)" />
</dialog-base>
<div
v-show="value"

View File

@@ -0,0 +1,153 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Delete User Account
</v-toolbar-title>
<div>
<h2>Are you sure you want to delete your account?</h2>
<v-alert
:value="true"
icon="warning"
color="error"
outline
>
Deleted accounts can not be recovered
</v-alert>
<p>We will immediately delete your account and all of your data</p>
<p>Your username will become available to anyone on DiceCloud</p>
<template v-if="characters.length">
<h3 v-if="characters.length > 1">
These {{ characters.length }} characters will be deleted:
</h3>
<h3 v-else>
This character will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="character in characters"
:key="character._id"
:model="character"
/>
</v-list>
</template>
<template v-if="libraries.length">
<h3 v-if="libraries.length > 1">
These {{ libraries.length }} libraries will be deleted:
</h3>
<h3 v-else>
This library will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="library in libraries"
:key="library._id"
:model="library"
/>
</v-list>
</template>
<v-layout
column
align-start
>
<v-text-field
v-if="user.username"
v-model="usernameInput"
label="Type your username or email"
style="width: 350px;"
:error-messages="usernameInputValid ? undefined : ' '"
:append-icon="usernameInputValid ? 'done' : undefined"
/>
<v-text-field
v-model="verificationInput"
label="To verify type 'delete my account'"
style="width: 350px;"
:error-messages="verificationInputValid ? undefined : ' '"
:append-icon="verificationInputValid ? 'done' : undefined"
/>
<v-btn
class="mt-4"
color="error"
:disabled="!valid"
@click="deleteAccount"
>
Permanently delete account
</v-btn>
</v-layout>
</div>
<div
slot="actions"
class="layout row justify-end"
>
<v-btn
flat
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
</div>
</dialog-base>
</template>
<script>
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
import CreatureListTile from '/imports/ui/creature/CreatureListTile.vue';
export default {
components: {
DialogBase,
CreatureListTile,
},
data(){return {
usernameInput: '',
verificationInput: '',
};},
meteor: {
$subscribe: {
'ownedDocuments'(){
return [];
},
},
characters(){
return Creatures.find({owner: Meteor.userId()});
},
libraries(){
return Libraries.find({owner: Meteor.userId()});
},
user(){
return Meteor.user();
},
},
computed:{
usernameInputValid(){
let username = this.user.username;
if (!username) return true;
let input = this.usernameInput;
if (!input) return false;
if (input.toLowerCase() === username.toLowerCase()){
return true;
} else {
return false;
}
},
verificationInputValid(){
let input = this.verificationInput || '';
return input.toLowerCase() === 'delete my account'
},
valid(){
return this.usernameInputValid && this.verificationInputValid;
}
},
methods: {
deleteAccount(){
this.$router.push('/');
Meteor.users.deleteMyAccount.call();
this.$store.dispatch('popDialogStack');
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -25,7 +25,8 @@ for (const name in SVG_ICONS) {
}
Vue.use(VueMeteorTracker);
Vue.config.meteor.freeze = true
Vue.config.meteor.freeze = true;
Vue.config.devtools = true;
Vue.use(Vuetify, {
theme,
iconfont: 'md',