@squiz/resource-browser 1.68.1 → 1.69.1

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 (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/Hooks/usePreselectedResourcePath.js +8 -3
  3. package/lib/Hooks/useRecentLocations.d.ts +3 -8
  4. package/lib/Hooks/useRecentLocations.js +5 -1
  5. package/lib/Hooks/useRecentResourcesPaths.d.ts +20 -0
  6. package/lib/Hooks/useRecentResourcesPaths.js +30 -0
  7. package/lib/Hooks/useResource.d.ts +13 -0
  8. package/lib/Hooks/useResource.js +12 -1
  9. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +62 -21
  10. package/lib/SourceDropdown/SourceDropdown.d.ts +3 -1
  11. package/lib/SourceDropdown/SourceDropdown.js +24 -22
  12. package/lib/SourceList/SourceList.d.ts +5 -1
  13. package/lib/SourceList/SourceList.js +19 -14
  14. package/lib/index.css +3 -0
  15. package/package.json +1 -1
  16. package/src/Hooks/usePreselectedResourcePath.ts +9 -5
  17. package/src/Hooks/useRecentLocations.spec.ts +36 -40
  18. package/src/Hooks/useRecentLocations.ts +10 -11
  19. package/src/Hooks/useRecentResourcesPaths.ts +54 -0
  20. package/src/Hooks/useResource.spec.ts +30 -1
  21. package/src/Hooks/useResource.ts +19 -0
  22. package/src/ResourcePicker/ResourcePicker.spec.tsx +18 -0
  23. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +63 -21
  24. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +12 -1
  25. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +79 -30
  26. package/src/SourceDropdown/SourceDropdown.spec.tsx +92 -27
  27. package/src/SourceDropdown/SourceDropdown.tsx +33 -29
  28. package/src/SourceList/SourceList.spec.tsx +133 -71
  29. package/src/SourceList/SourceList.stories.tsx +14 -6
  30. package/src/SourceList/SourceList.tsx +55 -29
  31. package/src/SourceList/sample-sources.json +34 -2
  32. package/src/__mocks__/StorybookHelpers.ts +30 -1
  33. package/src/index.stories.tsx +8 -2
@@ -2,35 +2,39 @@
2
2
  import React from 'react';
3
3
  import { screen, render, waitFor, within } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
- import { mockSource } from '../__mocks__/MockModels';
5
+ import { mockResource, mockSource } from '../__mocks__/MockModels';
6
6
  import { useOverlayTriggerState, OverlayTriggerState } from 'react-stately';
7
7
  import SourceList from './SourceList';
8
8
  import { Source } from '../types';
9
+ import { RecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
9
10
 
10
- const mockLocalStorageData = [
11
+ const mockRecentSources: RecentResourcesPaths[] = [
11
12
  {
12
- path: [],
13
- source: {
13
+ source: mockSource({
14
14
  id: '1',
15
- name: 'Test source',
16
- nodes: [],
17
- },
18
- rootNode: {
19
- childCount: 0,
20
- id: '1',
21
- lineages: [],
22
- name: 'Test resource',
23
- status: {
24
- code: 'live',
25
- name: 'Live',
26
- },
27
- type: {
28
- code: 'folder',
29
- name: 'Folder',
30
- },
31
- url: 'https://no-where.com',
32
- urls: [],
33
- },
15
+ name: 'Source 1',
16
+ nodes: [
17
+ {
18
+ id: '1',
19
+ type: {
20
+ code: 'site',
21
+ name: 'Site',
22
+ },
23
+ name: 'Node 1',
24
+ childCount: 21,
25
+ },
26
+ {
27
+ id: '2',
28
+ type: {
29
+ code: 'site',
30
+ name: 'Site',
31
+ },
32
+ name: 'Node 2',
33
+ childCount: 13,
34
+ },
35
+ ],
36
+ }),
37
+ path: [mockResource()],
34
38
  },
35
39
  ];
36
40
 
@@ -107,9 +111,11 @@ describe('SourceList', () => {
107
111
  previewModalState={previewModalState}
108
112
  isLoading={true}
109
113
  onSourceSelect={() => {}}
114
+ onSourceDrilldown={() => {}}
110
115
  error={null}
111
116
  handleReload={reload}
112
117
  setSource={() => {}}
118
+ recentSources={mockRecentSources}
113
119
  />
114
120
  );
115
121
  }}
@@ -133,9 +139,11 @@ describe('SourceList', () => {
133
139
  previewModalState={previewModalState}
134
140
  isLoading={false}
135
141
  onSourceSelect={() => {}}
142
+ onSourceDrilldown={() => {}}
136
143
  error={null}
137
144
  handleReload={reload}
138
145
  setSource={() => {}}
146
+ recentSources={mockRecentSources}
139
147
  />
140
148
  );
141
149
  }}
@@ -160,9 +168,11 @@ describe('SourceList', () => {
160
168
  previewModalState={previewModalState}
161
169
  isLoading={false}
162
170
  onSourceSelect={() => {}}
171
+ onSourceDrilldown={() => {}}
163
172
  error={null}
164
173
  handleReload={reload}
165
174
  setSource={() => {}}
175
+ recentSources={mockRecentSources}
166
176
  />
167
177
  );
168
178
  }}
