aboutsummaryrefslogtreecommitdiff
path: root/lib/vnlib.browser/src/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vnlib.browser/src/helpers')
-rw-r--r--lib/vnlib.browser/src/helpers/apiCall.ts276
-rw-r--r--lib/vnlib.browser/src/helpers/autoHeartbeat.ts63
-rw-r--r--lib/vnlib.browser/src/helpers/confirm.ts30
-rw-r--r--lib/vnlib.browser/src/helpers/envSize.ts49
-rw-r--r--lib/vnlib.browser/src/helpers/lastPage.ts109
-rw-r--r--lib/vnlib.browser/src/helpers/message.ts56
-rw-r--r--lib/vnlib.browser/src/helpers/pageGuard.ts49
-rw-r--r--lib/vnlib.browser/src/helpers/serverObjectBuffer.ts149
-rw-r--r--lib/vnlib.browser/src/helpers/validation.ts113
-rw-r--r--lib/vnlib.browser/src/helpers/wait.ts55
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);
+})()
+