@qoretechnologies/reqraft 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/components/form/fields/Field.js +24 -31
  2. package/dist/components/form/fields/Field.js.map +1 -1
  3. package/dist/components/form/fields/boolean/Boolean.js +5 -9
  4. package/dist/components/form/fields/boolean/Boolean.js.map +1 -1
  5. package/dist/components/form/fields/color/Color.js +7 -14
  6. package/dist/components/form/fields/color/Color.js.map +1 -1
  7. package/dist/components/form/fields/cron/Cron.js +12 -19
  8. package/dist/components/form/fields/cron/Cron.js.map +1 -1
  9. package/dist/components/form/fields/long-string/LongString.js +5 -9
  10. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  11. package/dist/components/form/fields/markdown/Markdown.js +13 -20
  12. package/dist/components/form/fields/markdown/Markdown.js.map +1 -1
  13. package/dist/components/form/fields/number/Number.js +5 -9
  14. package/dist/components/form/fields/number/Number.js.map +1 -1
  15. package/dist/components/form/fields/radio-group/RadioGroup.js +7 -11
  16. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  17. package/dist/components/form/fields/string/String.js +4 -8
  18. package/dist/components/form/fields/string/String.js.map +1 -1
  19. package/dist/components/form/index.js +8 -24
  20. package/dist/components/form/index.js.map +1 -1
  21. package/dist/components/menu/Menu.js +23 -31
  22. package/dist/components/menu/Menu.js.map +1 -1
  23. package/dist/contexts/FetchContext.js +2 -5
  24. package/dist/contexts/FetchContext.js.map +1 -1
  25. package/dist/contexts/ReqraftContext.js +2 -5
  26. package/dist/contexts/ReqraftContext.js.map +1 -1
  27. package/dist/contexts/StorageContext.js +2 -5
  28. package/dist/contexts/StorageContext.js.map +1 -1
  29. package/dist/hooks/useFetch/useFetch.js +10 -14
  30. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  31. package/dist/hooks/useReqraftProperty.js +4 -8
  32. package/dist/hooks/useReqraftProperty.js.map +1 -1
  33. package/dist/hooks/useStorage/useStorage.d.ts.map +1 -1
  34. package/dist/hooks/useStorage/useStorage.js +13 -13
  35. package/dist/hooks/useStorage/useStorage.js.map +1 -1
  36. package/dist/hooks/useValidation.js +1 -5
  37. package/dist/hooks/useValidation.js.map +1 -1
  38. package/dist/index.js +6 -30
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers/FetchProvider.js +11 -15
  41. package/dist/providers/FetchProvider.js.map +1 -1
  42. package/dist/providers/ReqraftProvider.js +12 -17
  43. package/dist/providers/ReqraftProvider.js.map +1 -1
  44. package/dist/providers/StorageProvider.d.ts +1 -1
  45. package/dist/providers/StorageProvider.d.ts.map +1 -1
  46. package/dist/providers/StorageProvider.js +17 -21
  47. package/dist/providers/StorageProvider.js.map +1 -1
  48. package/dist/types/Form.js +1 -2
  49. package/dist/types.js +1 -2
  50. package/dist/utils/fetch.js +12 -17
  51. package/dist/utils/fetch.js.map +1 -1
  52. package/package.json +1 -2
  53. package/src/components/form/fields/Field.stories.tsx +165 -0
  54. package/src/components/form/fields/Field.tsx +172 -0
  55. package/src/components/form/fields/boolean/Boolean.stories.tsx +46 -0
  56. package/src/components/form/fields/boolean/Boolean.tsx +34 -0
  57. package/src/components/form/fields/color/Color.stories.tsx +60 -0
  58. package/src/components/form/fields/color/Color.tsx +43 -0
  59. package/src/components/form/fields/cron/Cron.stories.tsx +60 -0
  60. package/src/components/form/fields/cron/Cron.tsx +77 -0
  61. package/src/components/form/fields/long-string/LongString.stories.tsx +62 -0
  62. package/src/components/form/fields/long-string/LongString.tsx +35 -0
  63. package/src/components/form/fields/markdown/Markdown.stories.tsx +47 -0
  64. package/src/components/form/fields/markdown/Markdown.tsx +106 -0
  65. package/src/components/form/fields/number/Number.stories.tsx +74 -0
  66. package/src/components/form/fields/number/Number.tsx +53 -0
  67. package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +79 -0
  68. package/src/components/form/fields/radio-group/RadioGroup.tsx +46 -0
  69. package/src/components/form/fields/radio-group/images/java-96x128.png +0 -0
  70. package/src/components/form/fields/radio-group/images/python-129x128.png +0 -0
  71. package/src/components/form/fields/radio-group/images/qore-106x128.png +0 -0
  72. package/src/components/form/fields/string/String.stories.tsx +70 -0
  73. package/src/components/form/fields/string/String.tsx +43 -0
  74. package/src/components/form/index.tsx +8 -0
  75. package/src/components/menu/Menu.stories.tsx +73 -0
  76. package/src/components/menu/Menu.tsx +244 -0
  77. package/src/contexts/FetchContext.tsx +25 -0
  78. package/src/contexts/ReqraftContext.tsx +9 -0
  79. package/src/contexts/StorageContext.tsx +33 -0
  80. package/src/global.d.ts +4 -0
  81. package/src/hooks/useFetch/useFetch.stories.tsx +123 -0
  82. package/src/hooks/useFetch/useFetch.tsx +71 -0
  83. package/src/hooks/useReqraftProperty.ts +16 -0
  84. package/src/hooks/useStorage/useStorage.stories.tsx +85 -0
  85. package/src/hooks/useStorage/useStorage.ts +43 -0
  86. package/src/hooks/useValidation.ts +9 -0
  87. package/src/index.tsx +17 -0
  88. package/src/providers/FetchProvider.tsx +40 -0
  89. package/src/providers/ReqraftProvider.tsx +52 -0
  90. package/src/providers/StorageProvider.tsx +85 -0
  91. package/src/types/Form.ts +46 -0
  92. package/src/types.ts +14 -0
  93. package/src/utils/fetch.ts +121 -0
