Implemented loading state for potentially long running operations

This commit is contained in:
Igor Zinken
2021-01-03 19:54:53 +01:00
parent ca74ac9c5e
commit 921e3c0133
8 changed files with 254 additions and 12 deletions

View File

@@ -75,7 +75,6 @@ npm run lint
* Implement iframe based rendering (as more compatible alternative to OffscreenCanvas) for effects
* Implement action queue when drawing, only execute drawing on update() hook
* Implement loaders on document load/save, image export and dropbox import
* Maintain cache for transformations and filters, rendered at the display destination size (invalidate on window resize)
* Drawing masks on a rotated layer that is panned (or mirrored) is broken
* Dragging of masks on rotated/mirror content is kinda broken

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -56,13 +56,14 @@
@close="closeModal()"
/>
</div>
<loader v-if="isLoading" />
<!-- notifications -->
<notifications />
</div>
</template>
<script>
import Vuex, { mapState, mapMutations, mapActions } from "vuex";
import Vuex, { mapState, mapGetters, mapMutations, mapActions } from "vuex";
import Vue from "vue";
import VueI18n from "vue-i18n";
import VTooltip from "v-tooltip";
@@ -71,7 +72,8 @@ import DocumentCanvas from "@/components/document-canvas/document-canvas";
import OptionsPanel from "@/components/options-panel/options-panel";
import Toolbox from "@/components/toolbox/toolbox";
import DialogWindow from "@/components/dialog-window/dialog-window";
import Notifications from '@/components/notifications/notifications';
import Notifications from "@/components/notifications/notifications";
import Loader from "@/components/loader/loader";
import { isMobile } from "@/utils/environment-util";
import ToolTypes from "@/definitions/tool-types";
import store from "./store";
@@ -95,11 +97,12 @@ export default {
store: new Vuex.Store( store ),
components: {
ApplicationMenu,
DocumentCanvas,
Toolbox,
DialogWindow,
OptionsPanel,
DocumentCanvas,
Loader,
Notifications,
OptionsPanel,
Toolbox,
},
data: () => ({
docWidth: "100%",
@@ -113,6 +116,9 @@ export default {
"modal",
"windowSize",
]),
...mapGetters([
"isLoading",
]),
activeModal() {
switch ( this.modal ) {
default:

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -183,6 +183,8 @@ export default {
"closeModal",
"showNotification",
"setDropboxConnected",
"setLoading",
"unsetLoading",
]),
...mapActions([
"loadDocument",
@@ -212,6 +214,7 @@ export default {
this.loading = false;
},
async handleNodeClick( node ) {
this.setLoading( "dbox" );
switch ( node.type ) {
case "folder":
await this.retrieveFiles( node.path );
@@ -233,6 +236,7 @@ export default {
this.closeModal();
break;
}
this.unsetLoading( "dbox" );
},
},
};

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2019-2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -110,10 +110,14 @@ export default {
methods: {
...mapMutations([
"closeModal",
"setLoading",
"unsetLoading",
]),
async exportImage() {
this.setLoading( "exp" );
const blob = await createDocumentSnapshot( this.activeDocument, this.type, this.quality );
saveBlobAsFile( blob, `${this.name}.${typeToExt(this.type)}` );
this.unsetLoading( "exp" );
this.closeModal();
},
},

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -88,12 +88,15 @@ export default {
"openDialog",
"setActiveDocumentName",
"showNotification",
"setLoading",
"unsetLoading",
]),
async requestSave() {
if ( !this.isValid ) {
return;
}
this.setActiveDocumentName( this.name );
this.setLoading( "save" );
try {
const blob = DocumentFactory.toBlob( this.activeDocument );
await uploadBlob( blob, `${this.name}${PROJECT_FILE_EXTENSION}` );
@@ -101,6 +104,7 @@ export default {
} catch ( e ) {
this.openDialog({ type: "error", message: this.$t( "errorOccurred" ) });
}
this.unsetLoading( "save" );
this.closeModal();
},
},

View File

@@ -0,0 +1,189 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2019 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
<template> <!-- functional> // https://github.com/vuejs/vue/issues/8822 -->
<div class="loader">
<div class="uil-default-css">
<div class="element" style="-webkit-transform: rotate(0deg) translate(0,-60px);transform: rotate(0deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(30deg) translate(0,-60px);transform:rotate(30deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(60deg) translate(0,-60px);transform:rotate(60deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(90deg) translate(0,-60px);transform:rotate(90deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(120deg) translate(0,-60px);transform:rotate(120deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(150deg) translate(0,-60px);transform:rotate(150deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(180deg) translate(0,-60px);transform:rotate(180deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(210deg) translate(0,-60px);transform:rotate(210deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(240deg) translate(0,-60px);transform:rotate(240deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(270deg) translate(0,-60px);transform:rotate(270deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(300deg) translate(0,-60px);transform:rotate(300deg) translate(0,-60px);"></div>
<div class="element" style="-webkit-transform:rotate(330deg) translate(0,-60px);transform:rotate(330deg) translate(0,-60px);"></div>
</div>
</div>
</template>
<script>
export default {
};
</script>
<style lang="scss" scoped>
@import "@/styles/_mixins";
$loaderSize: 200px;
.loader {
position: fixed;
background-image: $color-window-bg;
top: 50%;
left: 50%;
width: $loaderSize;
height: $loaderSize;
margin-left: -( $loaderSize / 2 );
margin-top: -( $loaderSize / 2 );
border-radius: 24px;
border-bottom: #{$spacing-medium - $spacing-xsmall} solid #000;
transform: scale( 0.33 );
z-index: 500;
}
.element {
position:absolute;
top: 80px;
left: 93px;
width: $spacing-medium;
border-radius: $spacing-medium;
height: 40px;
background: #FFF;
}
/* generated by loading.io */
@-webkit-keyframes uil-default-anim {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes uil-default-anim {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.uil-default-css > div:nth-of-type(1) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.uil-default-css {
position: relative;
width: $loaderSize;
height: $loaderSize;
transform:scale(0.75);
}
.uil-default-css > div:nth-of-type(2) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.4166666666666667s;
animation-delay: -0.4166666666666667s;
}
.uil-default-css > div:nth-of-type(3) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.33333333333333337s;
animation-delay: -0.33333333333333337s;
}
.uil-default-css > div:nth-of-type(4) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.25s;
animation-delay: -0.25s;
}
.uil-default-css > div:nth-of-type(5) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.16666666666666669s;
animation-delay: -0.16666666666666669s;
}
.uil-default-css > div:nth-of-type(6) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: -0.08333333333333331s;
animation-delay: -0.08333333333333331s;
}
.uil-default-css > div:nth-of-type(7) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0s;
animation-delay: 0s;
}
.uil-default-css > div:nth-of-type(8) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0.08333333333333337s;
animation-delay: 0.08333333333333337s;
}
.uil-default-css > div:nth-of-type(9) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0.16666666666666663s;
animation-delay: 0.16666666666666663s;
}
.uil-default-css > div:nth-of-type(10) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0.25s;
animation-delay: 0.25s;
}
.uil-default-css > div:nth-of-type(11) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0.33333333333333337s;
animation-delay: 0.33333333333333337s;
}
.uil-default-css > div:nth-of-type(12) {
-webkit-animation: uil-default-anim 1s linear infinite;
animation: uil-default-anim 1s linear infinite;
-webkit-animation-delay: 0.41666666666666663s;
animation-delay: 0.41666666666666663s;
}
</style>

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -56,6 +56,7 @@ export default {
panMode: false, // whether drag interactions with the document will pan its viewport
dialog: null, // currently opened dialog
modal: null, // currently opened modal
loadingStates: [], // wether one or more long running operations are running
notifications: [], // notification message queue
dropboxConnected: false,
windowSize: {
@@ -66,6 +67,7 @@ export default {
getters: {
// eslint-disable-next-line no-unused-vars
t: state => ( key, optArgs ) => translate( key, optArgs ),
isLoading: state => state.loadingStates.length > 0,
},
mutations: {
setMenuOpened( state, value ) {
@@ -86,6 +88,17 @@ export default {
setPanMode( state, value ) {
state.panMode = value;
},
setLoading( state, key ) {
if ( !state.loadingStates.includes( key )) {
state.loadingStates.push( key );
}
},
unsetLoading( state, key ) {
const idx = state.loadingStates.indexOf( key );
if ( idx > -1 ) {
state.loadingStates.splice( idx, 1 );
}
},
/**
* open a dialog window showing given title and message.
* types can be info, error or confirm. When type is confirm, optional

View File

@@ -1,7 +1,7 @@
import store, { PROJECT_FILE_EXTENSION } from "@/store";
import { LAYER_IMAGE } from "@/definitions/layer-types";
const { mutations, actions } = store;
const { getters, mutations, actions } = store;
let mockUpdateFn;
jest.mock( "@/services/keyboard-service", () => ({
@@ -19,6 +19,15 @@ jest.mock( "@/utils/file-util", () => ({
}))
describe( "Vuex store", () => {
describe( "getters", () => {
it( "should know when there is currently a loading state active", () => {
const state = { loadingStates: [] };
expect( getters.isLoading( state )).toBe( false );
state.loadingStates.push( "foo" );
expect( getters.isLoading( state )).toBe( true );
});
});
describe( "mutations", () => {
it( "should be able to toggle the opened state of the menu", () => {
const state = { menuOpened: false };
@@ -57,6 +66,20 @@ describe( "Vuex store", () => {
expect( state.panMode ).toBe( true );
});
describe( "when toggling loading states", () => {
it( "should be able to register a new loading state", () => {
const state = { loadingStates: [ "foo" ] };
mutations.setLoading( state, "bar" );
expect( state.loadingStates ).toEqual([ "foo", "bar" ]);
});
it( "should be able to unregister an existing loading state", () => {
const state = { loadingStates: [ "foo", "bar" ] };
mutations.unsetLoading( state, "foo" );
expect( state.loadingStates ).toEqual([ "bar" ]);
});
});
describe( "when toggling dialog windows", () => {
it( "should be able to open a dialog window and apply its request parameters", () => {
const state = { dialog: null };