@@ -186,9 +196,11 @@ describe('SourceList', () => {
186
196
  previewModalState={previewModalState}
187
197
  isLoading={false}
188
198
  onSourceSelect={() => {}}
199
+ onSourceDrilldown={() => {}}
189
200
  error={null}
190
201
  handleReload={reload}
191
202
  setSource={() => {}}
203
+ recentSources={mockRecentSources}
192
204
  />
193
205
  );
194
206
  }}
@@ -219,9 +231,11 @@ describe('SourceList', () => {
219
231
  previewModalState={previewModalState}
220
232
  isLoading={false}
221
233
  onSourceSelect={onSourceSelect}
234
+ onSourceDrilldown={() => {}}
222
235
  error={null}
223
236
  handleReload={reload}
224
237
  setSource={() => {}}
238
+ recentSources={mockRecentSources}
225
239
  />
226
240
  );
227
241
  }}
@@ -229,7 +243,7 @@ describe('SourceList', () => {
229
243
  );
230
244
 
231
245
  const user = userEvent.setup();
232
- const itemButton = screen.getByRole('button', { name: 'Drill down to Node 1 children' });
246
+ const itemButton = screen.getByRole('button', { name: 'site Node 1' });
233
247
  user.click(itemButton);
234
248
 
235
249
  await waitFor(() => {
@@ -244,10 +258,11 @@ describe('SourceList', () => {
244
258
  });
245
259
  });
246
260
 
247
- it('Renders error state when an error occurs loading source list', async () => {
261
+ it('Clicking node child count button triggers correct onSourceDrilldown', async () => {
262
+ const onSourceDrilldown = jest.fn();
248
263
  const reload = jest.fn();
249
264
 
250
- const { getByText } = render(
265
+ render(
251
266
  <SourceListTestWrapper
252
267
  constructFunction={(previewModalState) => {
253
268
  return (
@@ -256,28 +271,34 @@ describe('SourceList', () => {
256
271
  previewModalState={previewModalState}
257
272
  isLoading={false}
258
273
  onSourceSelect={() => {}}
259
- error={new Error('Source list error!')}
274
+ onSourceDrilldown={onSourceDrilldown}
275
+ error={null}
260
276
  handleReload={reload}
261
277
  setSource={() => {}}
278
+ recentSources={mockRecentSources}
262
279
  />
263
280
  );
264
281
  }}
265
282
  />,
266
283
  );
267
284
 
285
+ const user = userEvent.setup();
286
+ const itemButton = screen.getByRole('button', { name: 'Drill down to Node 1 children' });
287
+ user.click(itemButton);
288
+
268
289
  await waitFor(() => {
269
- const errorMessage = getByText('Source list error!');
270
- expect(errorMessage).toBeInTheDocument();
290
+ // Provides the item that was clicked and an id reference to the button that was clicked
291
+ expect(onSourceDrilldown).toHaveBeenCalledWith({
292
+ source: sources[0],
293
+ resource: sources[0].nodes[0],
294
+ });
271
295
  });
272
296
  });
273
297
 
274
- it('Renders recent locations section when local storage has data present', async () => {
298
+ it('Renders error state when an error occurs loading source list', async () => {
275
299
  const reload = jest.fn();
276
- const setSource = jest.fn();
277
-
278
- localStorage.setItem('rb_recent_locations', JSON.stringify(mockLocalStorageData));
279
300
 
280
- render(
301
+ const { getByText } = render(
281
302
  <SourceListTestWrapper
282
303
  constructFunction={(previewModalState) => {
283
304
  return (
@@ -286,9 +307,11 @@ describe('SourceList', () => {
286
307
  previewModalState={previewModalState}
287
308
  isLoading={false}
288
309
  onSourceSelect={() => {}}
289
- error={null}
310
+ onSourceDrilldown={() => {}}
311
+ error={new Error('Source list error!')}
290
312
  handleReload={reload}
291
- setSource={setSource}
313
+ setSource={() => {}}
314
+ recentSources={mockRecentSources}
292
315
  />
293
316
  );
294
317
  }}
@@ -296,17 +319,15 @@ describe('SourceList', () => {
296
319
  );
297
320
 
298
321
  await waitFor(() => {
299
- expect(screen.getByText('Recent locations')).toBeInTheDocument();
300
- expect(screen.getByText('Test resource')).toBeInTheDocument();
322
+ const errorMessage = getByText('Source list error!');
323
+ expect(errorMessage).toBeInTheDocument();
301
324
  });
302
325
  });
303
326
 
304
- it('Handles setSource on click of recent locations item', async () => {
327
+ it('Renders recent locations section when local storage has data present', async () => {
305
328
  const reload = jest.fn();
306
329
  const setSource = jest.fn();
307
330
 
308
- localStorage.setItem('rb_recent_locations', JSON.stringify(mockLocalStorageData));
309
-
310
331
  render(
311
332
  <SourceListTestWrapper
312
333
  constructFunction={(previewModalState) => {
@@ -316,50 +337,27 @@ describe('SourceList', () => {
316
337
  previewModalState={previewModalState}
317
338
  isLoading={false}
318
339
  onSourceSelect={() => {}}
340
+ onSourceDrilldown={() => {}}
319
341
  error={null}
320
342
  handleReload={reload}
321
343
  setSource={setSource}
344
+ recentSources={mockRecentSources}
322
345
  />
323
346
  );
324
347
  }}
325
348
  />,
326
349
  );
327
350
 
328
- const user = userEvent.setup();
329
- const itemButton = screen.getByRole('button', { name: 'Drill down to Test resource children' });
330
- user.click(itemButton);
331
-
332
351
  await waitFor(() => {
333
- // Provides the item that was clicked and an id reference to the button that was clicked
334
- expect(setSource).toHaveBeenCalledWith(
335
- {
336
- source: mockLocalStorageData[0].source,
337
- resource: mockLocalStorageData[0].rootNode,
338
- },
339
- mockLocalStorageData[0].path,
340
- );
352
+ expect(screen.getByText('Recent locations')).toBeInTheDocument();
353
+ expect(screen.getByText('Test resource')).toBeInTheDocument();
341
354
  });
342
355
  });
343
356
 
344
- it('Uses source name if resource name is not available', async () => {
357
+ it('Handles setSource on click of recent locations item', async () => {
345
358
  const reload = jest.fn();
346
359
  const setSource = jest.fn();
347
360
 
348
- localStorage.setItem(
349
- 'rb_recent_locations',
350
- JSON.stringify([
351
- {
352
- path: [],
353
- source: {
354
- id: '1',
355
- name: 'Test source',
356
- nodes: [],
357
- },
358
- rootNode: null,
359
- },
360
- ]),
361
- );
362
-
363
361
  render(
364
362
  <SourceListTestWrapper
365
363
  constructFunction={(previewModalState) => {
@@ -369,18 +367,82 @@ describe('SourceList', () => {
369
367
  previewModalState={previewModalState}
370
368
  isLoading={false}
371
369
  onSourceSelect={() => {}}
370
+ onSourceDrilldown={() => {}}
372
371
  error={null}
373
372
  handleReload={reload}
374
373
  setSource={setSource}
374
+ recentSources={mockRecentSources}
375
375
  />
376
376
  );
377
377
  }}
378
378
  />,
379
379
  );
380
380
 
381
+ const user = userEvent.setup();
382
+ const itemButton = screen.getByRole('button', { name: 'Drill down to Test resource children' });
383
+ user.click(itemButton);
384
+
381
385
  await waitFor(() => {
382
- expect(screen.getByText('Recent locations')).toBeInTheDocument();
383
- expect(screen.getByText('Test source')).toBeInTheDocument();
386
+ // Provides the item that was clicked and an id reference to the button that was clicked
387
+ expect(setSource).toHaveBeenCalledWith(
388
+ {
389
+ resource: {
390
+ childCount: 0,
391
+ id: '1',
392
+ lineages: [],
393
+ name: 'Test resource',
394
+ status: {
395
+ code: 'live',
396
+ name: 'Live',
397
+ },
398
+ type: {
399
+ code: 'folder',
400
+ name: 'Folder',
401
+ },
402
+ url: 'https://no-where.com',
403
+ urls: [],
404
+ },
405
+ source: {
406
+ id: '1',
407
+ name: 'Source 1',
408
+ nodes: [
409
+ {
410
+ childCount: 21,
411
+ id: '1',
412
+ lineages: [],
413
+ name: 'Node 1',
414
+ status: {
415
+ code: 'live',
416
+ name: 'Live',
417
+ },
418
+ type: {
419
+ code: 'site',
420
+ name: 'Site',
421
+ },
422
+ url: 'https://no-where.com',
423
+ urls: [],
424
+ },
425
+ {
426
+ childCount: 13,
427
+ id: '2',
428
+ lineages: [],
429
+ name: 'Node 2',
430
+ status: {
431
+ code: 'live',
432
+ name: 'Live',
433
+ },
434
+ type: {
435
+ code: 'site',
436
+ name: 'Site',
437
+ },
438
+ url: 'https://no-where.com',
439
+ urls: [],
440
+ },
441
+ ],
442
+ },
443
+ },
444
+ [],
445
+ );
384
446
  });
385
447
  });
386
448
  });
@@ -10,7 +10,7 @@ export default {
10
10
  component: SourceList,
11
11
  } as Meta<typeof SourceList>;
12
12
 
13
- const Template: StoryFn<typeof SourceList> = ({ sources, isLoading, allowedTypes }) => {
13
+ const Template: StoryFn<typeof SourceList> = ({ sources, isLoading }) => {
14
14
  const previewModalState = useOverlayTriggerState({});
15
15
 
16
16
  return (
@@ -18,9 +18,19 @@ const Template: StoryFn<typeof SourceList> = ({ sources, isLoading, allowedTypes
18
18
  sources={sources}
19
19
  previewModalState={previewModalState}
20
20
  isLoading={isLoading}
21
- onSourceSelect={({ source, id }) => alert(`Source Select: ${source} - ${id}`)}
22
- onSourceDrillDown={({ source, id }) => alert(`Child Drill Down: ${source} - ${id}`)}
23
- allowedTypes={allowedTypes}
21
+ onSourceSelect={({ source, resource }) =>
22
+ alert(`Resource Select: ${source.name} - ${resource?.name}[${resource?.id}]`)
23
+ }
24
+ onSourceDrilldown={({ source, resource }) =>
25
+ alert(`Child Drill Down: ${source.name} - ${resource?.name}[${resource?.id}]`)
26
+ }
27
+ handleReload={() => {
28
+ console.log('Handle reload');
29
+ }}
30
+ setSource={() => {
31
+ alert(`Recent Location Selected`);
32
+ }}
33
+ error={null}
24
34
  />
25
35
  );
26
36
  };
@@ -29,12 +39,10 @@ export const Primary = Template.bind({});
29
39
  Primary.args = {
30
40
  sources: sampleSources,
31
41
  isLoading: false,
32
- allowedTypes: ['site', 'image'],
33
42
  };
34
43
 
35
44
  export const Loading = Template.bind({});
36
45
  Loading.args = {
37
46
  ...Primary.args,
38
47
  isLoading: true,
39
- allowedTypes: ['site', 'image'],
40
48
  };
@@ -6,31 +6,37 @@ import clsx from 'clsx';
6
6
 
7
7
  import { Source, ScopedSource, Resource } from '../types';
8
8
  import { useCategorisedSources } from '../Hooks/useCategorisedSources';
9
- import { useRecentLocations } from '../Hooks/useRecentLocations';
10
9
  import { HistoryIcon } from '../Icons/HistoryIcon';
10
+ import { RecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
11
11
 
12
12
  export interface SourceListProps {
13
13
  sources: Source[];
14
+ selectedResource?: Resource | null;
14
15
  previewModalState: OverlayTriggerState;
15
16
  isLoading: boolean;
16
17
  onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
18
+ onSourceDrilldown: (source: ScopedSource) => void;
17
19
  handleReload: () => void;
18
20
  setSource: (source: ScopedSource | null, path?: Resource[]) => void;
21
+ recentSources: RecentResourcesPaths[];
19
22
  error: Error | null;
20
23
  }
21
24
 
22
25
  const SourceList = function ({
23
26
  sources,
27
+ selectedResource,
24
28
  previewModalState,
25
29
  isLoading,
26
30
  onSourceSelect,
31
+ onSourceDrilldown,
27
32
  handleReload,
28
33
  setSource,
34
+ recentSources,
29
35
  error,
30
36
  }: SourceListProps) {
31
37
  const categorisedSources = useCategorisedSources(sources);
32
38
  const listRef = useRef<HTMLUListElement>(null);
33
- const { recentLocations } = useRecentLocations();
39
+ const filteredRecentSources = recentSources.filter((item) => item.path?.length);
34
40
 
35
41
  useEffect(() => {
36
42
  if (listRef.current) {
@@ -58,7 +64,7 @@ const SourceList = function ({
58
64
  >
59
65
  {error && <ResourceState state="error" message={error.message} handleReload={handleReload} />}
60
66
 
61
- {!error && recentLocations.length > 0 && (
67
+ {!error && filteredRecentSources.length > 0 && (
62
68
  <li className={`flex flex-col text-sm font-semibold text-grey-800`}>
63
69
  <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
64
70
  <span className="z-10 bg-gray-100 px-2.5 flex gap-1 items-center">
@@ -67,31 +73,34 @@ const SourceList = function ({
67
73
  </span>
68
74
  </div>
69
75
  <ul aria-label={`recent location nodes`} className="flex flex-col">
70
- {recentLocations.map((item, index) => {
71
- const lastResource = item.path[item.path.length - 1];
72
- return (
73
- <ResourceItem
74
- key={`${index}-${item.source?.id}-${item.rootNode?.id}`}
75
- item={{ source: item.source, resource: item.rootNode }}
76
- label={lastResource?.name || item.rootNode?.name || item?.source.name}
77
- type={lastResource?.type?.code || item.rootNode?.type?.code || 'folder'}
78
- previewModalState={previewModalState}
79
- onSelect={() => {
80
- setSource(
81
- {
82
- source: item.source as Source,
83
- resource: item.rootNode,
84
- },
85
- item.path,
86
- );
87
- }}
88
- className={clsx(
89
- index === 0 && 'rounded-t-lg mt-3',
90
- index === recentLocations.length - 1 && 'rounded-b-lg',
91
- )}
92
- showChevron
93
- />
94
- );
76
+ {filteredRecentSources.map((item, index) => {
77
+ if (item.path) {
78
+ const lastResource = item.path[item.path.length - 1];
79
+ const [rootNode, ...path] = item.path;
80
+ return (
81
+ <ResourceItem
82
+ key={`${index}-${item.source?.id}-${lastResource?.id}`}
83
+ item={{ source: item.source, resource: lastResource }}
84
+ label={lastResource?.name || item.source?.name || ''}
85
+ type={lastResource?.type?.code || 'folder'}
86
+ previewModalState={previewModalState}
87
+ onSelect={() => {
88
+ setSource(
89
+ {
90
+ source: item.source as Source,
91
+ resource: rootNode,
92
+ },
93
+ path,
94
+ );
95
+ }}
96
+ className={clsx(
97
+ index === 0 && 'rounded-t-lg mt-3',
98
+ index === filteredRecentSources.length - 1 && 'rounded-b-lg',
99
+ )}
100
+ showChevron
101
+ />
102
+ );
103
+ }
95
104
  })}
96
105
  </ul>
97
106
  </li>
@@ -103,7 +112,7 @@ const SourceList = function ({
103
112
  <li
104
113
  key={key}
105
114
  className={`flex flex-col text-sm font-semibold text-grey-800 ${
106
- index > 0 || recentLocations.length > 0 ? 'mt-3' : ''
115
+ index > 0 || filteredRecentSources.length > 0 ? 'mt-3' : ''
107
116
  }`}
108
117
  >
109
118
  <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
@@ -112,14 +121,31 @@ const SourceList = function ({
112
121
  {sources.length > 0 && (
113
122
  <ul aria-label={`${label} nodes`} className="flex flex-col">
114
123
  {sources.map(({ source, resource }) => {
124
+ if (!resource || resource.childCount === 0) {
125
+ return (
126
+ <ResourceItem
127
+ key={`${source.id}-${resource?.id}`}
128
+ item={{ source, resource }}
129
+ label={resource?.name || source.name}
130
+ type={resource?.type.code || 'folder'}
131
+ previewModalState={previewModalState}
132
+ onSelect={onSourceDrilldown}
133
+ className="mt-3 rounded-lg"
134
+ showChevron
135
+ />
136
+ );
137
+ }
115
138
  return (
116
139
  <ResourceItem
117
140
  key={`${source.id}-${resource?.id}`}
118
141
  item={{ source, resource }}
142
+ selected={resource?.id == selectedResource?.id && resource != null}
119
143
  label={resource?.name || source.name}
120
144
  type={resource?.type.code || 'folder'}
145
+ childCount={resource?.childCount || undefined}
121
146
  previewModalState={previewModalState}
122
147
  onSelect={onSourceSelect}
148
+ onDrillDown={onSourceDrilldown}
123
149
  className="mt-3 rounded-lg"
124
150
  showChevron
125
151
  />
@@ -41,6 +41,19 @@
41
41
  },
42
42
  "name": "Images",
43
43
  "childCount": 100
44
+ },
45
+ {
46
+ "id": "9999",
47
+ "type": {
48
+ "code": "site",
49
+ "name": "Site"
50
+ },
51
+ "status": {
52
+ "code": "live",
53
+ "name": "Live"
54
+ },
55
+ "name": "Another very unique id site",
56
+ "childCount": 100
44
57
  }
45
58
  ]
46
59
  },
@@ -49,7 +62,7 @@
49
62
  "name": "Acme internal system",
50
63
  "nodes": [
51
64
  {
52
- "id": "30",
65
+ "id": "32",
53
66
  "type": {
54
67
  "code": "site",
55
68
  "name": "Site"
@@ -62,7 +75,7 @@
62
75
  "childCount": 15
63
76
  },
64
77
  {
65
- "id": "1",
78
+ "id": "33",
66
79
  "type": {
67
80
  "code": "site",
68
81
  "name": "Site"
@@ -215,5 +228,24 @@
215
228
  "id": "30",
216
229
  "name": "Unsplash image library 27",
217
230
  "nodes": []
231
+ },
232
+ {
233
+ "id": "31",
234
+ "name": "Acme no children",
235
+ "nodes": [
236
+ {
237
+ "id": "3",
238
+ "type": {
239
+ "code": "site",
240
+ "name": "Site"
241
+ },
242
+ "status": {
243
+ "code": "live",
244
+ "name": "Live"
245
+ },
246
+ "name": "What Children?",
247
+ "childCount": 0
248
+ }
249
+ ]
218
250
  }
219
251
  ]
@@ -9,6 +9,29 @@ type CreateCallbacksProps = Partial<{
9
9
  error?: string;
10
10
  }>;
11
11
 
12
+ const indexSources = (sources: Source[]) => {
13
+ const indexed: Record<string, Resource> = {};
14
+ const pending: Resource[] = [];
15
+
16
+ sources.forEach((source) => {
17
+ if (source && 'nodes' in source) {
18
+ pending.push(...source.nodes);
19
+ }
20
+ });
21
+
22
+ while (pending.length > 0) {
23
+ const resource = pending.shift();
24
+
25
+ if (!resource) {
26
+ continue;
27
+ }
28
+
29
+ indexed[resource.id] = resource;
30
+ }
31
+
32
+ return indexed;
33
+ };
34
+
12
35
  const indexResources = (resources: Resource[]) => {
13
36
  const indexed: Record<string, Resource> = {};
14
37
  const pending = [...resources];
@@ -36,6 +59,7 @@ export const createResourceBrowserCallbacks = ({
36
59
  resourceIsLoading = false,
37
60
  error,
38
61
  }: CreateCallbacksProps = {}) => {
62
+ const indexSourceResources = indexSources(sampleSources as Source[]);
39
63
  const indexedResources = indexResources(sampleResources as Resource[]);
40
64
 
41
65
  return {
@@ -78,7 +102,12 @@ export const createResourceBrowserCallbacks = ({
78
102
  if (error && Math.random() > 0.5) {
79
103
  reject(new Error(error));
80
104
  } else {
81
- resolve(indexedResources[reference.resource]);
105
+ const foundResource = indexedResources[reference.resource];
106
+ const foundSourceResource = indexSourceResources[reference.resource];
107
+ if (foundResource) {
108
+ resolve(foundResource);
109
+ }
110
+ resolve(foundSourceResource);
82
111
  }
83
112
  }, delay);
84
113
  }
@@ -33,8 +33,14 @@ Primary.args = {
33
33
  error: '',
34
34
  };
35
35
 
36
- export const Selected = Template.bind({});
37
- Selected.args = {
36
+ export const SourceSelected = Template.bind({});
37
+ SourceSelected.args = {
38
+ ...Primary.args,
39
+ value: { resource: '9999', source: '1' },
40
+ };
41
+
42
+ export const ResourceSelected = Template.bind({});
43
+ ResourceSelected.args = {
38
44
  ...Primary.args,
39
45
  value: { resource: '33', source: '1' },
40
46
  };