diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-01 10:56:02 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-01 10:56:02 -0500 |
commit | 1a82a909c5c4d0262d69a8a543e902ff6533a4b2 (patch) | |
tree | 7a0a296ae09209f5214e42917a46ad90f22c38d9 /lib/vnlib.browser/src/helpers | |
parent | 35a5763d675a68ec5e36e05584481b49e1c41ce0 (diff) |
swallow vnlib.browser
Diffstat (limited to 'lib/vnlib.browser/src/helpers')
-rw-r--r-- | lib/vnlib.browser/src/helpers/apiCall.ts | 276 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/autoHeartbeat.ts | 63 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/confirm.ts | 30 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/envSize.ts | 49 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/lastPage.ts | 109 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/message.ts | 56 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/pageGuard.ts | 49 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/serverObjectBuffer.ts | 149 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/validation.ts | 113 | ||||
-rw-r--r-- | lib/vnlib.browser/src/helpers/wait.ts | 55 |
10 files changed, 949 insertions, 0 deletions
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<T> { + /** + * 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<T> { + <TR>(callback: (data: T) => Promise<TR | undefined>): Promise<TR | undefined>; +} + +export type CustomMessageHandler = (message: string) => void; + +const useApiCallInternal = <T>(args: IApiHandle<T>): ApiCall<T> => { + + /** + * 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 <TR>(callback: (data: T) => Promise<TR | undefined>): Promise<TR | undefined> => { + 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<IErrorNotifier>, axios: Ref<Axios>): IApiHandle<IApiPassThrough> => { + + 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<IApiPassThrough> } =>{ + + 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 = <T>(callback: (api: IElevatedCallPassThrough) => Promise<T>): Promise<T | undefined> => { + //Invoke api call method but handle 401 errors by re-displaying the password prompt + return apiCall<T>(async (api: IApiPassThrough) : Promise<T | undefined> => { + // 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<boolean>; + /** + * 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<MaybeRef<number>>, enabled: MaybeRef<boolean>): 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<T> { + /** + * 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<T>; + /** + * The buffer is a reactive clone of the initial data state. It is used to track changes. + */ + readonly buffer: UnwrapNestedRefs<T>; + /** + * A computed value that indicates if the buffer has been modified from the data + */ + readonly modified: Readonly<Ref<boolean>>; + /** + * 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<T, TResut> = (state: ServerObjectBuffer<T>) => Promise<TResut>; + +/** + * 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<T, TResut> extends ServerObjectBuffer<T>{ + /** + * Pushes buffered changes to the server if configured + */ + update(): Promise<TResut>; +} + +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 + */ + <T extends object>(initialData: T): ServerObjectBuffer<T>; + /** + * 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 + */ + <T extends object, TState = unknown>(initialData: T, onUpdate: UpdateCallback<T, TState>): ServerDataBuffer<T, TState>; +} + + +const createObjectBuffer = <T extends object>(initialData: T): ServerObjectBuffer<T> => { + + /** + * 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 = <T extends object, TState = unknown> (initialData: T, onUpdate: UpdateCallback<T, TState>): ServerDataBuffer<T, TState> => { + //Configure the initial data state and the buffer + + const state: ServerObjectBuffer<T> = createObjectBuffer(initialData) + + /** + * Pushes buffered changes to the server if configured + */ + const update = async (): Promise<TState> => { + return onUpdate ? onUpdate(state) : Promise.resolve(undefined as unknown as TState); + } + + return{ ...state, update } +} + +const _useDataBuffer = <T extends object, TState = unknown>(initialData: T, onUpdate?: UpdateCallback<T, TState>) +: ServerObjectBuffer<T> | ServerDataBuffer<T, TState> => { + + return onUpdate ? createUpdatableBuffer<T, TState>(initialData, onUpdate) : createObjectBuffer<T>(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<boolean>; + /** + * 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<boolean>; +} + +export interface VuelidateInstance { + $validate: () => Promise<boolean>; + $errors: Array<{ $message: string }>; +} + +//Wrapper around a Vuelidate validator +const createVuelidateWrapper = <T extends VuelidateInstance>(validator: Readonly<MaybeRef<T>>): IValidator => { + const validate = async (): Promise<boolean> => { + return get(validator).$validate(); + } + + const firstError = (): Error => { + const errs = get(validator).$errors; + return new Error(first(errs)?.$message); + } + + return { validate, firstError } +} + +const createValidator = <T extends IValidator>(validator: MaybeRef<T>, toaster: IErrorNotifier): ValidationWrapper => { + const validate = async (): Promise<boolean> => { + // 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<Promise<boolean>> } + * const { validate } = useValidationWrapper(validator, toaster) + * const result = await validate() + */ +export const useValidationWrapper = <T extends IValidator>(validator: Readonly<MaybeRef<T>>, 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<Promise<boolean>> } + * const { validate } = useValidationWrapper(validator, toaster) + * const result = await validate() + */ +export const useVuelidateWrapper = <T extends VuelidateInstance>(v$: Readonly<MaybeRef<T>>, 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<Ref<boolean>>; + /** + * 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<boolean>) : 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); +})() + |