@@ -0,0 +1,71 @@
1
+ import { useState } from 'react';
2
+ import { useEffectOnce } from 'react-use';
3
+ import { useContextSelector } from 'use-context-selector';
4
+ import { FetchContext } from '../../contexts/FetchContext';
5
+ import { IReqraftQueryConfig } from '../../utils/fetch';
6
+
7
+ export interface IReqraftUseFetch<T> {
8
+ data: T | undefined;
9
+ loading: boolean;
10
+ load: () => Promise<T>;
11
+ error: Error | undefined;
12
+ }
13
+
14
+ export interface IReqraftUseFetchOptions<T> extends IReqraftQueryConfig {
15
+ defaultData?: T;
16
+ loadOnMount?: boolean;
17
+ }
18
+
19
+ export function useFetch<T>({
20
+ url,
21
+ method = 'GET',
22
+ body,
23
+ cache,
24
+ defaultData,
25
+ loadOnMount,
26
+ }: IReqraftUseFetchOptions<T>) {
27
+ const query = useContextSelector(FetchContext, (context) => {
28
+ switch (method) {
29
+ case 'GET':
30
+ return context.get;
31
+ case 'POST':
32
+ return context.post;
33
+ case 'PUT':
34
+ return context.put;
35
+ case 'DELETE':
36
+ return context.del;
37
+ default:
38
+ throw new Error('Invalid method');
39
+ }
40
+ });
41
+
42
+ const [loading, setLoading] = useState(loadOnMount);
43
+ const [data, setData] = useState<T | undefined>(defaultData);
44
+ const [error, setError] = useState<Error | undefined>();
45
+
46
+ async function load({
47
+ body: customBody,
48
+ mergeBodies,
49
+ }: { body?: Record<string | number, any>; mergeBodies?: boolean } = {}) {
50
+ setLoading(true);
51
+
52
+ const _body = mergeBodies ? { ...body, ...customBody } : customBody || body;
53
+ const response = await query<T>({ url, body: _body, cache });
54
+
55
+ setLoading(false);
56
+
57
+ if (response.ok) {
58
+ setData(response.data);
59
+ } else {
60
+ setError(response.error);
61
+ }
62
+ }
63
+
64
+ useEffectOnce(() => {
65
+ if (loadOnMount) {
66
+ load();
67
+ }
68
+ });
69
+
70
+ return { data, loading, load, error };
71
+ }
@@ -0,0 +1,16 @@
1
+ import { useContextSelector } from 'use-context-selector';
2
+ import { IReqraftContext, ReqraftContext } from '../contexts/ReqraftContext';
3
+
4
+ export const useReqraftProperty = <T extends keyof IReqraftContext>(
5
+ property: T
6
+ ): IReqraftContext[T] => {
7
+ const contextProperty = useContextSelector(ReqraftContext, (value) => {
8
+ if (!(property in value)) {
9
+ throw new Error(`Reqraft context property ${property} not found`);
10
+ }
11
+
12
+ return value[property];
13
+ });
14
+
15
+ return contextProperty;
16
+ };
@@ -0,0 +1,85 @@
1
+ import { ReqoreButton, ReqoreControlGroup, ReqoreP } from '@qoretechnologies/reqore';
2
+ import { StoryObj } from '@storybook/react';
3
+ import { fireEvent, within } from '@storybook/test';
4
+ import { storiesStorageMock } from '../../../__tests__/ mock';
5
+ import { testsWaitForText } from '../../../__tests__/utils';
6
+ import { StoryMeta } from '../../types';
7
+ import { useReqraftStorage } from './useStorage';
8
+
9
+ const meta = {
10
+ title: 'Hooks/useStorage',
11
+ render: () => {
12
+ const [storage, setStorage, removeValue] = useReqraftStorage<string>(
13
+ 'some-path',
14
+ 'This is a default value'
15
+ );
16
+
17
+ return (
18
+ <ReqoreControlGroup>
19
+ <ReqoreP>{storage}</ReqoreP>
20
+ <ReqoreButton onClick={() => setStorage('This is a NEW value')}>
21
+ Update storage
22
+ </ReqoreButton>
23
+ <ReqoreButton onClick={() => removeValue()}>Remove value</ReqoreButton>
24
+ </ReqoreControlGroup>
25
+ );
26
+ },
27
+ args: { reqraftOptions: { waitForStorage: true } },
28
+ } as StoryMeta<any>;
29
+
30
+ export default meta;
31
+ export type Story = StoryObj<typeof meta>;
32
+
33
+ export const DefaultValue: Story = {
34
+ args: {
35
+ method: 'GET',
36
+ },
37
+ parameters: {
38
+ mockData: [
39
+ {
40
+ url: 'https://hq.qoretechnologies.com:8092/api/latest/users/_current_/storage',
41
+ method: 'GET',
42
+ status: 200,
43
+ response: {},
44
+ },
45
+ ],
46
+ },
47
+ play: async () => {
48
+ await testsWaitForText('This is a default value');
49
+ },
50
+ };
51
+
52
+ export const StorageValue: Story = {
53
+ parameters: {
54
+ mockData: [...storiesStorageMock],
55
+ },
56
+ play: async () => {
57
+ await testsWaitForText('This is a storage value');
58
+ },
59
+ };
60
+
61
+ export const ValueCanBeUpdated: Story = {
62
+ ...StorageValue,
63
+ play: async ({ canvasElement }) => {
64
+ const canvas = within(canvasElement);
65
+
66
+ //await sleep(1000);
67
+
68
+ await testsWaitForText('This is a storage value');
69
+ await testsWaitForText('Update storage');
70
+ await fireEvent.click(canvas.getByText('Update storage'));
71
+ await testsWaitForText('This is a NEW value');
72
+ },
73
+ };
74
+
75
+ export const ValueCanBeRemoved: Story = {
76
+ ...StorageValue,
77
+ play: async ({ canvasElement }) => {
78
+ const canvas = within(canvasElement);
79
+
80
+ await testsWaitForText('This is a storage value');
81
+ await testsWaitForText('Remove value');
82
+ await fireEvent.click(canvas.getByText('Remove value'));
83
+ await testsWaitForText('This is a default value');
84
+ },
85
+ };
@@ -0,0 +1,43 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useContextSelector } from 'use-context-selector';
3
+ import { ReqraftStorageContext } from '../../contexts/StorageContext';
4
+
5
+ export type TReqraftStorageValue = string | number | boolean | Record<string | number, any> | any[];
6
+ export type TReqraftUseStorage<T extends TReqraftStorageValue> = [
7
+ T,
8
+ (newStorage: T) => void,
9
+ () => void,
10
+ ];
11
+
12
+ export function useReqraftStorage<T extends TReqraftStorageValue>(
13
+ path: string,
14
+ defaultValue?: T,
15
+ includeAppPrefix?: boolean
16
+ ): TReqraftUseStorage<T> {
17
+ const { getStorage, updateStorage, removeStorageValue } = useContextSelector(
18
+ ReqraftStorageContext,
19
+ ({ getStorage, updateStorage, removeStorageValue }) => ({
20
+ getStorage,
21
+ updateStorage,
22
+ removeStorageValue,
23
+ })
24
+ );
25
+
26
+ const value = useMemo(
27
+ () => getStorage(path, defaultValue, includeAppPrefix),
28
+ [path, defaultValue, includeAppPrefix, getStorage]
29
+ );
30
+
31
+ const updater = useCallback(
32
+ (newStorage: T) => {
33
+ updateStorage(path, newStorage, includeAppPrefix);
34
+ },
35
+ [path, includeAppPrefix, updateStorage]
36
+ );
37
+
38
+ const remover = useCallback(() => {
39
+ removeStorageValue(path, includeAppPrefix);
40
+ }, [path, includeAppPrefix, removeStorageValue]);
41
+
42
+ return [value, updater, remover];
43
+ }
@@ -0,0 +1,9 @@
1
+ import { TFormFieldType } from '../types/Form';
2
+
3
+ // @ts-expect-error "need to implement this"
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ export const useValidation = (value: any, type?: TFormFieldType) => {
6
+ // Build validation...
7
+
8
+ return true;
9
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,17 @@
1
+ export * from './components/form';
2
+ export {
3
+ IReqraftMenuItem,
4
+ IReqraftMenuProps,
5
+ ReqraftMenu,
6
+ TReqraftMenu,
7
+ TReqraftMenuItem,
8
+ } from './components/menu/Menu';
9
+
10
+ export { IReqraftUseFetch, useFetch } from './hooks/useFetch/useFetch';
11
+ export { TReqraftUseStorage, useReqraftStorage } from './hooks/useStorage/useStorage';
12
+ export {
13
+ ReqraftProvider,
14
+ ReqraftQueryClient,
15
+ initializeReqraft,
16
+ } from './providers/ReqraftProvider';
17
+ export { query } from './utils/fetch';
@@ -0,0 +1,40 @@
1
+ import { useQueryClient } from '@tanstack/react-query';
2
+ import { FetchContext, TReqraftContextQueryConfig } from '../contexts/FetchContext';
3
+ import { query } from '../utils/fetch';
4
+
5
+ export interface IReqraftFetchProviderProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export const ReqraftFetchProvider = ({ children }: IReqraftFetchProviderProps) => {
10
+ const queryClient = useQueryClient();
11
+
12
+ async function get<T>(config: TReqraftContextQueryConfig) {
13
+ return query<T>({ queryClient, ...config, method: 'GET' });
14
+ }
15
+
16
+ async function post<T>(config: TReqraftContextQueryConfig) {
17
+ return query<T>({ queryClient, ...config, method: 'POST' });
18
+ }
19
+
20
+ async function put<T>(config: TReqraftContextQueryConfig) {
21
+ return query<T>({ queryClient, ...config, method: 'PUT' });
22
+ }
23
+
24
+ async function del<T>(config: TReqraftContextQueryConfig) {
25
+ return query<T>({ queryClient, ...config, method: 'DELETE' });
26
+ }
27
+
28
+ return (
29
+ <FetchContext.Provider
30
+ value={{
31
+ get,
32
+ post,
33
+ put,
34
+ del,
35
+ }}
36
+ >
37
+ {children}
38
+ </FetchContext.Provider>
39
+ );
40
+ };
@@ -0,0 +1,52 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
+ import { ReactNode } from 'react';
3
+ import { IReqraftContext, ReqraftContext } from '../contexts/ReqraftContext';
4
+ import { IReqraftFetchConfig, setupFetch } from '../utils/fetch';
5
+ import { ReqraftFetchProvider } from './FetchProvider';
6
+ import { ReqraftStorageProvider } from './StorageProvider';
7
+
8
+ export const ReqraftQueryClient = new QueryClient();
9
+
10
+ export interface IReqraftProviderProps extends IReqraftContext {
11
+ children: ReactNode;
12
+ reactQueryClient?: QueryClient;
13
+ /*
14
+ * If true, the component will wait for the storage to be loaded before rendering the children.
15
+ */
16
+ waitForStorage?: boolean;
17
+ }
18
+
19
+ export interface IReqraftOptions {
20
+ instance?: string;
21
+ instanceToken: string;
22
+ instanceUnauthorizedRedirect?: IReqraftFetchConfig['unauthorizedRedirect'];
23
+ }
24
+
25
+ export const initializeReqraft = (options: IReqraftOptions ) => {
26
+ setupFetch({
27
+ instance: options.instance,
28
+ instanceToken: options.instanceToken,
29
+ unauthorizedRedirect: options.instanceUnauthorizedRedirect,
30
+ });
31
+
32
+ return ReqraftProvider;
33
+ };
34
+
35
+ export const ReqraftProvider = ({
36
+ appName,
37
+ children,
38
+ reactQueryClient,
39
+ waitForStorage = true,
40
+ }: IReqraftProviderProps) => {
41
+ return (
42
+ <ReqraftContext.Provider
43
+ value={{ appName }}
44
+ >
45
+ <QueryClientProvider client={reactQueryClient || ReqraftQueryClient}>
46
+ <ReqraftFetchProvider>
47
+ <ReqraftStorageProvider waitForStorage={waitForStorage}>{children}</ReqraftStorageProvider>
48
+ </ReqraftFetchProvider>
49
+ </QueryClientProvider>
50
+ </ReqraftContext.Provider>
51
+ );
52
+ };
@@ -0,0 +1,85 @@
1
+ import { cloneDeep, get, set } from 'lodash';
2
+ import { ReactNode, useEffect, useState } from 'react';
3
+ import type { Get } from 'type-fest';
4
+ import { ReqraftStorageContext, TReqraftStorage } from '../contexts/StorageContext';
5
+ import { useFetch } from '../hooks/useFetch/useFetch';
6
+ import { useReqraftProperty } from '../hooks/useReqraftProperty';
7
+ import { TReqraftStorageValue } from '../hooks/useStorage/useStorage';
8
+ import { IReqraftProviderProps } from './ReqraftProvider';
9
+
10
+ export interface IReqraftStorageProviderProps
11
+ extends Pick<IReqraftProviderProps, 'waitForStorage'> {
12
+ children: ReactNode;
13
+ }
14
+
15
+ export const ReqraftStorageProvider = ({
16
+ children,
17
+ waitForStorage,
18
+ }: IReqraftStorageProviderProps) => {
19
+ const appName = useReqraftProperty('appName');
20
+
21
+ const { data, loading } = useFetch({
22
+ url: 'users/_current_/storage',
23
+ cache: false,
24
+ loadOnMount: true,
25
+ });
26
+
27
+ const { load } = useFetch({
28
+ url: 'users/_current_/',
29
+ method: 'PUT',
30
+ cache: false,
31
+ });
32
+
33
+ const [storage, setStorage] = useState<TReqraftStorage>(data);
34
+
35
+ useEffect(() => {
36
+ if (data) {
37
+ setStorage(data);
38
+ }
39
+ }, [JSON.stringify(data)]);
40
+
41
+ const getStorage = function <T extends TReqraftStorageValue>(
42
+ path: string,
43
+ defaultValue: T,
44
+ includeAppPrefix: boolean = true
45
+ ): Get<TReqraftStorage, string> {
46
+ const _path = includeAppPrefix ? `${appName}.${path}` : path;
47
+
48
+ return get(storage, _path) ?? defaultValue;
49
+ };
50
+
51
+ const updateStorage = function <T extends TReqraftStorageValue>(
52
+ path: string,
53
+ value: T,
54
+ includeAppPrefix: boolean = true
55
+ ) {
56
+ const _path = includeAppPrefix ? `${appName}.${path}` : path;
57
+ const updatedStorage = set(cloneDeep(storage), _path, value);
58
+
59
+ setStorage(updatedStorage);
60
+
61
+ load({ body: { storage: updatedStorage } });
62
+ };
63
+
64
+ const removeStorageValue = function (path: string, includeAppPrefix: boolean = true) {
65
+ const _path = includeAppPrefix ? `${appName}.${path}` : path;
66
+
67
+ const updatedStorage = set(cloneDeep(storage), _path, null);
68
+
69
+ setStorage(updatedStorage);
70
+
71
+ load({ body: { storage_path: _path } });
72
+ };
73
+
74
+ if ((loading || !storage) && waitForStorage) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <ReqraftStorageContext.Provider
80
+ value={{ storage, getStorage, updateStorage, removeStorageValue }}
81
+ >
82
+ {children}
83
+ </ReqraftStorageContext.Provider>
84
+ );
85
+ };
@@ -0,0 +1,46 @@
1
+ import { IColorFormFieldProps } from '../components/form/fields/color/Color';
2
+
3
+ export type TFormFieldType =
4
+ | 'string'
5
+ | 'number'
6
+ | 'boolean'
7
+ | 'date'
8
+ | 'time'
9
+ | 'datetime'
10
+ | 'select'
11
+ | 'multiSelect'
12
+ | 'radio'
13
+ | 'checkbox'
14
+ | 'file'
15
+ | 'image'
16
+ | 'color'
17
+ | 'password'
18
+ | 'email'
19
+ | 'phone'
20
+ | 'url'
21
+ | 'markdown'
22
+ | 'long-string'
23
+ | 'cron';
24
+
25
+ export type TFormFieldValueType<T> =
26
+ T extends 'string' ? string
27
+ : T extends 'number' ? number
28
+ : T extends 'boolean' ? boolean
29
+ : T extends 'date' ? Date | string
30
+ : T extends 'time' ? Date | string
31
+ : T extends 'datetime' ? Date | string
32
+ : T extends 'select' ? string
33
+ : T extends 'multiSelect' ? string[]
34
+ : T extends 'radio' ? string
35
+ : T extends 'checkbox' ? boolean
36
+ : T extends 'file' ? File
37
+ : T extends 'image' ? string
38
+ : T extends 'color' ? IColorFormFieldProps['value']
39
+ : T extends 'password' ? string
40
+ : T extends 'email' ? string
41
+ : T extends 'phone' ? string
42
+ : T extends 'url' ? string
43
+ : T extends 'markdown' ? string
44
+ : T extends 'long-string' ? string
45
+ : T extends 'cron' ? string
46
+ : any;
package/src/types.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { IReqoreUIProviderProps } from '@qoretechnologies/reqore/dist/containers/UIProvider';
2
+ import { Meta } from '@storybook/react';
3
+ import { IReqraftProviderProps } from './providers/ReqraftProvider';
4
+
5
+ export type StoryMeta<
6
+ Component extends keyof JSX.IntrinsicElements | React.JSXElementConstructor<any>,
7
+ AdditionalArgs = Record<string, any>,
8
+ > = Meta<
9
+ React.ComponentProps<Component> &
10
+ AdditionalArgs & {
11
+ reqoreOptions: IReqoreUIProviderProps['options'];
12
+ reqraftOptions: IReqraftProviderProps;
13
+ }
14
+ >;
@@ -0,0 +1,121 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ import { ReqraftQueryClient } from '../providers/ReqraftProvider';
3
+
4
+ export interface IReqraftFetchConfig {
5
+ instance: string;
6
+ instanceToken: string;
7
+ unauthorizedRedirect?: (pathname: string) => string;
8
+ }
9
+
10
+ export interface IReqraftFetchResponse<T> {
11
+ data: T;
12
+ ok: boolean;
13
+ code?: number;
14
+ error?: any;
15
+ }
16
+
17
+ export const fetchConfig: IReqraftFetchConfig = {
18
+ instance: window.location.origin + '/',
19
+ instanceToken: '',
20
+ unauthorizedRedirect: (pathname: string) => `/?next=${pathname}`,
21
+ };
22
+
23
+ const CACHE_EXPIRATION_TIME = 5 * 60 * 1000; // 5 minutes
24
+
25
+ export const setupFetch = ({
26
+ instance,
27
+ instanceToken,
28
+ unauthorizedRedirect,
29
+ }: IReqraftFetchConfig) => {
30
+ fetchConfig.instance = instance;
31
+ fetchConfig.instanceToken = instanceToken;
32
+
33
+ if (unauthorizedRedirect) {
34
+ fetchConfig.unauthorizedRedirect = unauthorizedRedirect;
35
+ }
36
+ };
37
+
38
+ async function doFetchData(
39
+ url: string,
40
+ method = 'GET',
41
+ body?: { [key: string]: any }
42
+ ): Promise<Response> {
43
+ if (!fetchConfig.instanceToken) {
44
+ return new Response(JSON.stringify({}), {
45
+ status: 401,
46
+ statusText: 'Unauthorized',
47
+ });
48
+ }
49
+
50
+ return fetch(`${fetchConfig.instance}api/latest/${url}`, {
51
+ method,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ Authorization: `Bearer ${fetchConfig.instanceToken}`,
55
+ },
56
+ body: JSON.stringify(body),
57
+ }).catch((error) => {
58
+ return new Response(JSON.stringify({}), {
59
+ status: 500,
60
+ statusText: `Request failed ${error.message}`,
61
+ });
62
+ });
63
+ }
64
+
65
+ export interface IReqraftQueryConfig {
66
+ url: string;
67
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
68
+ body?: Record<string | number, any>;
69
+ cache?: boolean;
70
+ queryClient?: QueryClient;
71
+ }
72
+
73
+ export async function query<T>({
74
+ url,
75
+ method = 'GET',
76
+ body,
77
+ cache = true,
78
+ queryClient = ReqraftQueryClient,
79
+ }: IReqraftQueryConfig): Promise<IReqraftFetchResponse<T>> {
80
+ const shouldCache = method === 'DELETE' || method === 'POST' ? false : cache;
81
+ const cacheKey = `${url}:${method}:${JSON.stringify(body || {})}`;
82
+
83
+ const requestData = await queryClient.fetchQuery({
84
+ queryKey: [cacheKey],
85
+ queryFn: async () => {
86
+ const response = await doFetchData(url, method, body);
87
+
88
+ const clone = response.clone();
89
+ const json = await clone.json();
90
+
91
+ if (response.status === 401) {
92
+ window.location.href = fetchConfig.unauthorizedRedirect(window.location.pathname);
93
+ }
94
+
95
+ return {
96
+ data: json,
97
+ ok: response.ok,
98
+ status: response.status,
99
+ statusText: response.statusText,
100
+ };
101
+ },
102
+ staleTime: shouldCache ? CACHE_EXPIRATION_TIME : 0,
103
+ });
104
+
105
+ if (!requestData.ok) {
106
+ queryClient.invalidateQueries({ queryKey: [cacheKey] });
107
+
108
+ return {
109
+ data: null,
110
+ ok: false,
111
+ code: requestData.status,
112
+ error: requestData.statusText,
113
+ };
114
+ }
115
+
116
+ return {
117
+ data: requestData.data,
118
+ ok: true,
119
+ code: requestData.status,
120
+ };
121
+ }