From 1a82a909c5c4d0262d69a8a543e902ff6533a4b2 Mon Sep 17 00:00:00 2001 From: vnugent Date: Mon, 1 Jan 2024 10:56:02 -0500 Subject: swallow vnlib.browser --- lib/vnlib.browser/src/helpers/apiCall.ts | 276 +++++++++++++++++++++ lib/vnlib.browser/src/helpers/autoHeartbeat.ts | 63 +++++ lib/vnlib.browser/src/helpers/confirm.ts | 30 +++ lib/vnlib.browser/src/helpers/envSize.ts | 49 ++++ lib/vnlib.browser/src/helpers/lastPage.ts | 109 ++++++++ lib/vnlib.browser/src/helpers/message.ts | 56 +++++ lib/vnlib.browser/src/helpers/pageGuard.ts | 49 ++++ .../src/helpers/serverObjectBuffer.ts | 149 +++++++++++ lib/vnlib.browser/src/helpers/validation.ts | 113 +++++++++ lib/vnlib.browser/src/helpers/wait.ts | 55 ++++ 10 files changed, 949 insertions(+) create mode 100644 lib/vnlib.browser/src/helpers/apiCall.ts create mode 100644 lib/vnlib.browser/src/helpers/autoHeartbeat.ts create mode 100644 lib/vnlib.browser/src/helpers/confirm.ts create mode 100644 lib/vnlib.browser/src/helpers/envSize.ts create mode 100644 lib/vnlib.browser/src/helpers/lastPage.ts create mode 100644 lib/vnlib.browser/src/helpers/message.ts create mode 100644 lib/vnlib.browser/src/helpers/pageGuard.ts create mode 100644 lib/vnlib.browser/src/helpers/serverObjectBuffer.ts create mode 100644 lib/vnlib.browser/src/helpers/validation.ts create mode 100644 lib/vnlib.browser/src/helpers/wait.ts (limited to 'lib/vnlib.browser/src/helpers') diff --git a/lib/vnlib.browser/src/helpers/apiCall.ts b/lib/vnlib.browser/src/helpers/apiCall.ts new file mode 100644 index 0000000..b98fecb --- /dev/null +++ b/lib/vnlib.browser/src/helpers/apiCall.ts @@ -0,0 +1,276 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { type MaybeRef, type Ref } from 'vue' +import { AxiosError, type Axios } from 'axios'; +import { defaultTo, isArray, isNil, isEqual } from 'lodash-es'; +import { get, useConfirmDialog } from '@vueuse/core'; +import { useFormToaster, useToaster } from '../toast'; +import { useWait } from './wait'; +import { useAxiosInternal } from '../axios'; +import type { IErrorNotifier, CombinedToaster } from '../toast'; + +export interface IApiHandle { + /** + * Called to get the object to pass to apiCall is invoked + */ + getCallbackObject(): T; + + /** + * Called to get the notifier to use for the api call + */ + getNotifier(): IErrorNotifier; + + /** + * Called to set the waiting flag + */ + setWaiting: (waiting: boolean) => void; +} + +export interface IApiPassThrough { + readonly axios: Axios; + readonly toaster: CombinedToaster; +} + +export interface IElevatedCallPassThrough extends IApiPassThrough { + readonly password: string; +} + +export interface ApiCall { + (callback: (data: T) => Promise): Promise; +} + +export type CustomMessageHandler = (message: string) => void; + +const useApiCallInternal = (args: IApiHandle): ApiCall => { + + /** + * Provides a wrapper method for making remote api calls to a server + * while capturing context and errors and common api arguments. + * @param {*} callback The method to call within api request context + * @returns A promise that resolves to the result of the async function + */ + return async (callback: (data: T) => Promise): Promise => { + const notifier = args.getNotifier(); + + // Set the waiting flag + args.setWaiting(true); + + try { + //Close the current toast value + notifier.close(); + + const obj = args.getCallbackObject(); + + //Exec the async function + return await callback(obj); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (errMsg: any) { + console.error(errMsg) + // See if the error has an axios response + if (isNil(errMsg.response)) { + if (errMsg.message === 'Network Error') { + notifier.notifyError('Please check your internet connection') + } else { + notifier.notifyError('An unknown error occured') + } + return + } + // Axios error message + const response = errMsg.response + const errors = response?.data?.errors + const hasErrors = isArray(errors) && errors.length > 0 + + const SetMessageWithDefault = (message: string) => { + if (hasErrors) { + const title = 'Please verify your ' + defaultTo(errors[0].property, 'form') + notifier.notifyError(title, errors[0].message) + } else { + notifier.notifyError(defaultTo(response?.data?.result, message)) + } + } + + switch (response.status) { + case 200: + SetMessageWithDefault('') + break + case 400: + SetMessageWithDefault('Bad Request') + break + case 422: + SetMessageWithDefault('The server did not accept the request') + break + case 401: + SetMessageWithDefault('You are not logged in.') + break + case 403: + SetMessageWithDefault('Please clear you cookies/cache and try again') + break + case 404: + SetMessageWithDefault('The requested resource was not found') + break + case 409: + SetMessageWithDefault('Please clear you cookies/cache and try again') + break + case 410: + SetMessageWithDefault('The requested resource has expired') + break + case 423: + SetMessageWithDefault('The requested resource is locked') + break + case 429: + SetMessageWithDefault('You have made too many requests, please try again later') + break + case 500: + SetMessageWithDefault('There was an error processing your request') + break + default: + SetMessageWithDefault('An unknown error occured') + break + } + } finally { + // Clear the waiting flag + args.setWaiting(false); + } + } +} + +const creatApiHandle = (notifier: MaybeRef, axios: Ref): IApiHandle => { + + const toaster = useToaster(); + const { setWaiting } = useWait(); + + const getCallbackObject = (): IApiPassThrough => ({ axios: get(axios), toaster }) + const getNotifier = (): IErrorNotifier => get(notifier); + + return { getCallbackObject, getNotifier, setWaiting } +} + +/** + * Provides a wrapper method for making remote api calls to a server + * while capturing context and errors and common api arguments. + * @param {*} asyncFunc The method to call within api request context + * @returns A promise that resolves to the result of the async function + */ +export const apiCall = (() =>{ + + const axios = useAxiosInternal(null); + const errorNotifier = useFormToaster(); + + //Create the api call handle + const handle = creatApiHandle(errorNotifier, axios); + //Confiugre the api call to use global configuration + return useApiCallInternal(handle); +})(); + +/** + * Customizes the api call to use a custom error message + * @param msg The message to display when an error occurs + * @returns {Object} The api call object {apiCall: Promise } + */ +export const configureApiCall = (msg: CustomMessageHandler): { apiCall: ApiCall } =>{ + + const notifier = ((): IErrorNotifier => { + return{ + notifyError: (t: string, m?: string) => { + msg(t); + return m; + }, + close(id: string) { + msg('') + return id; + }, + } + })() + + const axios = useAxiosInternal(null); + + //Create custom api handle + const handle = creatApiHandle(notifier, axios); + + //Confiugre the api call to use global configuration + const apiCall = useApiCallInternal(handle); + return { apiCall } +} + +/** + * Gets the shared password prompt object and the elevated api call method handler + * to allow for elevated api calls that require a password. + * @returns {Object} The password prompt configuration object, and the elevated api call method + */ +export const usePassConfirm = (() => { + + //Shared confirm object + const confirm = useConfirmDialog(); + + /** + * Displays the password prompt and executes the api call with the password + * captured from the prompt. If the api call returns a 401 error, the password + * prompt is re-displayed and the server error message is displayed in the form + * error toaster. + * @param callback The async callback method that invokes the elevated api call. + * @returns A promise that resolves to the result of the async function + */ + const elevatedApiCall = (callback: (api: IElevatedCallPassThrough) => Promise): Promise => { + //Invoke api call method but handle 401 errors by re-displaying the password prompt + return apiCall(async (api: IApiPassThrough) : Promise => { + // eslint-disable-next-line no-constant-condition + while (1) { + + //Display the password prompt + const { data, isCanceled } = await confirm.reveal() + + if (isCanceled) { + break; + } + + try { + //Execute the api call with prompt response + return await callback({...api, ...data }); + } + //Catch 401 errors and re-display the password prompt, otherwise throw the error + catch (err) { + if(!(err instanceof AxiosError)){ + throw err; + } + + const { response } = err; + + if(isNil(response)){ + throw err; + } + + //Check status code, if 401, re-display the password prompt + if (!isEqual(response?.status, 401)) { + throw err; + } + + //Display the error message + api.toaster.form.error({ title: response.data.result }); + + //Re-display the password prompt + } + } + }) + } + + //Pass through confirm object and elevated api call + return () => ({ ...confirm, elevatedApiCall }) +})(); \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/autoHeartbeat.ts b/lib/vnlib.browser/src/helpers/autoHeartbeat.ts new file mode 100644 index 0000000..459a33a --- /dev/null +++ b/lib/vnlib.browser/src/helpers/autoHeartbeat.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { isRef, type Ref } from "vue"; +import { useIntervalFn, syncRef, get, type MaybeRef } from "@vueuse/core"; +import { useUser } from "../user"; +import { useSession } from "../session"; + +export interface IAutoHeartbeatControls { + /** + * The current state of the heartbeat interval + */ + enabled: Ref; + /** + * Enables the hearbeat interval if configured + */ + enable: () => void; + /** + * Disables the heartbeat interval + */ + disable: () => void; +} + +/** +* Configures shared controls for the heartbeat interval +* @param enabled The a reactive value that may be used to enable/disable the heartbeat interval +*/ +export const useAutoHeartbeat = (interval: Readonly>, enabled: MaybeRef): IAutoHeartbeatControls =>{ + + //Get heartbeat method to invoke when the timer fires + const { heartbeat } = useUser(); + const { loggedIn } = useSession(); + + //Setup the automatic heartbeat interval + const { isActive, pause, resume } = useIntervalFn(() => loggedIn.value ? heartbeat() : null, interval); + + //Sync the enabled ref with the active state if it is a ref + if(isRef(enabled)){ + //only sync from caller value to ref + syncRef(enabled, isActive, { direction: 'ltr' }) + } + + //Update timer immediately + get(enabled) ? resume() : pause(); + + return { enabled:isActive, enable:resume, disable:pause } +} \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/confirm.ts b/lib/vnlib.browser/src/helpers/confirm.ts new file mode 100644 index 0000000..77b92e0 --- /dev/null +++ b/lib/vnlib.browser/src/helpers/confirm.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { useConfirmDialog } from "@vueuse/core" + +/** + * Gets the global confirm dialog interface for building application wide + * confirm dialog state + */ +export const useConfirm = (() => { + //Store a global confirm dialog singleton to share across components + const globalConfirm = useConfirmDialog() + return () => globalConfirm; +})() \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/envSize.ts b/lib/vnlib.browser/src/helpers/envSize.ts new file mode 100644 index 0000000..e078e5b --- /dev/null +++ b/lib/vnlib.browser/src/helpers/envSize.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { toSafeInteger } from 'lodash-es'; +import { ref, computed } from "vue" +import { useElementSize } from '@vueuse/core' + +/** + * Setups a common state for the environment size + */ +export const useEnvSize = (() => { + //Element refs for app to watch + const header = ref(null); + const footer = ref(null); + const content = ref(null); + + //Setup reactive element sizes + const headerSize = useElementSize(header); + const footerSize = useElementSize(footer); + const contentSize = useElementSize(content); + + const footerHeight = computed(() => toSafeInteger(footerSize.height.value)); + const headerHeight = computed(() => toSafeInteger(headerSize.height.value)); + const contentHeight = computed(() => toSafeInteger(contentSize.height.value - headerSize.height.value - footerSize.height.value)); + + + return (exportElements?: boolean) => { + + return exportElements ? + { footerHeight, headerHeight, contentHeight, header, footer, content } : + { footerHeight, headerHeight, contentHeight } + } +})(); \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/lastPage.ts b/lib/vnlib.browser/src/helpers/lastPage.ts new file mode 100644 index 0000000..8be2116 --- /dev/null +++ b/lib/vnlib.browser/src/helpers/lastPage.ts @@ -0,0 +1,109 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { defaultTo } from "lodash-es"; +import { useRouter } from "vue-router" + +export interface ILastPage{ + /** + * Pushes the current page into the last-page stack + */ + push(): void; + /** + * Stores the current page and navigates to the desired route + * @param route The route to navigate to + */ + pushAndNavigate(route: object): void; + /** + * Navigates to the last page if it exists + */ + gotoLastPage(): void; +} + +export interface ILastPageStorage { + + /** + * Pushes the current page into the last-page stack + */ + push(route: string): void; + + /** + * Pops the last page from the last-page stack storage + * @returns {any} The last page route object stored + */ + pop(): string | null; +} + +const storageKey = "lastPage"; + +//Storage impl +const defaultStack = (): ILastPageStorage => { + const storage = sessionStorage; + + const push = (route: string) => { + //Serialize the route data and store it + storage?.setItem(storageKey, route); + } + + const pop = (): string | null => { + //Get the route data and deserialize it + const route = storage?.getItem(storageKey); + if (route) { + storage?.removeItem(storageKey); + return route; + } + return null; + } + return { push, pop } +} + +/** + * Gets the configuration for the last page the user was on + * when the page guard was called. This is used to return to the + * last page after login. + * @returns { gotoLastPage: Function } + */ +export const useLastPage = (storage?: ILastPageStorage): ILastPage => { + + //fallback to default storage + const _storage = defaultTo(storage, defaultStack()); + + //Get the current router instance + const router = useRouter(); + + //Store the current page to the last page stack + const push = () => _storage.push(router.currentRoute.value.fullPath); + + const pushAndNavigate = (route: object) => { + //Store the current page to the last page stack + push(); + //Navigate to the desired route + router.push(route); + }; + + const gotoLastPage = () => { + //Get the last stored page and navigate to it + const lp = _storage.pop(); + if (lp) { + router.push(lp); + } + }; + + return { push, pushAndNavigate, gotoLastPage } +} \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/message.ts b/lib/vnlib.browser/src/helpers/message.ts new file mode 100644 index 0000000..3c79577 --- /dev/null +++ b/lib/vnlib.browser/src/helpers/message.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { defaultTo } from 'lodash-es'; +import { useFormToaster } from '../toast' +import type { IErrorNotifier } from "../toast"; + +/** + * The message handler interface + */ +export interface IMessageHandler { + setMessage(title: string, text?: string): void; + clearMessage(): void; + onInput(): void; +} + +/** + * Gets the message state subsystem for displaying errors + * to the user and clearing them + * @param notifier An optional notifier to use instead of the default + */ +export const useMessage = (notifier?: IErrorNotifier): IMessageHandler =>{ + + const _notifier = defaultTo(notifier, useFormToaster()) + + const clearMessage = (): void => _notifier.close() + + const setMessage = (title: string, text = ''): void => { + //Setting a null value will also clear the message + title ? _notifier.notifyError(title, text) : clearMessage(); + } + + const onInput = clearMessage; + + return { + setMessage, + clearMessage, + onInput + } +} \ No newline at end of file diff --git a/lib/vnlib.browser/src/helpers/pageGuard.ts b/lib/vnlib.browser/src/helpers/pageGuard.ts new file mode 100644 index 0000000..c3a767b --- /dev/null +++ b/lib/vnlib.browser/src/helpers/pageGuard.ts @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { watch } from 'vue' +import { useSession } from "../session"; +import { useLastPage } from './lastPage'; +import type { ILastPageStorage } from "./lastPage"; + + +/** + * When called, configures the component to + * only be visible when the user is logged in. If the user is + * not logged in, the user is redirected to the login page. + * @remarks Once called, if the user is logged-in changes will be + * watch to redirect if the user becomes logged out. +*/ +export const usePageGuard = (loginRoute = { name: 'Login' }, lpStorage?: ILastPageStorage): void => { + //Get the session state + const session = useSession(); + + //Get last page controller, fall back to default session storage stack + const { pushAndNavigate } = useLastPage(lpStorage) + + // Initial check for logged in to guard the page + if (!session.loggedIn.value) { + //Store last route and redirect to login + pushAndNavigate(loginRoute); + } + + // setup watcher on session login value + // If the login value changes to false, redirect to login page + watch(session.loggedIn, value => value === false ? pushAndNavigate(loginRoute) : null) +} diff --git a/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts b/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts new file mode 100644 index 0000000..5595ec7 --- /dev/null +++ b/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts @@ -0,0 +1,149 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { clone, isEqual, assign } from 'lodash-es' +import { reactive, computed, type UnwrapNestedRefs, type Ref } from 'vue' + +/** + * Represents a server object that has been buffered for editing + * and can be reverted to the original state, or new state data + * applied from the server + */ +export interface ServerObjectBuffer { + /** + * The data is a reactive clone of the initial data state. It is used to store the current state of the resource. + */ + readonly data: UnwrapNestedRefs; + /** + * The buffer is a reactive clone of the initial data state. It is used to track changes. + */ + readonly buffer: UnwrapNestedRefs; + /** + * A computed value that indicates if the buffer has been modified from the data + */ + readonly modified: Readonly>; + /** + * Applies the new server response data to the resource data and reverts the buffer to the resource data + * @param newData The new server response data to apply to the buffer + */ + apply(newData: T): void; + /** + * Reverts the buffer to the resource data + */ + revert(): void; +} + +export type UpdateCallback = (state: ServerObjectBuffer) => Promise; + +/** + * Represents a server object that has been buffered for editing + * and can be reverted to the original state or updated on the server + */ +export interface ServerDataBuffer extends ServerObjectBuffer{ + /** + * Pushes buffered changes to the server if configured + */ + update(): Promise; +} + +export interface UseDataBuffer { + /** + * Configures a helper type that represents a data buffer that reflects the state of a resource + * on the server. This is useful for editing forms, where the user may want to revert their + * changes, or reload them from the server after an edit; + * @param initialData The intiial data to wrap in the buffer + * @returns + */ + (initialData: T): ServerObjectBuffer; + /** + * Configures a helper type that represents a data buffer that reflects the state of a resource + * on the server. This is useful for editing forms, where the user may want to revert their + * changes, or reload them from the server after an edit; + * @param initialData + * @returns + */ + (initialData: T, onUpdate: UpdateCallback): ServerDataBuffer; +} + + +const createObjectBuffer = (initialData: T): ServerObjectBuffer => { + + /** + * The data is a reactive clone of the initial data state. It is used to store the current state of the resource. + */ + const data = reactive(clone(initialData || {})) + + /** + * The buffer is a reactive clone of the initial data state. It is used to track changes. + */ + const buffer = reactive(clone(initialData || {})) + + /** + * A computed value that indicates if the buffer has been modified from the data + * @returns {boolean} True if the buffer has been modified from the data + */ + const modified = computed(() => !isEqual(buffer, data)) + + /** + * Applies the new server response data to the resource data and reverts the buffer to the resource data + * @param newData The new server response data to apply to the buffer + */ + const apply = (newData: T): void => { + // Apply the new data to the buffer + assign(data, newData); + // Revert the buffer to the resource data + assign(buffer, data) + } + + /** + * Reverts the buffer to the resource data + */ + const revert = (): void => { + assign(buffer, data); + } + + return { data, buffer, modified, apply, revert } +} + +const createUpdatableBuffer = (initialData: T, onUpdate: UpdateCallback): ServerDataBuffer => { + //Configure the initial data state and the buffer + + const state: ServerObjectBuffer = createObjectBuffer(initialData) + + /** + * Pushes buffered changes to the server if configured + */ + const update = async (): Promise => { + return onUpdate ? onUpdate(state) : Promise.resolve(undefined as unknown as TState); + } + + return{ ...state, update } +} + +const _useDataBuffer = (initialData: T, onUpdate?: UpdateCallback) +: ServerObjectBuffer | ServerDataBuffer => { + + return onUpdate ? createUpdatableBuffer(initialData, onUpdate) : createObjectBuffer(initialData) +} + +/** + * Cretes a buffer object that represents server data, tracks changes, + * can revert changes, and can optionally push buffered changes to the server + */ +export const useDataBuffer: UseDataBuffer = (_useDataBuffer as UseDataBuffer) diff --git a/lib/vnlib.browser/src/helpers/validation.ts b/lib/vnlib.browser/src/helpers/validation.ts new file mode 100644 index 0000000..e60563e --- /dev/null +++ b/lib/vnlib.browser/src/helpers/validation.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { defaultTo, first } from "lodash-es"; +import { get } from "@vueuse/core"; +import { ref, type MaybeRef } from "vue"; +import { useFormToaster } from "../toast"; +import type { IErrorNotifier } from "../toast"; + +/** + * Represents a validator that performs validation and returns a boolean + * along with an error message if the validation fails + */ +export interface IValidator { + /** + * Performs asynchronous validation and returns a boolean + * indicating if the validation was successful + */ + validate(): Promise; + /** + * Returns the first error message in the validation list + */ + firstError(): Error; +} + +export interface ValidationWrapper { + /** + * Computes the validation of the wrapped validator and captures the results. + * If the validation fails, the first error message is displayed in a toast notification. + * @returns The result of the validation + */ + validate: () => Promise; +} + +export interface VuelidateInstance { + $validate: () => Promise; + $errors: Array<{ $message: string }>; +} + +//Wrapper around a Vuelidate validator +const createVuelidateWrapper = (validator: Readonly>): IValidator => { + const validate = async (): Promise => { + return get(validator).$validate(); + } + + const firstError = (): Error => { + const errs = get(validator).$errors; + return new Error(first(errs)?.$message); + } + + return { validate, firstError } +} + +const createValidator = (validator: MaybeRef, toaster: IErrorNotifier): ValidationWrapper => { + const validate = async (): Promise => { + // Validate the form + const valid = await get(validator).validate(); + // If the form is no valid set the error message + if (!valid) { + const first = get(validator).firstError(); + // Set the error message to the first error in the form list + toaster.notifyError(first.message); + } + return valid + } + + return { validate } +} + +/** + * Wraps a validator with a toaster to display validation errors + * @param validator The validator to wrap + * @param toaster The toaster to use for validation errors + * @returns The validation toast wrapper + * @example returns { validate: Function> } + * const { validate } = useValidationWrapper(validator, toaster) + * const result = await validate() + */ +export const useValidationWrapper = (validator: Readonly>, toaster?: IErrorNotifier): ValidationWrapper => { + return createValidator(validator, toaster ?? useFormToaster()); +} + +/** + * Wraps a Vuelidate validator with a toaster to display validation errors + * @param validator The vuelidate instance to wrap + * @param toaster The toaster to use for validation errors + * @returns The validation toast wrapper + * @example returns { validate: Function> } + * const { validate } = useValidationWrapper(validator, toaster) + * const result = await validate() + */ +export const useVuelidateWrapper = (v$: Readonly>, toaster?: IErrorNotifier) => { + const wrapper = createVuelidateWrapper(v$); + //Vuelidate class wrapper around the validator + const validator = ref(wrapper); + return createValidator(validator, defaultTo(toaster, useFormToaster())); +} diff --git a/lib/vnlib.browser/src/helpers/wait.ts b/lib/vnlib.browser/src/helpers/wait.ts new file mode 100644 index 0000000..0443199 --- /dev/null +++ b/lib/vnlib.browser/src/helpers/wait.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Vaughn Nugent +// +// 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. + +import { set } from "@vueuse/core"; +import { readonly, ref, type Ref } from "vue"; + +export interface IWaitSingal{ + /** + * The waiting flag ref (readonly) + */ + readonly waiting: Readonly>; + /** + * Sets the waiting flag to the value passed in + * @param waiting The value to set the waiting flag to + */ + setWaiting(waiting: boolean): void; +} + +const wait = (waiting: Ref) : IWaitSingal => { + return{ + waiting: readonly(waiting), + setWaiting: (w: boolean) => set(waiting, w) + } +} + +export const useWait = (() => { + const waiting = ref(false) + /** + * Uses the internal waiting flag to determine if the component should be waiting + * based on pending apiCall() requests. + * @returns {Object} { waiting: Boolean, setWaiting: Function } + * @example //Waiting flag is reactive + * const { waiting, setWaiting } = useWait() + * setWaiting(true) //Manually set the waiting flag + * setWaiting(false) //Manually clear the waiting flag + */ + return (): IWaitSingal => wait(waiting); +})() + -- cgit