feat(global-state-store): ✨ implement GlobalStateStore and utils
* Create GlobalStateStore for managing application state. * Add utility functions for loading and saving state to storage. * Define types for global state and session management. * Implement ProgramDataWrapper for fetching and updating program data.
This commit is contained in:
237
src/GlobalStateStore/GlobalStateStore.ts
Normal file
237
src/GlobalStateStore/GlobalStateStore.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import type { StoreApi } from 'zustand';
|
||||||
|
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
import { useStoreWithEqualityFn } from 'zustand/traditional';
|
||||||
|
import { createStore } from 'zustand/vanilla';
|
||||||
|
|
||||||
|
import type { ExtractState, GlobalState, GlobalStateStoreState } from './GlobalStateStore.types';
|
||||||
|
|
||||||
|
import { loadStorage, saveStorage } from './GlobalStateStore.utils';
|
||||||
|
|
||||||
|
const emptyStore: GlobalState = {
|
||||||
|
connected: true, //Only invalidate when a connection cannot be made
|
||||||
|
controls: {},
|
||||||
|
environment: 'production',
|
||||||
|
loading: false,
|
||||||
|
meta: {},
|
||||||
|
program: {
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
},
|
||||||
|
|
||||||
|
user: {
|
||||||
|
access_control: false,
|
||||||
|
avatar_docid: 0,
|
||||||
|
id: 0,
|
||||||
|
login: '',
|
||||||
|
name: 'Guest',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
//We use vanilla store because we must be able to get the API key and token outside a react render loop
|
||||||
|
//The storage is custom because zustand's vanilla stores persist API crashes.
|
||||||
|
//Also not using the other store because it's using outdated methods and give that warning
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A zustand store function for managing program data and session information.
|
||||||
|
*
|
||||||
|
* @returns A zustand store state object.
|
||||||
|
*/
|
||||||
|
const GlobalStateStore = createStore<GlobalStateStoreState>((set, get) => ({
|
||||||
|
...emptyStore,
|
||||||
|
fetchData: async (url?: string) => {
|
||||||
|
const setFetched = async (
|
||||||
|
fn: (partial: GlobalState | Partial<GlobalState>) => Partial<GlobalStateStoreState>
|
||||||
|
) => {
|
||||||
|
const state = fn(get());
|
||||||
|
set((cur) => {
|
||||||
|
return { ...cur, ...state };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
set((s) => ({
|
||||||
|
...s,
|
||||||
|
loading: true,
|
||||||
|
session: { ...s.session, apiURL: url ?? s.session.apiURL },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = get().onFetchSession?.(get());
|
||||||
|
|
||||||
|
await setFetched((s) => ({
|
||||||
|
...s,
|
||||||
|
...result,
|
||||||
|
connected: true,
|
||||||
|
loading: false,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
await setFetched((s) => ({
|
||||||
|
...s,
|
||||||
|
connected: false,
|
||||||
|
error: `Load Exception: ${String(e)}`,
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
login: async (sessionData?: string) => {
|
||||||
|
const state = get();
|
||||||
|
const newstate = {
|
||||||
|
...state,
|
||||||
|
session: { ...state.session, authtoken: sessionData ?? '' },
|
||||||
|
user: { ...state.user },
|
||||||
|
};
|
||||||
|
|
||||||
|
set((cur) => {
|
||||||
|
return { ...cur, ...newstate };
|
||||||
|
});
|
||||||
|
await get().fetchData();
|
||||||
|
},
|
||||||
|
logout: async () => {
|
||||||
|
const newstate = { ...get(), ...emptyStore };
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
return { ...state, ...newstate };
|
||||||
|
});
|
||||||
|
await get().fetchData();
|
||||||
|
},
|
||||||
|
|
||||||
|
setAuthToken: (token: string) =>
|
||||||
|
set(
|
||||||
|
produce((state) => {
|
||||||
|
state.session.authtoken = token;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
setIsSecurity: (issecurity: boolean) =>
|
||||||
|
set(
|
||||||
|
produce((state) => {
|
||||||
|
state.session.jsonvalue.issecurity = issecurity;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
setState: (key, value) =>
|
||||||
|
set(
|
||||||
|
produce((state) => {
|
||||||
|
state[key] = value;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
setStateFN: (key, value) => {
|
||||||
|
set(
|
||||||
|
produce((state) => {
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
state[key] = (value as (value: any) => any)(state[key]);
|
||||||
|
} else {
|
||||||
|
console.error('value is not a function', value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateSession: (setter: UpdateSessionType) => {
|
||||||
|
const curState = get();
|
||||||
|
|
||||||
|
const newSession: null | SessionDetail | void =
|
||||||
|
typeof setter === 'function'
|
||||||
|
? setter(curState?.session)
|
||||||
|
: typeof setter === 'object'
|
||||||
|
? (setter as SessionDetail)
|
||||||
|
: null;
|
||||||
|
if (newSession === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedState = {
|
||||||
|
...curState,
|
||||||
|
session: { ...curState.session, ...(newSession || {}) },
|
||||||
|
};
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
session: { ...state.session, ...updatedState.session },
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Load storage after the createStore function is executed.
|
||||||
|
try {
|
||||||
|
loadStorage()
|
||||||
|
.then((state) =>
|
||||||
|
GlobalStateStore.setState((s: GlobalStateStoreState) => ({
|
||||||
|
...s,
|
||||||
|
...state,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Error loading storage:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
GlobalStateStore.subscribe((state, previousState) => {
|
||||||
|
//console.log('subscribe', state, previousState)
|
||||||
|
saveStorage(state).catch((e) => {
|
||||||
|
console.error('Error saving storage:', e);
|
||||||
|
});
|
||||||
|
if (state.session.authtoken !== previousState.session.authtoken) {
|
||||||
|
setAuthTokenAPI(state.session.authtoken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading storage:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-bounded version of useStore with shallow equality build in
|
||||||
|
*/
|
||||||
|
const createTypeBoundedUseStore = ((store) => (selector) =>
|
||||||
|
useStoreWithEqualityFn(store, selector, shallow)) as <S extends StoreApi<unknown>>(
|
||||||
|
store: S
|
||||||
|
) => {
|
||||||
|
(): ExtractState<S>;
|
||||||
|
<T>(selector: (state: ExtractState<S>) => T): T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a hook to access the state of the `GlobalStateStore` with shallow equality
|
||||||
|
* checking in the selector function.
|
||||||
|
*
|
||||||
|
* @typeParam S - The type of the store
|
||||||
|
* @param store - The store to be used
|
||||||
|
* @returns A function that returns the state of the store, or a selected part of it
|
||||||
|
*/
|
||||||
|
const useGlobalStateStore = createTypeBoundedUseStore(GlobalStateStore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the API URL in the program data store state.
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL to set as the API URL.
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
const setApiURL = (url: string) => {
|
||||||
|
if (typeof GlobalStateStore?.setState !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GlobalStateStore.setState((s: GlobalStateStoreState) => ({
|
||||||
|
...s,
|
||||||
|
session: {
|
||||||
|
...s.session,
|
||||||
|
apiURL: url,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the API URL from the session stored in the program data store.
|
||||||
|
*
|
||||||
|
* @return {string} The API URL from the session.
|
||||||
|
*/
|
||||||
|
const getApiURL = (): string => {
|
||||||
|
if (typeof GlobalStateStore?.setState !== 'function') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const s = GlobalStateStore.getState();
|
||||||
|
return s.session?.apiURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getApiURL, GlobalStateStore, setApiURL, useGlobalStateStore };
|
||||||
92
src/GlobalStateStore/GlobalStateStore.types.ts
Normal file
92
src/GlobalStateStore/GlobalStateStore.types.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { type FunctionComponent } from 'react';
|
||||||
|
|
||||||
|
type DatabaseDetail = {
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExtractState<S> = S extends { getState: () => infer X } ? X : never;
|
||||||
|
|
||||||
|
interface GlobalState {
|
||||||
|
[key: string]: any;
|
||||||
|
apiURL: string;
|
||||||
|
authtoken: string;
|
||||||
|
connected?: boolean;
|
||||||
|
environment?: 'development' | 'production';
|
||||||
|
error?: string;
|
||||||
|
globals?: Record<string, any>;
|
||||||
|
lastLoadTime?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
menu?: Array<any>;
|
||||||
|
meta?: ProgramMetaData;
|
||||||
|
program: ProgramDetail;
|
||||||
|
|
||||||
|
updatedAt?: string;
|
||||||
|
user: UserDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalStateStoreState extends GlobalState {
|
||||||
|
fetchData: (url?: string) => Promise<void>;
|
||||||
|
login: (sessionData?: string) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
onFetchSession?: (state: GlobalState) => Promise<GlobalState>;
|
||||||
|
setAuthToken: (token: string) => void;
|
||||||
|
setIsSecurity: (isSecurity: boolean) => void;
|
||||||
|
setState: <K extends keyof GlobalState>(key: K, value: GlobalState[K]) => void;
|
||||||
|
setStateFN: <K extends keyof GlobalState>(
|
||||||
|
key: K,
|
||||||
|
value: (current: GlobalState[K]) => Partial<GlobalState[K]>
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgramDetail = {
|
||||||
|
backend_version?: string;
|
||||||
|
biglogolink?: string;
|
||||||
|
database?: DatabaseDetail;
|
||||||
|
database_version?: string;
|
||||||
|
logolink?: string;
|
||||||
|
name: string;
|
||||||
|
programSummary?: string;
|
||||||
|
rid_owner?: number;
|
||||||
|
slug: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProgramMetaData {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgramWrapperProps {
|
||||||
|
apiURL?: string;
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
debugMode?: boolean;
|
||||||
|
fallback?: React.ReactNode | React.ReactNode[];
|
||||||
|
renderFallback?: boolean;
|
||||||
|
testMode?: boolean;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserDetail = {
|
||||||
|
access_control?: boolean;
|
||||||
|
avatar_docid?: number;
|
||||||
|
fullnames?: string;
|
||||||
|
guid?: string;
|
||||||
|
id?: number;
|
||||||
|
isadmin?: boolean;
|
||||||
|
login?: string;
|
||||||
|
name?: string;
|
||||||
|
notice_msg?: string;
|
||||||
|
parameters?: Record<string, any>;
|
||||||
|
rid_hub?: number;
|
||||||
|
rid_user?: number;
|
||||||
|
secuser?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ExtractState,
|
||||||
|
GlobalState,
|
||||||
|
GlobalStateStoreState,
|
||||||
|
ProgramDetail,
|
||||||
|
ProgramWrapperProps,
|
||||||
|
UserDetail,
|
||||||
|
};
|
||||||
109
src/GlobalStateStore/GlobalStateStore.utils.ts
Normal file
109
src/GlobalStateStore/GlobalStateStore.utils.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { createStore, entries, set, type UseStore } from 'idb-keyval';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'app-data';
|
||||||
|
|
||||||
|
const initilizeStore = () => {
|
||||||
|
if (indexedDB) {
|
||||||
|
try {
|
||||||
|
return createStore('programdata', 'programdata');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize indexedDB store: ', STORAGE_KEY, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const programDataIndexDBStore: null | UseStore = initilizeStore();
|
||||||
|
|
||||||
|
const skipKeysCallback = (dataKey: string, dataValue: any) => {
|
||||||
|
if (typeof dataValue === 'function') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dataKey === 'loading' ||
|
||||||
|
dataKey === 'error' ||
|
||||||
|
dataKey === 'security' ||
|
||||||
|
dataKey === 'meta' ||
|
||||||
|
dataKey === 'help'
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadStorage<T = any>(storageKey?: string): Promise<T> {
|
||||||
|
if (indexedDB) {
|
||||||
|
try {
|
||||||
|
const storeValues = await entries(programDataIndexDBStore);
|
||||||
|
const obj: any = {};
|
||||||
|
|
||||||
|
storeValues.forEach((arr: string[]) => {
|
||||||
|
const k = String(arr[0]);
|
||||||
|
obj[k] = JSON.parse(arr[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e);
|
||||||
|
}
|
||||||
|
} else if (localStorage) {
|
||||||
|
try {
|
||||||
|
const storagedata = localStorage.getItem(storageKey ?? STORAGE_KEY);
|
||||||
|
if (storagedata && storagedata.length > 0) {
|
||||||
|
const obj = JSON.parse(storagedata, (_dataKey, dataValue) => {
|
||||||
|
if (typeof dataValue === 'string' && dataValue.startsWith('function')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return dataValue;
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return {} as T;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load storage: ', storageKey ?? STORAGE_KEY, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStorage<T = any>(data: T, storageKey?: string): Promise<T> {
|
||||||
|
if (indexedDB) {
|
||||||
|
try {
|
||||||
|
const keys = Object.keys(data as object).filter(
|
||||||
|
(key) =>
|
||||||
|
key !== 'loading' &&
|
||||||
|
key !== 'error' &&
|
||||||
|
key !== 'help' &&
|
||||||
|
key !== 'meta' &&
|
||||||
|
key !== 'security' &&
|
||||||
|
typeof data[key as keyof T] !== 'function'
|
||||||
|
);
|
||||||
|
const promises = keys.map((key) => {
|
||||||
|
return set(
|
||||||
|
key,
|
||||||
|
JSON.stringify((data as any)[key], skipKeysCallback) ?? '{}',
|
||||||
|
programDataIndexDBStore
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save indexedDB storage: ', storageKey ?? STORAGE_KEY, e);
|
||||||
|
}
|
||||||
|
} else if (localStorage) {
|
||||||
|
try {
|
||||||
|
const dataString = JSON.stringify(data, skipKeysCallback);
|
||||||
|
|
||||||
|
localStorage.setItem(storageKey ?? STORAGE_KEY, dataString ?? '{}');
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save localStorage storage: ', storageKey ?? STORAGE_KEY, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { loadStorage, saveStorage };
|
||||||
117
src/GlobalStateStore/GlobalStateStoreWrapper.tsx
Normal file
117
src/GlobalStateStore/GlobalStateStoreWrapper.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { setDefaultAPIOption } from '@bitechdev/utils'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { Center, Notification } from '@mantine/core'
|
||||||
|
import { programDataStore, useProgramDataStore } from './store/ProgramDataStore.store'
|
||||||
|
import { ProgramDataStoreState, ProgramWrapperProps, SessionDetail } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches program data and updates session details.
|
||||||
|
*
|
||||||
|
* @param {ProgramWrapperProps} props - Props for the ProgramDataWrapper component
|
||||||
|
* @return {ReactNode} The children of the component
|
||||||
|
*/
|
||||||
|
const ProgramDataWrapper = (props: ProgramWrapperProps) => {
|
||||||
|
const { fetchData, updateSession, connected, session, setState } = useProgramDataStore(
|
||||||
|
(state: ProgramDataStoreState) => ({
|
||||||
|
fetchData: state.fetchData,
|
||||||
|
updateSession: state.updateSession,
|
||||||
|
connected: state.connected,
|
||||||
|
session: state.session,
|
||||||
|
setState: state.setState,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.version) {
|
||||||
|
const program = programDataStore.getState()?.program
|
||||||
|
if (program) {
|
||||||
|
setState('program', { ...program, version: props.version })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.version])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session?.apiURL && session?.apiURL !== '') {
|
||||||
|
fetchData(props.apiURL)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('Error fetching data:', e)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
//Set the default API options
|
||||||
|
setDefaultAPIOption({
|
||||||
|
getAPIProvider: () => {
|
||||||
|
const s = programDataStore.getState()
|
||||||
|
if (s.session?.authtoken && s.session?.authtoken !== '') {
|
||||||
|
return { provider: undefined, providerKey: undefined }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: s.session?.provider,
|
||||||
|
providerKey: s.session?.providerKey,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAuthToken: () => {
|
||||||
|
const s = programDataStore.getState()
|
||||||
|
return s.session?.authtoken
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [session?.apiURL, session?.authtoken])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
updateSession?.((state: SessionDetail) => ({
|
||||||
|
apiURL: state.apiURL !== props.apiURL || !state.apiURL ? props.apiURL : state.apiURL,
|
||||||
|
debugMode: props.debugMode ?? state.debugMode,
|
||||||
|
testMode: props.testMode ?? state.testMode,
|
||||||
|
authtoken:
|
||||||
|
props.testMode && (!state.authtoken || state.authtoken === '') ? 'test' : state.authtoken,
|
||||||
|
provider:
|
||||||
|
props.testMode && (!state.provider || state.provider === '') ? 'bitech' : state.provider,
|
||||||
|
providerKey:
|
||||||
|
props.testMode && (!state.providerKey || state.providerKey === '')
|
||||||
|
? 'test'
|
||||||
|
: state.providerKey,
|
||||||
|
}))
|
||||||
|
|
||||||
|
axios.defaults.headers.common = {
|
||||||
|
'X-Program-API': 'bitechcore',
|
||||||
|
Authorization: `Bearer ${session?.authtoken}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session?.provider !== '') {
|
||||||
|
axios.defaults.headers.common['x-api-provider'] = session?.provider
|
||||||
|
}
|
||||||
|
if (session?.providerKey !== '') {
|
||||||
|
axios.defaults.headers.common['x-api-key'] = session?.providerKey
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in ProgramDataWrapper useEffect:', e)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
session.authtoken,
|
||||||
|
session.provider,
|
||||||
|
session.providerKey,
|
||||||
|
props.apiURL,
|
||||||
|
props.debugMode,
|
||||||
|
props.testMode,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (props.renderFallback && !connected) {
|
||||||
|
return (
|
||||||
|
props.fallback ?? (
|
||||||
|
<Center>
|
||||||
|
<Notification withCloseButton={false} color='red' title='Program URL not set'>
|
||||||
|
Please make sure your app was correctly installed. If this problem persists, reset your
|
||||||
|
page cache and go to the login page.
|
||||||
|
</Notification>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ProgramDataWrapper }
|
||||||
9
src/GlobalStateStore/index.ts
Normal file
9
src/GlobalStateStore/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { ProgramDataWrapper } from './src/ProgramDataWrapper'
|
||||||
|
export {
|
||||||
|
getApiURL,
|
||||||
|
setApiURL,
|
||||||
|
programDataStore,
|
||||||
|
useProgramDataStore,
|
||||||
|
} from './src/store/ProgramDataStore.store'
|
||||||
|
|
||||||
|
export type * from './src/types'
|
||||||
Reference in New Issue
Block a user