Skip to content

Commit

Permalink
pkp/pkp-lib#10663 Spinners for dashboard&workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
jardakotesovec committed Jan 4, 2025
1 parent cd9dcf6 commit 72c3280
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 12 deletions.
37 changes: 33 additions & 4 deletions src/components/Spinner/Spinner.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
<template>
<span class="pkpSpinner" aria-hidden="true" />
<span
class="pkpSpinner"
:class="{'pkpSpinner--big': sizeVariant === 'big'}"
aria-hidden="true"
>
<span v-if="message" class="ms-3 text-base-normal text-disabled">
{{ message }}
</span>
</span>
</template>

<script setup>
defineProps({
message: {required: false, type: String, default: null},
sizeVariant: {
required: false,
type: String,
default: () => 'default',
validator: (prop) => ['default', 'big'].includes(prop),
},
});
</script>
<style lang="less">
@import '../../styles/_import';
Expand All @@ -14,15 +32,26 @@
vertical-align: middle;
animation: pkp_anim_spinner 0.6s linear infinite;
border-radius: 100%;
border-top: 1px solid @bg-anchor;
border-top: 1px solid @primary;
border-bottom: 1px solid transparent;
border-left: 1px solid @bg-anchor;
border-left: 1px solid @primary;
border-right: 1px solid transparent;
content: '';
opacity: 1;
}
}
.pkpSpinner--big {
&:before {
width: 2rem;
height: 2rem;
border-top: 3px solid @primary;
border-bottom: 3px solid transparent;
border-left: 3px solid @primary;
border-right: 3px solid transparent;
}
}
// Animation
@keyframes pkp_anim_spinner {
0% {
Expand Down
15 changes: 15 additions & 0 deletions src/components/Spinner/SpinnerFullScreen.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template>
<div
v-if="progressStore.isFullScreenSpinner"
class="fixed inset-0 z-50 flex items-center justify-center bg-default bg-opacity-75"
>
<Spinner size-variant="big" />
</div>
</template>

<script setup>
import Spinner from '@/components/Spinner/Spinner.vue';
import {useProgressStore} from '@/stores/progressStore';
const progressStore = useProgressStore();
</script>
21 changes: 19 additions & 2 deletions src/composables/useFetch.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {ref, unref} from 'vue';
import {ref, unref, inject} from 'vue';
import {ofetch, createFetch} from 'ofetch';
import {useModalStore} from '@/stores/modalStore';
import {useProgressStore} from '@/stores/progressStore';

import {useDebounceFn} from '@vueuse/core';

let ofetchInstance = ofetch;
Expand All @@ -15,6 +17,7 @@ export function getCSRFToken() {

return FALLBACK_TOKEN;
}

/**
*
* Composable for handling API requests
Expand All @@ -27,7 +30,8 @@ export function getCSRFToken() {
* @param {Object} [options.body] - The request payload, typically used with 'POST', 'PUT', or 'DELETE' requests.
* @param {Object} [options.headers] - Additional HTTP headers to be sent with the request.
* @param {string} [options.method] - The HTTP method to be used for the request (e.g., 'GET', 'POST', etc.).
* @param {number} options.debouncedMs - When the fetch should be debounce, this defines the delay
* @param {number} [options.debouncedMs] - When the fetch should be debounce, this defines the delay
* @param {boolean} [options.showFullScreenSpinner] - Automatically shows full screen spinner, when set to true
* @returns {Object} An object containing several reactive properties and a method for performing the fetch operation:
* @returns {Ref<Object|null>} return.data - A ref object containing the response data from the fetch operation.
Expand Down Expand Up @@ -64,6 +68,10 @@ export function useFetch(url, options = {}) {

let lastRequestController = null;

const modalLevel = inject('modalLevel');
const screenName = modalLevel?.value ? `modal_${modalLevel.value}` : 'base';
const progressStore = useProgressStore();

async function _fetch() {
if (lastRequestController) {
// abort in-flight request
Expand Down Expand Up @@ -95,6 +103,11 @@ export function useFetch(url, options = {}) {
}

isLoading.value = true;
progressStore.fetchStarted(screenName);
if (opts.showFullScreenSpinner) {
progressStore.startFullScreenSpinner();
}

isSuccess.value = null;
try {
const result = await ofetchInstance(unref(url), opts);
Expand Down Expand Up @@ -122,6 +135,10 @@ export function useFetch(url, options = {}) {
} finally {
lastRequestController = null;
isLoading.value = false;
progressStore.fetchFinished(screenName);
if (opts.showFullScreenSpinner) {
progressStore.stopFullScreenSpinner();
}
}
}

Expand Down
20 changes: 15 additions & 5 deletions src/pages/dashboard/DashboardPage.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
<template>
<div class="min-h-screentext-base-normal me-3 ms-5 text-base-normal">
<div class="">
<h1 class="flex items-center gap-4 py-6 text-5xl-bold">
{{
`${store.currentView.name} (${store.submissionsPagination.itemCount})`
}}
</h1>
<span>
<h1 class="flex-inline items-center gap-4 py-6 text-5xl-bold">
{{
`${store.currentView.name} (${store.submissionsPagination.itemCount})`
}}
<span class="ms-3">
<Spinner
size-variant="big"
:class="!store.isSubmissionsLoading ? 'invisible' : ''"
/>
</span>
</h1>
</span>

<div class="mt-2">
<div class="flex justify-between">
<div class="flex flex-row items-center space-x-3">
Expand Down Expand Up @@ -56,6 +65,7 @@ import DashboardBulkDeleteButton from './components/DashboardBulkDeleteButton.vu
import Search from '@/components/Search/Search.vue';
import {useDashboardPageStore} from './dashboardPageStore';
import Spinner from '@/components/Spinner/Spinner.vue';
const props = defineProps({
dashboardPage: {
Expand Down
15 changes: 14 additions & 1 deletion src/pages/workflow/WorkflowPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
<SideModalBody>
<template #pre-title>
{{ workflowStore.submissionId }}
<span class="ms-3">
<Spinner
size-variant="big"
:message="t('common.refreshingData')"
:class="
progressStore.screensInProgress.includes('modal_1')
? ''
: 'invisible'
"
/>
</span>
</template>
<template #title>
<span v-if="selectedPublication" class="underline">
Expand Down Expand Up @@ -134,14 +145,16 @@ import {storeToRefs} from 'pinia';
import SideMenu from '@/components/SideMenu/SideMenu.vue';
import SideModalBody from '@/components/Modal/SideModalBody.vue';
import StageBubble from '@/components/StageBubble/StageBubble.vue';
import Spinner from '@/components/Spinner/Spinner.vue';
import {useWorkflowStore} from './workflowStore';
import {useProgressStore} from '@/stores/progressStore';
import SideModalLayoutMenu2Columns from '@/components/Modal/SideModalLayoutMenu2Columns.vue';
import {useLocalize} from '@/composables/useLocalize';
const {localizeSubmission} = useLocalize();
const workflowStore = useWorkflowStore();
const progressStore = useProgressStore();
const {submission, selectedPublication} = storeToRefs(workflowStore);
</script>
2 changes: 2 additions & 0 deletions src/pages/workflow/composables/useWorkflowActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useUrl} from '@/composables/useUrl';
import {useForm} from '@/composables/useForm';
import {useFetch} from '@/composables/useFetch';
import {useLegacyGridUrl} from '@/composables/useLegacyGridUrl';

import WorkflowModalChangeSubmissionLanguage from '@/pages/workflow/modals/WorkflowChangeSubmissionLanguageModal.vue';

import WorkflowSelectRevisionFormModal from '@/pages/workflow/modals/WorkflowSelectRevisionFormModal.vue';
Expand Down Expand Up @@ -284,6 +285,7 @@ export function useWorkflowActions({
createNewVersionUrl,
{
method: 'POST',
showFullScreenSpinner: true,
},
);
await fetch();
Expand Down
62 changes: 62 additions & 0 deletions src/stores/progressStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {defineStore} from 'pinia';
import {ref, computed} from 'vue';

export const useProgressStore = defineStore('progress', () => {
/**
* Background spinner currently used on workflow page
* Detects all fetch activity on current screen
* */
const screenFetchesInProgress = ref({});
const screensInProgress = computed(() => {
const screens = [];
Object.keys(screenFetchesInProgress.value).forEach((key) => {
if (screenFetchesInProgress.value[key] > 0) {
screens.push(key);
}
});

return screens;
});

function fetchStarted(screenName = 'base') {
if (
Object.prototype.hasOwnProperty.call(
screenFetchesInProgress.value,
screenName,
)
) {
screenFetchesInProgress.value[screenName] = 0;
}

screenFetchesInProgress.value[screenName]++;
}

function fetchFinished(screenName = 'base') {
screenFetchesInProgress.value[screenName]--;
}

/**
* Fullscreen spinner
*/

const isFullScreenSpinner = ref(false);
function startFullScreenSpinner() {
isFullScreenSpinner.value = true;
}
function stopFullScreenSpinner() {
isFullScreenSpinner.value = false;
}

return {
fetchStarted,
fetchFinished,
screensInProgress,

/**
* Full Screen spinner
*/
isFullScreenSpinner,
startFullScreenSpinner,
stopFullScreenSpinner,
};
});

0 comments on commit 72c3280

Please sign in to comment.