@availity/mui-spaces 0.2.6 → 0.3.0

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.
@@ -0,0 +1,162 @@
1
+ import React, { createContext, useContext, useReducer } from 'react';
2
+ import { isAbsoluteUrl } from '@availity/resolve-url';
3
+ import { Dialog, DialogTitle, DialogActions } from '@availity/mui-dialog';
4
+ import { Button } from '@availity/mui-button';
5
+ import { getUrl, getTarget, updateUrl } from '../helpers';
6
+ import { updateTopApps } from '../topApps';
7
+ import { DisclaimerModal } from './DisclaimerModal';
8
+ import { MultiPayerModal } from './MultiPayerModal';
9
+ import type {
10
+ ModalProviderState,
11
+ ModalState,
12
+ DisclaimerOnSubmitProps,
13
+ MultiPayerOnSubmitProps,
14
+ ModalOptions,
15
+ ModalTypes,
16
+ ModalContextType,
17
+ ModalReducerType,
18
+ } from './modal-types';
19
+ import { isModalOptions, isModalState } from './modal-types';
20
+
21
+ export const MODAL_INITIAL_STATE = {
22
+ isOpen: false,
23
+ modalOptions: undefined,
24
+ modalState: { selectedOption: { id: '', name: '' } },
25
+ selectedModal: {},
26
+ };
27
+
28
+ export const ModalContext = createContext<ModalContextType | null>(null);
29
+
30
+ export const useModal = () => {
31
+ const ctx = useContext(ModalContext);
32
+ if (!ctx) throw new Error('ModalContext be used inside a Provider');
33
+ return ctx;
34
+ };
35
+
36
+ export const MODAL_TYPES = {
37
+ DISCLAIMER: {
38
+ body: DisclaimerModal,
39
+ buttonProps: () => ({
40
+ children: 'Accept',
41
+ }),
42
+ onSubmit: ({ link, id: spaceId }: DisclaimerOnSubmitProps) => {
43
+ window.open(link.url?.[0] === '/' ? updateUrl(link.url, 'spaceId', spaceId) : link.url, link.target);
44
+ },
45
+ },
46
+ MULTI_PAYER: {
47
+ body: MultiPayerModal,
48
+ buttonProps: ({ selectedOption }: ModalState) => ({
49
+ children: 'Continue',
50
+ disabled: selectedOption === undefined,
51
+ }),
52
+ onSubmit: (
53
+ { metadata, link, id: spaceId, name }: MultiPayerOnSubmitProps,
54
+ modalState: ModalState,
55
+ dispatch: React.Dispatch<{ [x: string]: any; type: any }>
56
+ ) => {
57
+ if (metadata?.disclaimerId) {
58
+ dispatch({ type: 'OPEN_DISCLAIMER_MODAL', disclaimerId: metadata.disclaimerId, link, id: spaceId, name });
59
+ return;
60
+ }
61
+
62
+ const target = getTarget(link.target);
63
+
64
+ if (link.url) {
65
+ window.open(
66
+ !isAbsoluteUrl(link.url)
67
+ ? getUrl(updateUrl(link.url, 'spaceId', modalState.selectedOption.id), false, false)
68
+ : link.url,
69
+ target
70
+ );
71
+ }
72
+ },
73
+ },
74
+ };
75
+
76
+ export const modalActions = {
77
+ RESET: (): ModalProviderState => MODAL_INITIAL_STATE,
78
+ OPEN_DISCLAIMER_MODAL: (state: ModalProviderState, modalOptions: ModalOptions): ModalProviderState => ({
79
+ ...state,
80
+ isOpen: true,
81
+ selectedModal: MODAL_TYPES.DISCLAIMER,
82
+ modalOptions: { ...modalOptions, type: modalOptions.spaceType },
83
+ }),
84
+ OPEN_MULTI_PAYER_MODAL: (state: ModalProviderState, modalOptions: ModalOptions): ModalProviderState => ({
85
+ ...state,
86
+ isOpen: true,
87
+ selectedModal: MODAL_TYPES.MULTI_PAYER,
88
+ modalOptions: {
89
+ ...modalOptions,
90
+ type: modalOptions.spaceType,
91
+ },
92
+ }),
93
+ UPDATE_MODAL_STATE: (state: ModalProviderState, modalState: ModalState): ModalProviderState => ({
94
+ ...state,
95
+ modalState,
96
+ }),
97
+ };
98
+
99
+ export const modalReducer: ModalReducerType = (state, { type, ...action }) => {
100
+ if (type === 'RESET') return modalActions.RESET();
101
+ if (isModalOptions(action)) {
102
+ if (type === 'OPEN_MULTI_PAYER_MODAL') return modalActions.OPEN_MULTI_PAYER_MODAL(state, action);
103
+ else if (type === 'OPEN_DISCLAIMER_MODAL') return modalActions.OPEN_DISCLAIMER_MODAL(state, action);
104
+ } else if (isModalState(action)) {
105
+ if (type === 'UPDATE_MODAL_STATE') return modalActions.UPDATE_MODAL_STATE(state, action);
106
+ }
107
+ return state;
108
+ };
109
+
110
+ export const ModalProvider = ({ children }: { children?: React.ReactNode }) => {
111
+ const [{ selectedModal, modalOptions, modalState, isOpen }, dispatch] = useReducer(modalReducer, MODAL_INITIAL_STATE);
112
+
113
+ const toggle = () => dispatch({ type: 'RESET' });
114
+
115
+ const buttonProps = selectedModal?.buttonProps && selectedModal?.buttonProps({ ...modalState, modalOptions });
116
+
117
+ const Body = selectedModal?.body;
118
+
119
+ return (
120
+ <ModalContext.Provider
121
+ value={(modalType: ModalTypes, modalOptions: ModalOptions) =>
122
+ dispatch({ type: `OPEN_${modalType}`, ...modalOptions })
123
+ }
124
+ >
125
+ <Dialog open={isOpen}>
126
+ <DialogTitle id="disclaimer-header">{modalOptions?.title}</DialogTitle>
127
+ {Body && (
128
+ <Body
129
+ {...modalOptions}
130
+ setState={(newState: ModalState) => dispatch({ type: 'UPDATE_MODAL_STATE', ...newState })}
131
+ state={modalState}
132
+ />
133
+ )}
134
+ <DialogActions>
135
+ <Button onClick={toggle}>Cancel</Button>
136
+ <Button
137
+ color="primary"
138
+ {...buttonProps}
139
+ onClick={() => {
140
+ if (selectedModal?.onSubmit && modalOptions && modalState) {
141
+ selectedModal.onSubmit(modalOptions, modalState, dispatch);
142
+ }
143
+ if (modalOptions) {
144
+ updateTopApps(
145
+ {
146
+ configurationId: modalOptions.id,
147
+ type: modalOptions.type,
148
+ name: modalOptions.name,
149
+ id: modalOptions.id,
150
+ },
151
+ modalOptions.user
152
+ );
153
+ }
154
+ toggle();
155
+ }}
156
+ />
157
+ </DialogActions>
158
+ </Dialog>
159
+ {children}
160
+ </ModalContext.Provider>
161
+ );
162
+ };
@@ -0,0 +1,61 @@
1
+ import { render, waitFor, fireEvent } from '@testing-library/react';
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { Spaces } from '../Spaces';
4
+ import { SpacesLink } from '../SpacesLink/SpacesLink';
5
+
6
+ const parentSpace2 = {
7
+ type: 'space',
8
+ name: 'parent space 2',
9
+ id: '2',
10
+ configurationId: '2',
11
+ images: {
12
+ tile: '/spaces/tile.jpg',
13
+ },
14
+ };
15
+
16
+ const parentSpace3 = {
17
+ type: 'space',
18
+ name: 'parent space 3',
19
+ id: '3',
20
+ configurationId: '3',
21
+ images: {
22
+ tile: '/spaces/tile.jpg',
23
+ },
24
+ };
25
+
26
+ const space = {
27
+ id: 'multipayermodal',
28
+ configurationId: 'multipayermodal',
29
+ name: 'Some Application',
30
+ type: 'space',
31
+ parents: [parentSpace2, parentSpace3],
32
+ link: {
33
+ url: '/some-url',
34
+ target: 'newBody',
35
+ },
36
+ };
37
+
38
+ const MultiPayerModal = () => {
39
+ const queryClient = new QueryClient();
40
+ return (
41
+ <QueryClientProvider client={queryClient}>
42
+ <Spaces spaces={[space, parentSpace2, parentSpace3]} clientId="my-client-id">
43
+ <SpacesLink spaceId="multipayermodal" clientId="my-client-id" />
44
+ </Spaces>
45
+ </QueryClientProvider>
46
+ );
47
+ };
48
+
49
+ describe('MultiPayerModal', () => {
50
+ afterEach(() => {
51
+ jest.clearAllMocks();
52
+ });
53
+ it('renders modal when space has payerspace parents', async () => {
54
+ const { getByText } = render(<MultiPayerModal />);
55
+
56
+ const link = await waitFor(() => getByText('Some Application'));
57
+ fireEvent.click(link);
58
+
59
+ await waitFor(() => expect(getByText('Open Some Application as:')).toBeDefined());
60
+ });
61
+ });
@@ -0,0 +1,19 @@
1
+ import { DialogContent } from '@availity/mui-dialog';
2
+ import { Grid, Box } from '@availity/mui-layout';
3
+ import { Typography } from '@availity/mui-typography';
4
+ import { SpacesImage } from '../SpacesImage';
5
+ import { ModalProps } from './modal-types';
6
+
7
+ export const MultiPayerModal = ({ parentPayerSpaces, name, state: { selectedOption }, setState }: ModalProps) => (
8
+ <DialogContent>
9
+ <Typography>{`Open ${name} as: ${selectedOption ? selectedOption.name || selectedOption.id : ''}`}</Typography>
10
+ <Grid direction="row">
11
+ {parentPayerSpaces &&
12
+ parentPayerSpaces.map((payerSpace) => (
13
+ <Box onClick={() => setState({ selectedOption: payerSpace })}>
14
+ <SpacesImage spaceId={payerSpace.configurationId} imageType="images.tile" />
15
+ </Box>
16
+ ))}
17
+ </Grid>
18
+ </DialogContent>
19
+ );
@@ -0,0 +1,91 @@
1
+ import { Link, Space } from '../spaces-types';
2
+
3
+ export type ModalTypes = 'DISCLAIMER_MODAL' | 'MULTI_PAYER_MODAL';
4
+
5
+ type MetadataKeys = 'disclaimerId' | string;
6
+
7
+ type Metadata = Record<MetadataKeys, string>;
8
+
9
+ export type ModalProps = {
10
+ disclaimerId?: string;
11
+ parentPayerSpaces?: Space[];
12
+ name?: string;
13
+ state?: any;
14
+ setState?: any;
15
+ };
16
+
17
+ export type ModalOptions = {
18
+ disclaimerId?: string;
19
+ description?: string;
20
+ parentPayerSpaces?: Space[];
21
+ metadata?: Metadata;
22
+ id: string;
23
+ name: string;
24
+ user: any;
25
+ spaceType: string;
26
+ title: string;
27
+ link: Link;
28
+ };
29
+
30
+ export type DisclaimerOnSubmitProps = {
31
+ link: Link;
32
+ id: string;
33
+ };
34
+
35
+ export type MultiPayerOnSubmitProps = {
36
+ metadata?: Metadata;
37
+ link: Link;
38
+ id: string;
39
+ name: string;
40
+ };
41
+
42
+ type NormalizedModalOptions = {
43
+ type: string;
44
+ } & ModalOptions;
45
+
46
+ export type ModalState = {
47
+ selectedOption: {
48
+ id: string;
49
+ name: string;
50
+ };
51
+ };
52
+
53
+ export type ModalProviderState = {
54
+ isOpen: boolean;
55
+ modalOptions?: NormalizedModalOptions;
56
+ modalState: ModalState;
57
+ selectedModal?: {
58
+ body?:
59
+ | (({ disclaimerId }: ModalProps) => JSX.Element)
60
+ | (({ parentPayerSpaces, name, state: { selectedOption }, setState }: ModalProps) => JSX.Element);
61
+ onSubmit?: (
62
+ props: ModalOptions,
63
+ modalState: ModalState,
64
+ dispatch: React.Dispatch<ModalAction | (ModalAction & ModalOptions) | (ModalAction & ModalProviderState)>
65
+ ) => void;
66
+ buttonProps?: (prop: object) => object;
67
+ };
68
+ };
69
+
70
+ export type ModalActions = 'RESET' | 'OPEN_DISCLAIMER_MODAL' | 'OPEN_MULTI_PAYER_MODAL' | 'UPDATE_MODAL_STATE';
71
+
72
+ type ModalAction = {
73
+ type: ModalActions;
74
+ };
75
+
76
+ export type ModalContextType = {
77
+ (modalType: ModalTypes, modalOptions: ModalOptions): void;
78
+ };
79
+
80
+ export type ModalReducerType = (
81
+ state: ModalProviderState,
82
+ { type, ...action }: ModalAction | (ModalAction & ModalOptions) | (ModalAction & ModalProviderState)
83
+ ) => ModalProviderState;
84
+
85
+ // type guards: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
86
+ export const isModalOptions = (
87
+ action: ModalOptions | ModalProviderState | Record<string, any>
88
+ ): action is ModalOptions => (action as ModalOptions).spaceType !== undefined;
89
+
90
+ export const isModalState = (action: ModalOptions | ModalState | Record<string, any>): action is ModalState =>
91
+ (action as ModalState).selectedOption !== undefined;
@@ -21,16 +21,21 @@ afterAll(() => {
21
21
 
22
22
  describe('getAllSpaces', () => {
23
23
  it('gets all spaces', async () => {
24
+ let apiCalls = 0;
25
+ server.events.on('request:start', () => (apiCalls += 1));
26
+
24
27
  const spaces = await fetchAllSpaces({
25
28
  query: configurationFindMany,
26
29
  clientId: 'clientId',
27
30
  variables: {
28
31
  types: ['space'],
32
+ perPage: 10,
29
33
  },
30
34
  });
31
35
  // Check correct spaces get returned
32
- expect(spaces.length).toBe(10);
33
- expect(spaces[0].id).toBe('1');
34
- expect(spaces[spaces.length - 1].id).toBe('10');
36
+ expect(spaces.length).toBe(20);
37
+
38
+ // Check that it took 2 calls to return all 18
39
+ expect(apiCalls).toBe(2);
35
40
  });
36
41
  });
@@ -1,8 +1,11 @@
1
+ import { SsoTypeSpace } from './SpacesLink/spaces-link-types';
2
+
1
3
  export type Link = {
2
4
  /** Contains a URL or URL Fragment that the hyperlink points to. */
3
- url: string;
5
+ url?: string;
4
6
  /** Specifies where to open the linked URL. */
5
- target: string;
7
+ target?: string;
8
+ text?: string;
6
9
  };
7
10
 
8
11
  export type NameValuePair = {
@@ -45,16 +48,15 @@ export type Space = {
45
48
  mapping?: Record<string, string>;
46
49
  mappingPairs?: NameValuePair[];
47
50
  /** Whether or not the space is ghosted */
48
- isGhost?: boolean;
49
- link?: {
50
- /** Contains a URL or URL Fragment that the hyperlink points to. */
51
- url: string;
52
- /** Specifies where to open the linked URL. */
53
- target: string;
54
- };
51
+ isGhosted?: boolean;
52
+ link?: Link;
55
53
  /** The description of the configuration. */
56
54
  description?: string;
57
55
  url?: string;
56
+ parents?: Space[];
57
+ shortName?: string;
58
+ activeDate?: string;
59
+ isNew?: boolean;
58
60
  };
59
61
 
60
62
  export type FetchSpacesProps = {
@@ -74,14 +76,14 @@ export type FetchAllSpacesProps = {
74
76
  /** The Client ID obtained from APIConnect. Must be subscribed to the thanos API. */
75
77
  clientId: string;
76
78
  /** The variables sent to the avWebQL endpoint. */
77
- variables?: object;
79
+ variables?: Record<string, any>;
78
80
  /** Array of spaces to be passed into the Spaces provider. */
79
81
  _spaces?: Space[];
80
82
  };
81
83
 
82
84
  export type SpacesContextType = {
83
85
  /** Array of spaces to be passed into the Spaces provider. */
84
- spaces?: Map<string, Space>;
86
+ spaces?: Map<string, Space | SsoTypeSpace>;
85
87
  /** Array of spaces from previous page load. */
86
88
  previousSpacesMap?: Map<string, Space>;
87
89
  /** Array of spaces organized by configurationId. */
@@ -119,7 +121,7 @@ export type SpacesProps = {
119
121
  /** The Client ID obtained from APIConnect. Must be subscribed to the thanos API. */
120
122
  clientId: string;
121
123
  /** Children can be a react child or render prop. */
122
- children?: React.ReactNode;
124
+ children?: React.ReactNode | ((props: any | undefined) => React.ReactNode);
123
125
  /** Array of payerIds the Spaces provider should fetch the spaces for.
124
126
  * Any payerIds already included in spaces will not be fetched again.
125
127
  * Note: If a payerId is associated with more than one payer space, the order in which they are returned should not be relied upon.
@@ -132,3 +134,5 @@ export type SpacesProps = {
132
134
  * Useful for if you already have the spaces in your app and don't want the spaces provider to have to fetch them again. */
133
135
  spaces?: Space[];
134
136
  };
137
+
138
+ export type UseSpaces = (...ids: string[]) => (Space | SsoTypeSpace)[] | undefined;