Reworked log data format, overhauled snackbar

This commit is contained in:
Stefan Zermatten
2021-03-28 12:31:39 +02:00
parent ada1355c29
commit 3c26bb2fc6
27 changed files with 273 additions and 244 deletions

View File

@@ -0,0 +1,16 @@
// Modified from https://gitlab.com/tozd/vue/snackbar-que
import Vue from 'vue';
const globalState = Vue.observable({queue: []});
let lastSnackbarId = 0;
function snackbar(data) {
globalState.queue.push({
data,
id: ++lastSnackbarId,
enqueuedAt: new Date(),
shown: false,
});
}
export {snackbar, globalState}

View File

@@ -0,0 +1,128 @@
<template lang="html">
<v-snackbar
bottom
left
v-bind="$attrs"
:value="isShown"
:timeout="timeout"
@input="value => isShown = value"
>
<div class="layout align-center">
<template v-if="snackbar && snackbar.data">
<div v-if="snackbar.data.text">
{{ snackbar.data.text }}
</div>
<template v-else-if="snackbar.data.content">
<log-content :model="snackbar.data.content" />
</template>
<v-spacer />
<v-btn
v-if="snackbar.data.callback"
color="primary"
text
@click="closeSnackbar(); snackbar.data.callback()"
>
{{ snackbar.data.callbackName }}
</v-btn>
</template>
</div>
<template #action="{ attrs }">
<v-btn
icon
v-bind="attrs"
@click="closeSnackbar"
>
<v-icon>close</v-icon>
</v-btn>
</template>
</v-snackbar>
</template>
<script lang="js">
// Modified from https://gitlab.com/tozd/vue/snackbar-queue
import { globalState } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import LogContent from '/imports/ui/log/LogContent.vue';
export default {
components: {
LogContent,
},
props: {
timeout: {
type: Number,
default: 6000,
},
pause: {
type: Number,
default: 300,
},
},
data() {
return {
isShown: false,
snackbar: null,
};
},
watch: {
isShown(newValue) {
if (newValue === false && this.snackbar) {
const snackbarIndex = globalState.queue.findIndex((element) => element.id === this.snackbar.id);
if (snackbarIndex > -1) {
globalState.queue.splice(snackbarIndex, 1);
}
this.snackbar = null;
}
},
},
created() {
this.handle = null;
this.unwait = null;
this.showNextSnackbar();
},
methods: {
clearSnackbarState() {
if (this.handle) {
clearTimeout(this.handle);
this.handle = null;
}
if (this.unwait) {
this.unwait();
this.unwait = null;
}
},
showNextSnackbar() {
this.clearSnackbarState();
// Wait for the first next snackbar to be available.
this.unwait = this.$wait(function () {
// Snackbars are enqueued from oldest to newest and "find" searches array elements in
// same order as well, so the first one which matches is also the oldest one.
return globalState.queue.find((element) => element.shown === false);
}, function (snackbar) {
this.unwait = null;
snackbar.shown = true;
this.snackbar = snackbar;
this.isShown = true;
this.handle = setTimeout(() => {
this.handle = null;
this.showNextSnackbar();
}, this.timeout + this.pause);
});
},
closeSnackbar() {
this.clearSnackbarState();
this.isShown = false;
setTimeout(() => {
this.showNextSnackbar();
}, this.pause);
},
},
};
</script>

View File

