From ebd6a2eed2889adda55bbc4ce3b0e861946c237f Mon Sep 17 00:00:00 2001 From: Josh Traill <josh.traill@quartech.com> Date: Mon, 13 Jan 2025 10:59:08 -0800 Subject: [PATCH 1/4] feat: add in snack bar exceptions feat: snackbar store feat: raise snacks globally --- web/src/components/shared/Snackbar.vue | 23 +++++++++++++ web/src/services/HttpService.ts | 10 ++++++ web/src/stores/SnackbarStore.ts | 18 ++++++++++ web/src/stores/index.ts | 1 + web/tests/components/shared/Snackbar.test.ts | 31 +++++++++++++++++ web/tests/stores/SnackbarStore.test.ts | 35 ++++++++++++++++++++ 6 files changed, 118 insertions(+) create mode 100644 web/src/components/shared/Snackbar.vue create mode 100644 web/src/stores/SnackbarStore.ts create mode 100644 web/tests/components/shared/Snackbar.test.ts create mode 100644 web/tests/stores/SnackbarStore.test.ts diff --git a/web/src/components/shared/Snackbar.vue b/web/src/components/shared/Snackbar.vue new file mode 100644 index 00000000..d2cac715 --- /dev/null +++ b/web/src/components/shared/Snackbar.vue @@ -0,0 +1,23 @@ +<template> + <v-snackbar + v-model="snackbarStore.isVisible" + :timeout="-1" + :color="snackbarStore.color" + location="bottom right" + > + <h3>{{ snackbarStore.title }}</h3> + <span>{{ snackbarStore.message }}</span> + <template v-slot:actions> + <v-icon class="mx-2" :icon="mdiCloseCircle" @click="close" /> + </template> + </v-snackbar> +</template> + +<script setup> + import { useSnackbarStore } from '@/stores/SnackBarStore'; + import { mdiCloseCircle } from '@mdi/js'; + const snackbarStore = useSnackbarStore(); + const close = () => { + snackbarStore.isVisible = false; + }; +</script> \ No newline at end of file diff --git a/web/src/services/HttpService.ts b/web/src/services/HttpService.ts index 03a45c18..98b883d9 100644 --- a/web/src/services/HttpService.ts +++ b/web/src/services/HttpService.ts @@ -4,6 +4,7 @@ import axios, { InternalAxiosRequestConfig, } from 'axios'; import redirectHandlerService from './RedirectHandlerService'; +import { useSnackbarStore } from '@/stores/SnackBarStore'; export interface IHttpService { get<T>(resource: string, queryParams?: Record<string, any>): Promise<T>; @@ -17,6 +18,7 @@ export interface IHttpService { export class HttpService implements IHttpService { readonly client: AxiosInstance; + snackBarStore = useSnackbarStore(); constructor(baseURL: string) { this.client = axios.create({ @@ -47,8 +49,16 @@ export class HttpService implements IHttpService { private handleAuthError(error: any) { console.error(error); console.log('User unauthenticated.'); + // todo: check for a 403 and handle it if (error.response && error.response.status === 401) { redirectHandlerService.handleUnauthorized(window.location.href); + } else { + // The user should be notified about unhandled server exceptions. + this.snackBarStore.showSnackbar( + 'Something went wrong, please contact your Administrator.', + '#b84157', + 'Error' + ); } return Promise.reject(new Error(error)); } diff --git a/web/src/stores/SnackbarStore.ts b/web/src/stores/SnackbarStore.ts new file mode 100644 index 00000000..820262f5 --- /dev/null +++ b/web/src/stores/SnackbarStore.ts @@ -0,0 +1,18 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export const useSnackbarStore = defineStore('snackbar', () => { + const isVisible = ref(false); + const message = ref(''); + const color = ref('success'); + const title = ref(''); + + const showSnackbar = (msg = '', col = 'success', ti = '') => { + message.value = msg; + color.value = col; + title.value = ti; + isVisible.value = true; + }; + + return { isVisible, message, color, showSnackbar, title }; +}); \ No newline at end of file diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts index 79dd7b44..4d11ce15 100644 --- a/web/src/stores/index.ts +++ b/web/src/stores/index.ts @@ -14,3 +14,4 @@ export { useCommonStore } from './CommonStore'; export { useCourtFileSearchStore } from './CourtFileSearchStore'; export { useCourtListStore } from './CourtListStore'; export { useCriminalFileStore } from './CriminalFileStore'; +export { useSnackbarStore } from './SnackBarStore'; \ No newline at end of file diff --git a/web/tests/components/shared/Snackbar.test.ts b/web/tests/components/shared/Snackbar.test.ts new file mode 100644 index 00000000..6c8e14b1 --- /dev/null +++ b/web/tests/components/shared/Snackbar.test.ts @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils'; +import { useSnackbarStore } from '@/stores/SnackBarStore'; +import { describe, it, expect, beforeEach } from 'vitest'; +import Snackbar from '@/components/shared/Snackbar.vue'; +import { setActivePinia, createPinia } from 'pinia' + +describe('Snackbar.vue', () => { + let store: ReturnType<typeof useSnackbarStore>; + + beforeEach(() => { + setActivePinia(createPinia()); + store = useSnackbarStore(); + }); + + it('renders snackbar with correct props', () => { + store.showSnackbar('Test message', 'error', 'Test title'); + const wrapper = mount(Snackbar); + + expect(wrapper.find('h3').text()).toBe('Test title'); + expect(wrapper.text()).toContain('Test message'); + expect(store.isVisible).toBe(true); + }); + + it('closes snackbar when close button is clicked', async () => { + const wrapper = mount(Snackbar); + + await wrapper.find('v-snackbar__actions v-icon').trigger('click'); + + expect(store.isVisible).toBe(false); + }); +}); \ No newline at end of file diff --git a/web/tests/stores/SnackbarStore.test.ts b/web/tests/stores/SnackbarStore.test.ts new file mode 100644 index 00000000..fe3588f7 --- /dev/null +++ b/web/tests/stores/SnackbarStore.test.ts @@ -0,0 +1,35 @@ +import { setActivePinia, createPinia } from 'pinia' +import { useSnackbarStore } from '@/stores/SnackBarStore'; +import { beforeEach, describe, expect, it } from 'vitest'; + +describe('SnackBarStore', () => { + let store: ReturnType<typeof useSnackbarStore>; + + beforeEach(() => { + setActivePinia(createPinia()) + store = useSnackbarStore(); + }); + + it('initializes with default values', () => { + expect(store.isVisible).toBe(false); + expect(store.message).toBe(''); + expect(store.color).toBe('success'); + expect(store.title).toBe(''); + }); + + it('shows snackbar with given message, color, and title', () => { + store.showSnackbar('Test message', 'error', 'Test title'); + expect(store.isVisible).toBe(true); + expect(store.message).toBe('Test message'); + expect(store.color).toBe('error'); + expect(store.title).toBe('Test title'); + }); + + it('shows snackbar with default values when no arguments are passed', () => { + store.showSnackbar(); + expect(store.isVisible).toBe(true); + expect(store.message).toBe(''); + expect(store.color).toBe('success'); + expect(store.title).toBe(''); + }); +}); \ No newline at end of file From f521887e4d542b972df6baed393683265dfbe695 Mon Sep 17 00:00:00 2001 From: Josh Traill <josh.traill@quartech.com> Date: Mon, 13 Jan 2025 11:22:01 -0800 Subject: [PATCH 2/4] fix: incorrect casing --- web/src/App.vue | 2 ++ web/src/components/shared/Snackbar.vue | 2 +- web/src/services/HttpService.ts | 2 +- web/src/stores/index.ts | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/src/App.vue b/web/src/App.vue index 35f78f7f..4cad85dc 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -26,6 +26,7 @@ <v-main> <router-view /> </v-main> + <snackbar /> </v-app> </v-theme-provider> </template> @@ -34,6 +35,7 @@ import { mdiAccountCircle } from '@mdi/js'; import { ref } from 'vue'; import ProfileOffCanvas from './components/shared/ProfileOffCanvas.vue'; + import Snackbar from './components/shared/Snackbar.vue'; import { useThemeStore } from './stores/ThemeStore'; const themeStore = useThemeStore(); diff --git a/web/src/components/shared/Snackbar.vue b/web/src/components/shared/Snackbar.vue index d2cac715..13c0b7bc 100644 --- a/web/src/components/shared/Snackbar.vue +++ b/web/src/components/shared/Snackbar.vue @@ -14,7 +14,7 @@ </template> <script setup> - import { useSnackbarStore } from '@/stores/SnackBarStore'; + import { useSnackbarStore } from '@/stores/SnackbarStore'; import { mdiCloseCircle } from '@mdi/js'; const snackbarStore = useSnackbarStore(); const close = () => { diff --git a/web/src/services/HttpService.ts b/web/src/services/HttpService.ts index 98b883d9..e000498b 100644 --- a/web/src/services/HttpService.ts +++ b/web/src/services/HttpService.ts @@ -1,10 +1,10 @@ +import { useSnackbarStore } from '@/stores/SnackbarStore'; import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig, } from 'axios'; import redirectHandlerService from './RedirectHandlerService'; -import { useSnackbarStore } from '@/stores/SnackBarStore'; export interface IHttpService { get<T>(resource: string, queryParams?: Record<string, any>): Promise<T>; diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts index 4d11ce15..c6c90970 100644 --- a/web/src/stores/index.ts +++ b/web/src/stores/index.ts @@ -14,4 +14,4 @@ export { useCommonStore } from './CommonStore'; export { useCourtFileSearchStore } from './CourtFileSearchStore'; export { useCourtListStore } from './CourtListStore'; export { useCriminalFileStore } from './CriminalFileStore'; -export { useSnackbarStore } from './SnackBarStore'; \ No newline at end of file +export { useSnackbarStore } from './SnackbarStore'; \ No newline at end of file From 224897f9a06b4fc251f43bc37d7abe6e717bba8d Mon Sep 17 00:00:00 2001 From: Josh Traill <josh.traill@quartech.com> Date: Mon, 13 Jan 2025 11:25:35 -0800 Subject: [PATCH 3/4] test: fix casing --- web/tests/components/shared/Snackbar.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/components/shared/Snackbar.test.ts b/web/tests/components/shared/Snackbar.test.ts index 6c8e14b1..13edb7f7 100644 --- a/web/tests/components/shared/Snackbar.test.ts +++ b/web/tests/components/shared/Snackbar.test.ts @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { useSnackbarStore } from '@/stores/SnackBarStore'; +import { useSnackbarStore } from '@/stores/SnackbarStore'; import { describe, it, expect, beforeEach } from 'vitest'; import Snackbar from '@/components/shared/Snackbar.vue'; import { setActivePinia, createPinia } from 'pinia' From 154a98439f25cf1e05a7288ad60082a225fbcd91 Mon Sep 17 00:00:00 2001 From: Josh Traill <josh.traill@quartech.com> Date: Mon, 13 Jan 2025 11:26:18 -0800 Subject: [PATCH 4/4] test: fix casing --- web/tests/stores/SnackbarStore.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/stores/SnackbarStore.test.ts b/web/tests/stores/SnackbarStore.test.ts index fe3588f7..67ee2707 100644 --- a/web/tests/stores/SnackbarStore.test.ts +++ b/web/tests/stores/SnackbarStore.test.ts @@ -1,5 +1,5 @@ import { setActivePinia, createPinia } from 'pinia' -import { useSnackbarStore } from '@/stores/SnackBarStore'; +import { useSnackbarStore } from '@/stores/SnackbarStore'; import { beforeEach, describe, expect, it } from 'vitest'; describe('SnackBarStore', () => {