@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,280 @@
1
+ import { waitFor, cleanup, render, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
2
+ import nativeForm from '@availity/native-form';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { Spaces } from '../Spaces';
5
+ import { SpacesLink } from './SpacesLink';
6
+ import type { Space } from '../spaces-types';
7
+ import type { SsoTypeSpace } from './spaces-link-types';
8
+
9
+ // eslint-disable-next-line @nx/enforce-module-boundaries
10
+ import { server } from '@availity/mock/src/lib/server';
11
+
12
+ jest.mock('@availity/native-form');
13
+
14
+ const buildSpacesLink = (space: Space | SsoTypeSpace, linkAttributes: Record<any, any>) => {
15
+ const queryClient = new QueryClient();
16
+ return (
17
+ <QueryClientProvider client={queryClient}>
18
+ <Spaces clientId="my-client-id" spaces={[space]}>
19
+ <SpacesLink
20
+ id={`application-link-${space.id}`}
21
+ titleTag="h5"
22
+ space={space}
23
+ linkAttributes={linkAttributes}
24
+ clientId="my-client-id"
25
+ linkStyle="card"
26
+ title={space.link?.text}
27
+ />
28
+ </Spaces>
29
+ </QueryClientProvider>
30
+ );
31
+ };
32
+
33
+ describe('useLink', () => {
34
+ beforeAll(() => {
35
+ // Start the interception.
36
+ server.listen();
37
+ });
38
+ beforeEach(() => {
39
+ Object.defineProperty(window, 'open', { value: jest.fn() });
40
+ });
41
+ afterEach(() => {
42
+ jest.clearAllMocks();
43
+ cleanup();
44
+ server.resetHandlers();
45
+ });
46
+
47
+ const space: Space | SsoTypeSpace = {
48
+ type: 'APPLICATION',
49
+ name: 'an application',
50
+ description: 'This is an application',
51
+ id: '1',
52
+ configurationId: '1',
53
+ };
54
+
55
+ it('should not call linkUrl onclick with no url', async () => {
56
+ space.id = '1';
57
+ space.configurationId = '1';
58
+
59
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
60
+
61
+ fireEvent.click(container);
62
+ await waitFor(() => {
63
+ expect(window.open).not.toHaveBeenCalled();
64
+ expect(nativeForm).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
68
+ it('should not call linkUrl on enter keypress with no url', async () => {
69
+ space.id = '2';
70
+ space.configurationId = '2';
71
+
72
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
73
+
74
+ fireEvent.keyPress(container, { charCode: 13 });
75
+ await waitFor(() => {
76
+ expect(window.open).not.toHaveBeenCalled();
77
+ expect(nativeForm).not.toHaveBeenCalled();
78
+ });
79
+ });
80
+
81
+ it('should call linkUrl onclick with relativeUrl', async () => {
82
+ space.id = '3';
83
+ space.configurationId = '3';
84
+ space.link = {
85
+ text: 'the link',
86
+ target: '_self',
87
+ url: '/path/to/url',
88
+ };
89
+
90
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
91
+
92
+ const linkHeader3 = await waitFor(() => container.querySelector('#app-title-3'));
93
+ if (linkHeader3) fireEvent.click(linkHeader3);
94
+
95
+ await waitFor(() => {
96
+ expect(window.open).toHaveBeenCalledWith('/path/to/url?spaceId=3', '_self');
97
+ expect(nativeForm).not.toHaveBeenCalled();
98
+ });
99
+ });
100
+
101
+ it('should call linkUrl on enter keydown with relativeUrl', async () => {
102
+ space.id = '4';
103
+ space.configurationId = '4';
104
+ space.link = {
105
+ text: 'the link',
106
+ target: '_self',
107
+ url: '/path/to/url',
108
+ };
109
+
110
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
111
+
112
+ const linkHeader4 = await waitFor(() => container.querySelector('#app-title-4'));
113
+
114
+ if (linkHeader4) fireEvent.keyDown(linkHeader4, { key: 'Enter' });
115
+
116
+ await waitFor(() => {
117
+ expect(window.open).toHaveBeenCalledWith('/path/to/url?spaceId=4', '_self');
118
+ expect(nativeForm).not.toHaveBeenCalled();
119
+ });
120
+ });
121
+
122
+ it('should call linkUrl onclick with absoluteUrl', async () => {
123
+ space.id = '5';
124
+ space.configurationId = '5';
125
+ space.link = { text: 'the link', target: '_self', url: 'https://www.google.com' };
126
+
127
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
128
+
129
+ const linkHeader5 = await waitFor(() => container.querySelector('#app-title-5'));
130
+
131
+ if (linkHeader5) fireEvent.click(linkHeader5);
132
+ await waitFor(() => {
133
+ expect(window.open).toHaveBeenCalledWith('https://www.google.com', '_self');
134
+ expect(nativeForm).not.toHaveBeenCalled();
135
+ });
136
+ });
137
+
138
+ it('should call linkUrl on enter keypress with absoluteUrl', async () => {
139
+ space.id = '6';
140
+ space.configurationId = '6';
141
+ space.link = { text: 'the link', target: '_self', url: 'https://www.google.com' };
142
+
143
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
144
+
145
+ const linkHeader6 = await waitFor(() => container.querySelector('#app-title-6'));
146
+
147
+ if (linkHeader6) fireEvent.keyDown(linkHeader6, { key: 'Enter' });
148
+ await waitFor(() => {
149
+ expect(window.open).toHaveBeenCalledWith('https://www.google.com', '_self');
150
+ expect(nativeForm).not.toHaveBeenCalled();
151
+ });
152
+ });
153
+
154
+ it('should call linkUrl if multiPayerModal used but linkAttributes.spaceId is passed', async () => {
155
+ space.id = '6';
156
+ space.configurationId = '6';
157
+ space.link = { text: 'the link', target: '_self', url: '/path/to/url' };
158
+ space.parents = [
159
+ {
160
+ id: 'parentId',
161
+ type: 'space',
162
+ name: 'parentId',
163
+ configurationId: 'parentId',
164
+ },
165
+ {
166
+ id: 'parentId2',
167
+ type: 'space',
168
+ name: 'parentId2',
169
+ configurationId: 'parentId2',
170
+ },
171
+ ];
172
+
173
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
174
+
175
+ const linkHeader6 = await waitFor(() => container.querySelector('#app-title-6'));
176
+
177
+ if (linkHeader6) fireEvent.keyDown(linkHeader6, { key: 'Enter' });
178
+ await waitFor(() => {
179
+ expect(window.open).toHaveBeenCalledWith('/path/to/url?spaceId=6', '_self');
180
+ expect(nativeForm).not.toHaveBeenCalled();
181
+ });
182
+ });
183
+
184
+ it('should call legacySSO onclick with disclaimerId metadata', async () => {
185
+ space.id = '7';
186
+ space.configurationId = '7';
187
+ space.link = { text: 'the link', target: '_self', url: '/path/to/url' };
188
+ space.meta = { disclaimerId: 'disclaimer' };
189
+
190
+ const { container, findByText, getByRole } = render(buildSpacesLink(space, { spaceId: space.id }));
191
+
192
+ const linkHeader7 = await waitFor(() => container.querySelector('#app-title-7'));
193
+ if (linkHeader7) fireEvent.click(linkHeader7);
194
+
195
+ await waitForElementToBeRemoved(() => getByRole('progressbar'));
196
+
197
+ const disclaimer = await findByText('This is a disclaimer.');
198
+ expect(disclaimer).toBeDefined();
199
+
200
+ const modalSubmit = await findByText('Accept');
201
+ fireEvent.click(modalSubmit);
202
+
203
+ await waitFor(() => {
204
+ expect(window.open).toHaveBeenCalledWith('/path/to/url?spaceId=7', '_self');
205
+ expect(nativeForm).not.toHaveBeenCalled();
206
+ });
207
+ });
208
+
209
+ it('should call legacySSO on enter keypress with disclaimerId metadata', async () => {
210
+ space.id = '8';
211
+ space.configurationId = '8';
212
+ space.link = { text: 'the link', target: '_self', url: '/path/to/url' };
213
+ space.meta = { disclaimerId: '1234' };
214
+
215
+ const { container, findByText } = render(buildSpacesLink(space, { spaceId: space.id }));
216
+
217
+ const linkHeader8 = await waitFor(() => container.querySelector('#app-title-8'));
218
+ if (linkHeader8) fireEvent.keyDown(linkHeader8, { key: 'Enter' });
219
+
220
+ const disclaimer = await findByText('hello world');
221
+ expect(disclaimer).toBeDefined();
222
+
223
+ const modalSubmit = await findByText('Accept');
224
+ fireEvent.click(modalSubmit);
225
+
226
+ await waitFor(() => {
227
+ expect(window.open).toHaveBeenCalledWith('/path/to/url?spaceId=8', '_self');
228
+ expect(nativeForm).not.toHaveBeenCalled();
229
+ });
230
+ });
231
+
232
+ it('should call ssoId onclick with ssoId metadata', async () => {
233
+ space.id = '9';
234
+ space.configurationId = '9';
235
+ space.link = { text: 'the link', target: '_self', url: '/path/to/url' };
236
+ space.meta = {
237
+ ssoId: 'ssoId',
238
+ };
239
+ space.type = 'saml';
240
+
241
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
242
+
243
+ const linkHeader9 = await waitFor(() => container.querySelector('#app-title-9'));
244
+ if (linkHeader9) fireEvent.click(linkHeader9);
245
+
246
+ await waitFor(() => {
247
+ expect(nativeForm).toHaveBeenCalledWith(
248
+ 'ssoId',
249
+ { X_Client_ID: 'my-client-id', X_XSRF_TOKEN: '', spaceId: '9' },
250
+ { target: '_self' },
251
+ 'saml'
252
+ );
253
+ expect(window.open).not.toHaveBeenCalled();
254
+ });
255
+ });
256
+
257
+ it('should call ssoId on enter keypress with ssoId metadata', async () => {
258
+ space.id = '10';
259
+ space.configurationId = '10';
260
+ space.link = { text: 'the link', target: '_self', url: '/path/to/url' };
261
+ space.meta = {
262
+ ssoId: 'ssoId',
263
+ };
264
+ space.type = 'saml';
265
+
266
+ const { container } = render(buildSpacesLink(space, { spaceId: space.id }));
267
+
268
+ const linkHeader10 = await waitFor(() => container.querySelector('#app-title-10'));
269
+ if (linkHeader10) fireEvent.keyDown(linkHeader10, { key: 'Enter' });
270
+ await waitFor(() => {
271
+ expect(nativeForm).toHaveBeenCalledWith(
272
+ 'ssoId',
273
+ { X_Client_ID: 'my-client-id', X_XSRF_TOKEN: '', spaceId: '10' },
274
+ { target: '_self' },
275
+ 'saml'
276
+ );
277
+ expect(window.open).not.toHaveBeenCalled();
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,111 @@
1
+ import { useCurrentUser } from '@availity/hooks';
2
+ import { useSpaces } from '../Spaces';
3
+ import { useModal } from '../modals/ModalProvider';
4
+ import { openLink, openLinkWithSso } from './linkHandlers';
5
+ import type { UseLink, MediaProps, SsoTypeSpace } from './spaces-link-types';
6
+ import { Space } from '../spaces-types';
7
+
8
+ const isSsoSpace = (space: Space | SsoTypeSpace | undefined): space is SsoTypeSpace =>
9
+ space?.type === 'saml' || space?.type === 'openid';
10
+
11
+ export const useLink: UseLink = (spaceOrSpaceId, options) => {
12
+ const spaceFromSpacesProvider = useSpaces(
13
+ (typeof spaceOrSpaceId === 'string' ? spaceOrSpaceId : spaceOrSpaceId?.id) || ''
14
+ );
15
+
16
+ const { data: user } = useCurrentUser();
17
+
18
+ const openModal = useModal();
19
+
20
+ const space = spaceFromSpacesProvider?.[0];
21
+
22
+ const parentPayerSpaces = space?.parents?.filter(
23
+ (p) => p.type && (p.type.toLowerCase() === 'space' || p.type.toLowerCase() === 'payerspace')
24
+ );
25
+
26
+ const legacySso = () => {
27
+ if (space && space.meta?.disclaimerId && space.link) {
28
+ openModal('DISCLAIMER_MODAL', {
29
+ disclaimerId: space.meta.disclaimerId,
30
+ name: space.name,
31
+ spaceType: space.type,
32
+ id: space.configurationId,
33
+ title: space.name,
34
+ description: space.description,
35
+ link: space.link,
36
+ user: user?.akaname,
37
+ });
38
+ }
39
+ };
40
+
41
+ const openMultiPayerModal = () => {
42
+ if (space && space.link)
43
+ openModal('MULTI_PAYER_MODAL', {
44
+ title: 'Open Link as Payer',
45
+ name: space?.name,
46
+ parentPayerSpaces,
47
+ link: space?.link,
48
+ id: space?.configurationId,
49
+ spaceType: space?.type,
50
+ metadata: space?.meta,
51
+ user: user?.akaname,
52
+ });
53
+ };
54
+
55
+ const mediaProps: MediaProps = {
56
+ role: 'link',
57
+ };
58
+
59
+ if (isSsoSpace(space) && space?.meta?.ssoId) {
60
+ if (!options?.clientId) {
61
+ throw new Error('clientId is required for SSO spaces');
62
+ }
63
+ if (!options.linkAttributes) {
64
+ throw new Error('linkAttributes are required for SSO spaces');
65
+ }
66
+ mediaProps.onClick = (event) => {
67
+ event.preventDefault();
68
+ if (options.clientId && options.linkAttributes) {
69
+ openLinkWithSso(space, {
70
+ akaname: user?.akaname,
71
+ clientId: options.clientId,
72
+ payerSpaceId: options.linkAttributes.spaceId,
73
+ ssoParams: options.linkAttributes,
74
+ });
75
+ }
76
+ };
77
+ mediaProps.onKeyDown = (event) => {
78
+ if (event.key === 'Enter') {
79
+ event.preventDefault();
80
+ if (options.clientId && options.linkAttributes) {
81
+ openLinkWithSso(space, {
82
+ akaname: user?.akaname,
83
+ clientId: options.clientId,
84
+ payerSpaceId: options.linkAttributes.spaceId,
85
+ ssoParams: options.linkAttributes,
86
+ });
87
+ }
88
+ }
89
+ };
90
+ } else if (space?.meta?.disclaimerId) {
91
+ mediaProps.onClick = legacySso;
92
+ mediaProps.onKeyDown = (event) => {
93
+ if (event.key === 'Enter') return legacySso();
94
+ };
95
+ } else if (parentPayerSpaces && parentPayerSpaces.length > 1 && !options?.linkAttributes?.spaceId) {
96
+ mediaProps.onClick = openMultiPayerModal;
97
+ mediaProps.onKeyDown = (event) => {
98
+ if (event.key === 'Enter') return openMultiPayerModal();
99
+ };
100
+ } else {
101
+ mediaProps.onClick = () => {
102
+ if (space) return openLink(space, { akaname: user?.akaname, payerSpaceId: options?.linkAttributes?.spaceId });
103
+ };
104
+ mediaProps.onKeyDown = (event) => {
105
+ if (event.key === 'Enter' && space)
106
+ return openLink(space, { akaname: user?.akaname, payerSpaceId: options?.linkAttributes?.spaceId });
107
+ };
108
+ }
109
+
110
+ return [space, mediaProps];
111
+ };
@@ -10,6 +10,9 @@ describe('updateUrl', () => {
10
10
 
11
11
  updatedUrl = updateUrl('http://www.example.com?foo=bar', 'fakeKey', 'fakeValue');
12
12
  expect(updatedUrl).toBe('http://www.example.com?fakeKey=fakeValue&foo=bar');
13
+
14
+ updatedUrl = updateUrl('http://www.example.com?foo=bar');
15
+ expect(updatedUrl).toBe('http://www.example.com?foo=bar');
13
16
  });
14
17
  });
15
18
 
@@ -1,9 +1,10 @@
1
1
  import qs from 'qs';
2
2
 
3
- export const updateUrl = (url: string, key: string, value: string) => {
3
+ export const updateUrl = (url: string, key?: string, value?: string) => {
4
4
  const [uri, queryString] = url.split('?');
5
5
  const currentParams = qs.parse(queryString);
6
- const newParams = qs.stringify({ ...currentParams, [key]: value }, { sort: (a, b) => a.localeCompare(b) });
6
+ const additionalParams = key && value && { [key]: value };
7
+ const newParams = qs.stringify({ ...currentParams, ...additionalParams }, { sort: (a, b) => a.localeCompare(b) });
7
8
 
8
9
  return `${uri}?${newParams}`;
9
10
  };
@@ -13,3 +14,26 @@ export const getUrl = (url = '', loadApp: boolean, absolute: boolean) => {
13
14
 
14
15
  return `/public/apps/home/#!/loadApp?appUrl=${encodeURIComponent(url)}`;
15
16
  };
17
+
18
+ export const getTarget = (target?: string) => {
19
+ // should start with _, otherwise it is specifying a specific frame name
20
+ // _blank = new tab/window, _self = same frame, _parent = parent frame (use for home page from modals), _top = document body, framename = specific frame
21
+ if (target && !target.startsWith('_')) {
22
+ if (target === 'BODY') {
23
+ return '_self';
24
+ }
25
+ if (target === 'TAB') {
26
+ return '_blank';
27
+ }
28
+ }
29
+
30
+ return target || '_self';
31
+ };
32
+
33
+ export const isFunction = (
34
+ children: JSX.Element | ((props: any | undefined) => JSX.Element)
35
+ ): children is (props: any | undefined) => JSX.Element => typeof children === 'function';
36
+
37
+ export const isReactNodeFunction = (
38
+ children: React.ReactNode | ((props: any | undefined) => React.ReactNode)
39
+ ): children is (props: any | undefined) => React.ReactNode => typeof children === 'function';
@@ -0,0 +1,66 @@
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
+ // eslint-disable-next-line @nx/enforce-module-boundaries
7
+ import { server } from '@availity/mock/src/lib/server';
8
+
9
+ window.open = jest.fn();
10
+
11
+ const queryClient = new QueryClient();
12
+
13
+ const DisclaimerModal = () => (
14
+ <QueryClientProvider client={queryClient}>
15
+ <Spaces
16
+ spaces={[
17
+ {
18
+ id: '1',
19
+ configurationId: 'space1',
20
+ name: 'Some Application',
21
+ type: 'space',
22
+ meta: { disclaimerId: '1234' },
23
+ link: {
24
+ url: '/some-url',
25
+ target: 'newBody',
26
+ },
27
+ },
28
+ ]}
29
+ clientId="my-client-id"
30
+ >
31
+ <SpacesLink
32
+ spaceId="1"
33
+ clientId="my-client-id"
34
+ linkAttributes={{
35
+ spaceId: '1',
36
+ }}
37
+ />
38
+ </Spaces>
39
+ </QueryClientProvider>
40
+ );
41
+
42
+ describe('DisclaimerModal', () => {
43
+ beforeAll(() => {
44
+ // Start the interception.
45
+ server.listen();
46
+ });
47
+ afterEach(() => {
48
+ jest.clearAllMocks();
49
+ server.resetHandlers();
50
+ });
51
+
52
+ it('renders modal when space metadata contains disclaimerId', async () => {
53
+ const { getByText } = render(<DisclaimerModal />);
54
+
55
+ const link = await waitFor(() => getByText('Some Application'));
56
+ fireEvent.click(link);
57
+
58
+ const disclaimerText = await waitFor(() => getByText('hello world'));
59
+ expect(disclaimerText).toBeDefined();
60
+
61
+ const submitBtn = await waitFor(() => getByText('Accept'));
62
+ fireEvent.click(submitBtn);
63
+
64
+ expect(window.open).toHaveBeenCalledTimes(1);
65
+ });
66
+ });
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { avWebQLApi } from '@availity/api-axios';
3
+ import ReactMarkdown from 'react-markdown';
4
+ import { CircularProgress } from '@availity/mui-progress';
5
+ import { DialogContent } from '@availity/mui-dialog';
6
+ import type { ModalProps } from './modal-types';
7
+
8
+ const disclaimerQuery = `query disclaimerFindOne($id: ID!) {
9
+ configurationFindOne(id: $id) {
10
+ ... on Text {
11
+ description
12
+ }
13
+ }
14
+ }`;
15
+
16
+ export const DisclaimerModal = ({ disclaimerId }: ModalProps) => {
17
+ const [disclaimer, setDisclaimer] = useState('');
18
+
19
+ useEffect(() => {
20
+ const fetchDisclaimer = async () => {
21
+ if (disclaimerId) {
22
+ const result = await avWebQLApi.create({ query: disclaimerQuery, variables: { id: disclaimerId } });
23
+
24
+ setDisclaimer(result.data.data.configurationFindOne.description);
25
+ }
26
+ };
27
+
28
+ fetchDisclaimer();
29
+ }, [disclaimerId]);
30
+
31
+ return (
32
+ <DialogContent>{disclaimer ? <ReactMarkdown>{disclaimer}</ReactMarkdown> : <CircularProgress />}</DialogContent>
33
+ );
34
+ };