@@ -1,46 +0,0 @@
<template lang="html">
<v-snackbar
v-if="snackbar"
:key="snackbar.text"
auto-height
bottom
:value="true"
:timeout="0"
>
{{ snackbar.text }}
<v-btn
v-if="snackbar.callback"
class="primary--text"
icon
@click="doCallback"
>
{{ snackbar.callbackName }}
</v-btn>
<v-btn
v-if="snackbar.showCloseButton"
icon
@click="$store.dispatch('closeSnackbar')"
>
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
</template>
<script lang="js">
export default {
computed: {
snackbar(){
return this.$store.state.snackbars.snackbars[0];
}
},
methods: {
doCallback(){
this.snackbar.callback();
this.$store.dispatch('closeSnackbar')
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,47 +0,0 @@
const snackbarStore = {
state: {
snackbars: [],
snackbarTimout: undefined,
},
mutations: {
addSnackbar(state, value){
state.snackbars.push(value)
},
closeCurrentSnackbar (state){
state.snackbars.shift();
},
cancelSnackbarTimeout (state){
if(state.snackbarTimout){
clearTimeout(state.snackbarTimout);
}
},
setSnackbarTimout(state, value){
state.snackbarTimout = value;
},
},
actions: {
snackbar({dispatch, commit}, value){
// value = {
// text,
// showCloseButton,
// callback,
// callbackName
// }
commit('addSnackbar', value);
commit('setSnackbarTimout', setTimeout(() => {
dispatch('closeSnackbar');
}, 5000));
},
closeSnackbar({dispatch, commit, state}){
commit('closeCurrentSnackbar');
commit('cancelSnackbarTimeout');
if (state.snackbars.length){
commit('setSnackbarTimout', setTimeout(() => {
dispatch('closeSnackbar');
}, 5000));
}
},
}
};
export default snackbarStore;

View File

@@ -78,6 +78,7 @@
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
@@ -119,13 +120,10 @@
this.logObserver = CreatureLogs.find({
creatureId: this.creatureId,
}).observe({
added(doc){
added({content}){
if (!that.$subReady.singleCharacter) return;
if (that.$store.state.rightDrawer) return;
that.$store.dispatch('snackbar', {
text: doc.name,
showCloseButton: true,
});
snackbar({content});
},
});
},

View File

@@ -80,6 +80,7 @@ import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js'
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { get, findLast } from 'lodash';
import equipItem from '/imports/api/creature/creatureProperties/methods/equipItem.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -194,7 +195,7 @@ export default {
} else {
this.$store.dispatch('popDialogStack');
}
this.$store.dispatch('snackbar', {
snackbar({
text: `Deleted ${getPropertyTitle(this.model)}`,
callbackName: 'undo',
callback(){

View File

@@ -57,6 +57,7 @@ import softRemoveProperty from '/imports/api/creature/creatureProperties/methods
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
@@ -104,7 +105,7 @@ export default {
},
remove(model){
softRemoveProperty.call({_id: model._id});
this.$store.dispatch('snackbar', {
snackbar({
text: `Deleted ${getPropertyTitle(model)}`,
callbackName: 'undo',
callback(){

View File

@@ -65,7 +65,7 @@
name="rightDrawer"
/>
<dialog-stack />
<snackbars />
<snackbar-queue />
</v-app>
</template>
@@ -74,13 +74,13 @@
import Sidebar from '/imports/ui/layouts/Sidebar.vue';
import DialogStack from '/imports/ui/dialogStack/DialogStack.vue';
import { mapMutations } from 'vuex';
import Snackbars from '/imports/ui/components/snackbars/Snackbars.vue';
import SnackbarQueue from '/imports/ui/components/snackbars/SnackbarQueue.vue';
export default {
components: {
Sidebar,
DialogStack,
Snackbars,
SnackbarQueue,
},
data(){return {
name: 'Home',

View File

@@ -0,0 +1,51 @@
<template lang="html">
<div class="log-content">
<div
v-for="(content, index) in model"
:key="index"
class="content-line"
>
<h4 class="content-name">
{{ content.name }}
</h4>
<markdown-text
v-if="content.value"
class="content-value"
:markdown="content.value"
/>
</div>
</div>
</template>
<script lang="js">
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
export default {
components: {
MarkdownText,
},
props: {
model: {
type: Array,
default: () => [],
},
},
}
</script>
<style lang="css" scoped>
.content-line {
min-height: 24px;
margin-top: 2px;
margin-bottom: 2px;
}
.content-line .details {
display: inline-block;
}
</style>
<style lang="css">
.log-content .content-value > p:last-of-type{
margin-bottom: 0;
}
</style>

View File

@@ -6,42 +6,17 @@
v-if="model.text || (model.content && model.content.length)"
class="pa-2"
>
<div
v-for="(content, index) in model.content"
:key="index"
class="content-line"
>
<h4>
{{ content.name }}
</h4>
<span
v-if="content.error"
class="error"
>{{ content.error }}</span>
{{ content.resultPrefix }}
<span
v-if="content.result"
class="subheading font-weight-bold mx-1"
>{{ content.result }}</span>
<span
v-if="content.details"
>{{ content.details }}</span>
<markdown-text
v-if="content.description"
class="description"
:markdown="content.description"
/>
</div>
<log-content :model="model.content" />
</v-card-text>
</v-card>
</template>
<script lang="js">
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import LogContent from '/imports/ui/log/LogContent.vue';
export default {
components: {
MarkdownText,
LogContent,
},
props: {
model: {
@@ -51,20 +26,3 @@ export default {
},
}
</script>
<style lang="css" scoped>
.content-line {
min-height: 24px;
margin-top: 2px;
margin-bottom: 2px;
}
.content-line .details {
display: inline-block;
}
</style>
<style lang="css">
.log-entry .description > p:last-of-type{
margin-bottom: 0;
}
</style>

View File

@@ -3,6 +3,7 @@ import store from '/imports/ui/vuexStore.js';
import VueMeteorTracker from 'vue-meteor-tracker';
import AppLayout from '/imports/ui/layouts/AppLayout.vue';
import ReactiveProvide from 'vue-reactive-provide';
import VueObserverUtils from '@tozd/vue-observer-utils';
import router from '/imports/ui/router.js';
import themes from '/imports/ui/themes.js';
import '/imports/ui/components/global/globalIndex.js';
@@ -35,7 +36,8 @@ Vue.use(Vuetify, {
});
Vue.use(ReactiveProvide, {
name: 'reactiveProvide', // default value
})
});
Vue.use(VueObserverUtils);
// App start
Meteor.startup(() => {

View File

@@ -1,14 +1,12 @@
import Vue from 'vue';
import Vuex from 'vuex';
import dialogStackStore from '/imports/ui/dialogStack/dialogStackStore.js';
import snackbarStore from '/imports/ui/components/snackbars/snackboxStore.js';
Vue.use(Vuex);
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
modules: {
dialogStack: dialogStackStore,
snackbars: snackbarStore,
},
state: {
drawer: undefined,