Merge pull request #19 from AlexisNP/bugfix/event-operations-should-have-a-loading
Bugfix/event operations should have a loading
This commit is contained in:
@@ -5,64 +5,64 @@
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
:root.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
||||
@@ -73,4 +73,21 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
.fade-delay-enter-active,
|
||||
.fade-delay-leave-active {
|
||||
transition: all .5s ease 1s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to,
|
||||
.fade-delay-enter-from,
|
||||
.fade-delay-leave-to {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ onMounted(() => {
|
||||
|
||||
setCurrentMenu([
|
||||
{
|
||||
phIcon: PhMagnifyingGlass,
|
||||
phIcon: shallowRef(PhMagnifyingGlass),
|
||||
tooltip: 'Recherche avancée',
|
||||
action: 'event-search'
|
||||
}
|
||||
@@ -116,10 +116,10 @@ onMounted(() => {
|
||||
<component :is="currentViewComponent"/>
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<LazyCalendarSearch />
|
||||
<LazyCalendarFormUpdateEvent />
|
||||
<LazyCalendarFormDeleteEvent />
|
||||
<LazyCalendarSearch />
|
||||
<LazyCalendarFormUpdateEvent />
|
||||
<LazyCalendarFormDeleteEvent />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { RPGDate } from '~/models/Date';
|
||||
|
||||
import { PhAlarm, PhMapPinArea } from '@phosphor-icons/vue'
|
||||
import { PhAlarm, PhCircleNotch, PhMapPinArea } from '@phosphor-icons/vue'
|
||||
|
||||
const { eventSkeleton } = storeToRefs(useCalendarEvents())
|
||||
const { resetSkeleton, submitSkeleton } = useCalendarEvents()
|
||||
const { eventSkeleton, operationInProgress } = storeToRefs(useCalendarEvents())
|
||||
const { resetSkeleton, submitSkeleton, cancelLatestRequest } = useCalendarEvents()
|
||||
const popoverOpen = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
@@ -20,6 +21,12 @@ const props = defineProps<{
|
||||
* Opens event creation's popover
|
||||
*/
|
||||
function openEventCreatePopover() {
|
||||
// If another operation is in progress, whether it's another create popup or a modal, don't bother opening it
|
||||
if (operationInProgress.value) {
|
||||
popoverOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
resetSkeleton()
|
||||
|
||||
popoverOpen.value = true
|
||||
@@ -31,6 +38,11 @@ function openEventCreatePopover() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// Prevent form submission if already loading
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await submitSkeleton()
|
||||
|
||||
@@ -39,8 +51,31 @@ async function handleSubmit() {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel() {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -52,7 +87,13 @@ async function handleSubmit() {
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
@@ -118,8 +159,18 @@ async function handleSubmit() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<UiButton size="sm">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
Annuler
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="opacity-50 animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
Sauvegarder
|
||||
</UiButton>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from '@phosphor-icons/vue';
|
||||
|
||||
const { isDeleteEventModalOpen } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const { resetSkeleton, deleteEventFromSkeleton } = useCalendarEvents()
|
||||
const { resetSkeleton, deleteEventFromSkeleton, cancelLatestRequest } = useCalendarEvents()
|
||||
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
@@ -16,6 +20,10 @@ watch(isDeleteEventModalOpen, (hasOpened, _o) => {
|
||||
})
|
||||
|
||||
async function handleAction() {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await deleteEventFromSkeleton()
|
||||
|
||||
@@ -26,19 +34,42 @@ async function handleAction() {
|
||||
}
|
||||
} finally {
|
||||
resetSkeleton()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel() {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog v-model:open="isDeleteEventModalOpen">
|
||||
<UiAlertDialogContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 bg-slate-900 border-slate-800"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="handleClosing"
|
||||
>
|
||||
<UiAlertDialogTitle> Supprimer l'évènement</UiAlertDialogTitle>
|
||||
|
||||
@@ -46,7 +77,7 @@ async function handleAction() {
|
||||
Les données associés à cet évènement seront supprimées et vous ne pourrez plus les récupérer !
|
||||
</UiAlertDialogDescription>
|
||||
|
||||
<form>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
@@ -54,16 +85,23 @@ async function handleAction() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<UiAlertDialogFooter>
|
||||
<UiAlertDialogCancel>
|
||||
Annuler
|
||||
</UiAlertDialogCancel>
|
||||
<UiAlertDialogAction class="destructive" @click="handleAction">
|
||||
Supprimer
|
||||
</UiAlertDialogAction>
|
||||
</UiAlertDialogFooter>
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
Annuler
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
Supprimer
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhAlarm, PhMapPinArea } from '@phosphor-icons/vue'
|
||||
import { PhAlarm, PhCircleNotch, PhMapPinArea } from '@phosphor-icons/vue'
|
||||
import { VisuallyHidden } from 'radix-vue'
|
||||
|
||||
const { isEditEventModalOpen } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const { resetSkeleton, updateEventFromSkeleton } = useCalendarEvents()
|
||||
const { resetSkeleton, updateEventFromSkeleton, cancelLatestRequest } = useCalendarEvents()
|
||||
const { eventSkeleton, lastActiveEvent } = storeToRefs(useCalendarEvents())
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
@@ -19,6 +21,10 @@ watch(isEditEventModalOpen, (hasOpened, _o) => {
|
||||
})
|
||||
|
||||
async function handleAction() {
|
||||
if (isLoading.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await updateEventFromSkeleton()
|
||||
|
||||
@@ -29,29 +35,52 @@ async function handleAction() {
|
||||
}
|
||||
} finally {
|
||||
resetSkeleton()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents the modal from closing if's still loading
|
||||
*
|
||||
* @param e The closing event (can be keydown or click)
|
||||
*/
|
||||
function handleClosing(e: Event) {
|
||||
if (isLoading.value) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel() {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiAlertDialog v-model:open="isEditEventModalOpen">
|
||||
<UiAlertDialogContent
|
||||
:align="'center'"
|
||||
:side="'right'"
|
||||
:collision-padding="60"
|
||||
<UiDialog v-model:open="isEditEventModalOpen">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="pl-3 min-w-96 bg-slate-900 border-slate-800"
|
||||
@escape-key-down="handleClosing"
|
||||
@focus-outside="handleClosing"
|
||||
@interact-outside="handleClosing"
|
||||
@pointer-down-outside="(e) => e.preventDefault()"
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<UiAlertDialogTitle> Modifier l'évènement</UiAlertDialogTitle>
|
||||
<UiDialogTitle> Modifier l'évènement</UiDialogTitle>
|
||||
|
||||
<UiAlertDialogDescription>
|
||||
<UiDialogDescription>
|
||||
Mettre à jour les données de l'évènement
|
||||
</UiAlertDialogDescription>
|
||||
</UiDialogDescription>
|
||||
</VisuallyHidden>
|
||||
|
||||
<form>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="col-span-2 ml-8">
|
||||
<input
|
||||
@@ -115,15 +144,23 @@ async function handleAction() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
Annuler
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
Enregistrer
|
||||
</UiButton>
|
||||
</footer>
|
||||
</form>
|
||||
<UiAlertDialogFooter>
|
||||
<UiAlertDialogCancel>
|
||||
Annuler
|
||||
</UiAlertDialogCancel>
|
||||
<UiAlertDialogAction @click="handleAction">
|
||||
Sauvegarder
|
||||
</UiAlertDialogAction>
|
||||
</UiAlertDialogFooter>
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
|
||||
@@ -146,7 +146,7 @@ const eventsNotDisplayed: ComputedRef<number> = computed<number>(() => eventsFo
|
||||
</ClientOnly>
|
||||
|
||||
<ClientOnly>
|
||||
<CalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
<LazyCalendarFormCreateEvent :date btn-class="absolute inset-0 w-full h-full cursor-default z-0" />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ShallowRef } from "vue"
|
||||
|
||||
export type SidebarMenuActionType = "event-search"
|
||||
|
||||
export interface SidebarMenuItem {
|
||||
phIcon: Component
|
||||
phIcon: ShallowRef
|
||||
tooltip: string
|
||||
action?: SidebarMenuActionType
|
||||
to?: string
|
||||
|
||||
@@ -187,6 +187,12 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
||||
* EVENT CREATION FUNCTIONS
|
||||
*/
|
||||
const lastActiveEvent = ref<CalendarEvent | null>()
|
||||
const isCreatingEvent = ref<boolean>(false)
|
||||
const isUpdatingEvent = ref<boolean>(false)
|
||||
const isDeletingEvent = ref<boolean>(false)
|
||||
const operationInProgress = computed(() => isCreatingEvent.value || isUpdatingEvent.value || isDeletingEvent.value)
|
||||
let abortController: AbortController | null = null
|
||||
|
||||
/**
|
||||
* Dummy event to hold creation data
|
||||
*/
|
||||
@@ -205,31 +211,58 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
||||
* We assume it's been sanitized by the caller
|
||||
*/
|
||||
async function submitSkeleton() {
|
||||
abortController = new AbortController()
|
||||
isCreatingEvent.value = true
|
||||
|
||||
try {
|
||||
const res = await $fetch('/api/calendars/events/create', { method: 'POST', body: { event : eventSkeleton.value, calendarId: calendarId.value }})
|
||||
const res = await $fetch('/api/calendars/events/create', { method: 'POST', body: { event : eventSkeleton.value, calendarId: calendarId.value }, signal: abortController.signal })
|
||||
|
||||
baseEvents.value.push(res)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
abortController = null
|
||||
isCreatingEvent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateEventFromSkeleton() {
|
||||
abortController = new AbortController()
|
||||
isUpdatingEvent.value = true
|
||||
|
||||
try {
|
||||
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: 'PATCH', body: { event : eventSkeleton.value, calendarId: calendarId.value }})
|
||||
const res = await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: 'PATCH', body: { event : eventSkeleton.value, calendarId: calendarId.value }, signal: abortController.signal })
|
||||
|
||||
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
||||
baseEvents.value[eventIndex] = res
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
abortController = null
|
||||
isUpdatingEvent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEventFromSkeleton() {
|
||||
abortController = new AbortController()
|
||||
isDeletingEvent.value = true
|
||||
|
||||
try {
|
||||
await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: 'DELETE' })
|
||||
await $fetch(`/api/calendars/events/${eventSkeleton.value.id}`, { method: 'DELETE', signal: abortController.signal })
|
||||
|
||||
const eventIndex = baseEvents.value.findIndex(e => e.id === eventSkeleton.value.id)
|
||||
baseEvents.value.splice(eventIndex, 1)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
abortController = null
|
||||
isDeletingEvent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelLatestRequest() {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +272,11 @@ export const useCalendarEvents = defineStore('calendar-events', () => {
|
||||
currentEvents,
|
||||
getRelativeEventFromDate,
|
||||
getRelativeEventFromEvent,
|
||||
cancelLatestRequest,
|
||||
isCreatingEvent,
|
||||
isUpdatingEvent,
|
||||
isDeletingEvent,
|
||||
operationInProgress,
|
||||
eventSkeleton,
|
||||
resetSkeleton,
|
||||
submitSkeleton,
|
||||
|
||||
Reference in New Issue
Block a user