@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
@@ -1,35 +1,8 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
2
  import { useRecentLocations } from './useRecentLocations';
3
3
 
4
- import { mockResource, mockSource } from '../__mocks__/MockModels';
5
-
6
4
  describe('useRecentLocations', () => {
7
- const mockLocalStorageData = [
8
- {
9
- path: [],
10
- source: {
11
- id: '1',
12
- name: 'Test source',
13
- nodes: [],
14
- },
15
- rootNode: {
16
- childCount: 0,
17
- id: '1',
18
- lineages: [],
19
- name: 'Test resource',
20
- status: {
21
- code: 'live',
22
- name: 'Live',
23
- },
24
- type: {
25
- code: 'folder',
26
- name: 'Folder',
27
- },
28
- url: 'https://no-where.com',
29
- urls: [],
30
- },
31
- },
32
- ];
5
+ const mockLocalStorageData = [{ resource: '20', source: '1' }];
33
6
 
34
7
  beforeEach(() => {
35
8
  localStorage.clear();
@@ -45,13 +18,17 @@ describe('useRecentLocations', () => {
45
18
 
46
19
  act(() => {
47
20
  result.current.addRecentLocation({
48
- path: [],
49
- source: mockSource(),
50
- rootNode: mockResource(),
21
+ source: '1',
22
+ resource: '32',
51
23
  });
52
24
  });
53
25
 
54
- expect(result.current.recentLocations).toEqual(mockLocalStorageData);
26
+ expect(result.current.recentLocations).toEqual([
27
+ {
28
+ source: '1',
29
+ resource: '32',
30
+ },
31
+ ]);
55
32
  });
56
33
 
57
34
  it('should not add duplicate recent locations', () => {
@@ -59,20 +36,31 @@ describe('useRecentLocations', () => {
59
36
 
60
37
  act(() => {
61
38
  result.current.addRecentLocation({
62
- path: [],
63
- source: mockSource(),
64
- rootNode: mockResource(),
39
+ source: '1',
40
+ resource: '55',
65
41
  });
42
+ });
43
+
44
+ expect(result.current.recentLocations).toEqual([
45
+ {
46
+ source: '1',
47
+ resource: '55',
48
+ },
49
+ ]);
66
50
 
67
- // Add duplicate
51
+ act(() => {
68
52
  result.current.addRecentLocation({
69
- path: [],
70
- source: mockSource(),
71
- rootNode: mockResource(),
53
+ source: '1',
54
+ resource: '55',
72
55
  });
73
56
  });
74
57
 
75
- expect(result.current.recentLocations).toEqual(mockLocalStorageData);
58
+ expect(result.current.recentLocations).toEqual([
59
+ {
60
+ source: '1',
61
+ resource: '55',
62
+ },
63
+ ]);
76
64
  });
77
65
 
78
66
  it('should load recent locations from local storage on mount', () => {
@@ -82,4 +70,12 @@ describe('useRecentLocations', () => {
82
70
 
83
71
  expect(result.current.recentLocations).toEqual(mockLocalStorageData);
84
72
  });
73
+
74
+ it('should handle local storage recent locations not being in the correct format', () => {
75
+ localStorage.setItem('rb_recent_locations', JSON.stringify({}));
76
+
77
+ const { result } = renderHook(() => useRecentLocations());
78
+
79
+ expect(result.current.recentLocations).toEqual([]);
80
+ });
85
81
  });
@@ -1,14 +1,8 @@
1
1
  import { useState } from 'react';
2
- import { Resource, Source } from '../types';
3
-
4
- export interface RecentLocation {
5
- rootNode: Resource | null;
6
- source: Source;
7
- path: Array<Resource>;
8
- }
2
+ import { ResourceReference } from '../types';
9
3
 
10
4
  export const useRecentLocations = (maxLocations = 3, storageKey = 'rb_recent_locations') => {
11
- let initialRecentLocations = [];
5
+ let initialRecentLocations: Array<ResourceReference> = [];
12
6
 
13
7
  try {
14
8
  initialRecentLocations = JSON.parse(localStorage.getItem(storageKey) ?? '[]');
@@ -21,11 +15,16 @@ export const useRecentLocations = (maxLocations = 3, storageKey = 'rb_recent_loc
21
15
  initialRecentLocations = [];
22
16
  }
23
17
 
24
- const [recentLocations, setRecentLocations] = useState<Array<RecentLocation>>(initialRecentLocations);
18
+ // Check if any item in the current recent locations is not the right format, if so, we reset it
19
+ if (initialRecentLocations.find((item) => !(item?.resource?.length && item?.source?.length))) {
20
+ initialRecentLocations = [];
21
+ }
22
+
23
+ const [recentLocations, setRecentLocations] = useState<Array<ResourceReference>>(initialRecentLocations);
25
24
 
26
- const addRecentLocation = (newLocation: RecentLocation) => {
25
+ const addRecentLocation = (newLocation: ResourceReference) => {
27
26
  // Check if the new location to make sure we don't already have a recent location for this
28
- if (JSON.stringify(recentLocations).indexOf(JSON.stringify(newLocation)) > -1) {
27
+ if (recentLocations.find((item) => item.resource === newLocation.resource && item.source === newLocation.source)) {
29
28
  return;
30
29
  }
31
30
 
@@ -0,0 +1,54 @@
1
+ import { Resource, OnRequestResource, OnRequestSources, Source } from '../types';
2
+ import { useAsync } from '@squiz/generic-browser-lib';
3
+ import { findBestMatchLineage } from '../utils/findBestMatchLineage';
4
+
5
+ export type RecentResourcesPathsProps = {
6
+ sourceIds?: string[];
7
+ resources?: Resource | null | (Resource | null)[];
8
+ onRequestResource: OnRequestResource;
9
+ onRequestSources: OnRequestSources;
10
+ };
11
+
12
+ export type RecentResourcesPaths = {
13
+ source?: Source;
14
+ path?: Resource[];
15
+ };
16
+
17
+ export const useRecentResourcesPaths = ({
18
+ sourceIds,
19
+ resources,
20
+ onRequestResource,
21
+ onRequestSources,
22
+ }: RecentResourcesPathsProps) => {
23
+ const callbackArray = sourceIds?.map((sourceId, index) => async () => {
24
+ let path: Resource[] | undefined;
25
+
26
+ const sources = await onRequestSources();
27
+ const source = sources.find((source) => source.id === sourceId);
28
+ const resource = Array.isArray(resources) ? resources[index] : null;
29
+
30
+ if (sourceId && source && resource) {
31
+ const bestMatchLineage = findBestMatchLineage(source, resource);
32
+
33
+ if (Array.isArray(bestMatchLineage) && bestMatchLineage.length > 0) {
34
+ path = await Promise.all(
35
+ bestMatchLineage.map(async (resourceId) => {
36
+ return onRequestResource({ source: sourceId, resource: resourceId });
37
+ }),
38
+ );
39
+ } else {
40
+ path = [resource];
41
+ }
42
+ }
43
+
44
+ return { source, path };
45
+ });
46
+
47
+ return useAsync(
48
+ {
49
+ callback: callbackArray ? callbackArray : () => null,
50
+ defaultValue: [] as RecentResourcesPaths[],
51
+ },
52
+ [JSON.stringify(sourceIds), resources],
53
+ );
54
+ };
@@ -1,6 +1,6 @@
1
1
  import { renderHook, waitFor } from '@testing-library/react';
2
2
  import { mockResource } from '../__mocks__/MockModels';
3
- import { useResource } from './useResource';
3
+ import { useResource, useResources } from './useResource';
4
4
 
5
5
  describe('useResource', () => {
6
6
  it('Should load the resource', async () => {
@@ -30,3 +30,32 @@ describe('useResource', () => {
30
30
  expect(onRequestResource).not.toBeCalled();
31
31
  });
32
32
  });
33
+
34
+ describe('useResources', () => {
35
+ it('Should load the resources', async () => {
36
+ const resources = mockResource();
37
+ const references = [{ source: 'source-id', resource: 'resource-id' }];
38
+ const onRequestResource = jest.fn().mockResolvedValue(resources);
39
+ const { result } = renderHook(() => useResources({ onRequestResource, references }));
40
+
41
+ expect(result.current.isLoading).toBe(true);
42
+ expect(result.current.error).toBe(null);
43
+ expect(result.current.data).toEqual(null);
44
+
45
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
46
+
47
+ expect(result.current.isLoading).toBe(false);
48
+ expect(result.current.data).toStrictEqual([resources]);
49
+ });
50
+
51
+ it('Should not load the resources if no references are provided', async () => {
52
+ const references = null;
53
+ const onRequestResource = jest.fn();
54
+ const { result } = renderHook(() => useResources({ onRequestResource, references }));
55
+
56
+ expect(result.current.isLoading).toBe(false);
57
+ expect(result.current.error).toBe(null);
58
+ expect(result.current.data).toEqual(null);
59
+ expect(onRequestResource).not.toBeCalled();
60
+ });
61
+ });
@@ -6,6 +6,11 @@ type UseResourceProps = {
6
6
  reference?: ResourceReference | null;
7
7
  };
8
8
 
9
+ type UseResourcesProps = {
10
+ onRequestResource: (reference: ResourceReference) => Promise<Resource | null>;
11
+ references?: ResourceReference[] | null;
12
+ };
13
+
9
14
  /**
10
15
  * Loads the resource indicated by the provided reference.
11
16
  */
@@ -17,3 +22,17 @@ export const useResource = ({ onRequestResource, reference }: UseResourceProps)
17
22
  },
18
23
  [reference?.source, reference?.resource],
19
24
  );
25
+
26
+ /**
27
+ * Loads the resources indicated by the provided reference.
28
+ */
29
+ export const useResources = ({ onRequestResource, references }: UseResourcesProps) => {
30
+ const callbackArray = references?.map((item) => () => onRequestResource(item));
31
+ return useAsync(
32
+ {
33
+ callback: callbackArray ? callbackArray : () => null,
34
+ defaultValue: null,
35
+ },
36
+ [],
37
+ );
38
+ };
@@ -102,4 +102,22 @@ describe('Resource picker', () => {
102
102
  expect(pickerLabel).toBeInTheDocument();
103
103
  expect(removeButton).not.toBeInTheDocument();
104
104
  });
105
+
106
+ it('should display a default status if an unsupported status is found for resource', () => {
107
+ render(
108
+ <ResourcePicker
109
+ {...defaultProps}
110
+ resource={{
111
+ ...mockResource,
112
+ status: {
113
+ code: 'this_is_not_real',
114
+ name: 'This is not real',
115
+ },
116
+ }}
117
+ />,
118
+ );
119
+
120
+ const item = screen.getByTitle('This is not real');
121
+ expect(item?.style.backgroundColor).toEqual('rgb(255, 0, 0)');
122
+ });
105
123
  });
@@ -71,6 +71,16 @@ const baseProps = {
71
71
  };
72
72
 
73
73
  describe('ResourcePickerContainer', () => {
74
+ beforeEach(() => {
75
+ localStorage.setItem(
76
+ 'rb_recent_locations',
77
+ JSON.stringify([
78
+ { resource: '32', source: '1' },
79
+ { resource: '20', source: '1' },
80
+ ]),
81
+ );
82
+ });
83
+
74
84
  it('Queries onRequestSources for source list on startup', async () => {
75
85
  const onRequestSources = jest.fn(() => {
76
86
  return Promise.resolve([]);
@@ -209,6 +219,8 @@ describe('ResourcePickerContainer', () => {
209
219
  ['the preselected resource lineage does not exist under a root node', 100],
210
220
  ['the preselected resource lineage does not appear under a root node', 200],
211
221
  ])('The source list is displayed if %s', async (description: string, preselectedResourceId: number) => {
222
+ localStorage.clear();
223
+
212
224
  const resources: Record<string, Resource> = {
213
225
  10: mockResource({
214
226
  id: '100',
@@ -256,8 +268,10 @@ describe('ResourcePickerContainer', () => {
256
268
  // Breadcrumbs should not be displayed.
257
269
  // Source list should be displayed.
258
270
  // "Leaf" resource should be selected, "Another leaf" resource should not be selected.
259
- expect(screen.queryByLabelText('Resource breadcrumb')).not.toBeInTheDocument();
260
- expect(screen.getByRole('button', { name: 'Drill down to Source root node #1 children' })).toBeInTheDocument();
271
+ await waitFor(() => expect(screen.queryByLabelText('Resource breadcrumb')).not.toBeInTheDocument());
272
+ await waitFor(() =>
273
+ expect(screen.getByRole('button', { name: 'Drill down to Source root node #1 children' })).toBeInTheDocument(),
274
+ );
261
275
  });
262
276
 
263
277
  it('Selecting a child count drills down', async () => {
@@ -708,31 +722,59 @@ describe('ResourcePickerContainer', () => {
708
722
  ]);
709
723
  });
710
724
 
711
- it('handleDetailSelect() works', async () => {
712
- const onChangeMock = jest.fn();
713
- const onCloseMock = jest.fn();
714
- const { getAllByText } = render(
715
- <ResourcePickerContainer {...baseProps} onChange={onChangeMock} onClose={onCloseMock} />,
716
- );
725
+ describe('handleDetailSelect() tests', () => {
726
+ it('Source select works', async () => {
727
+ const onChangeMock = jest.fn();
728
+ const onCloseMock = jest.fn();
729
+ const { getAllByText } = render(
730
+ <ResourcePickerContainer {...baseProps} onChange={onChangeMock} onClose={onCloseMock} />,
731
+ );
717
732
 
718
- await waitFor(() => {
719
- expect(getAllByText('Test system')[0]).toBeInTheDocument();
733
+ await waitFor(() => {
734
+ expect(getAllByText('Test system')[0]).toBeInTheDocument();
735
+ });
736
+
737
+ const user = userEvent.setup();
738
+
739
+ // Select the resource
740
+ user.click(screen.getByRole('button', { name: 'site Test Website' }));
741
+
742
+ // Wait for the preview panel to open
743
+ await waitFor(() => expect(screen.getByText('Site')).toBeInTheDocument());
744
+ await waitFor(() => expect(screen.getByText('#1')).toBeInTheDocument());
745
+
746
+ user.click(screen.getByRole('button', { name: 'Select' }));
747
+
748
+ await waitFor(() => expect(onChangeMock).toHaveBeenCalled());
749
+ await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
720
750
  });
721
751
 
722
- const user = userEvent.setup();
723
- user.click(screen.getByRole('button', { name: 'Drill down to Test Website children' }));
724
- await waitFor(() => expect(screen.getByRole('button', { name: 'page Test Page' })).toBeInTheDocument());
752
+ it('Resource select works', async () => {
753
+ const onChangeMock = jest.fn();
754
+ const onCloseMock = jest.fn();
755
+ const { getAllByText } = render(
756
+ <ResourcePickerContainer {...baseProps} onChange={onChangeMock} onClose={onCloseMock} />,
757
+ );
725
758
 
726
- // Select the resource
727
- user.click(screen.getByRole('button', { name: 'page Test Page' }));
759
+ await waitFor(() => {
760
+ expect(getAllByText('Test system')[0]).toBeInTheDocument();
761
+ });
728
762
 
729
- // Wait for the preview panel to open
730
- await waitFor(() => expect(screen.getByText('Mocked Page')).toBeInTheDocument());
731
- await waitFor(() => expect(screen.getByText('#123')).toBeInTheDocument());
763
+ const user = userEvent.setup();
764
+ user.click(screen.getByRole('button', { name: 'Drill down to Test Website children' }));
765
+ await waitFor(() => expect(screen.getByRole('button', { name: 'page Test Page' })).toBeInTheDocument());
732
766
 
733
- user.click(screen.getByRole('button', { name: 'Select' }));
767
+ // Select the resource
768
+ user.click(screen.getByRole('button', { name: 'page Test Page' }));
769
+
770
+ // Wait for the preview panel to open
771
+ await waitFor(() => expect(screen.getByText('Mocked Page')).toBeInTheDocument());
772
+ await waitFor(() => expect(screen.getByText('#123')).toBeInTheDocument());
734
773
 
735
- await waitFor(() => expect(onChangeMock).toHaveBeenCalled());
736
- await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
774
+ user.click(screen.getByRole('button', { name: 'Select' }));
775
+
776
+ await waitFor(() => expect(onChangeMock).toHaveBeenCalled());
777
+ await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
778
+ });
737
779
  });
738
780
  });
@@ -2,13 +2,14 @@ import React from 'react';
2
2
  import { StoryFn, Meta } from '@storybook/react';
3
3
  import ResourcePickerContainer from './ResourcePickerContainer';
4
4
  import { createResourceBrowserCallbacks } from '../__mocks__/StorybookHelpers';
5
+ import sampleSources from '../SourceList/sample-sources.json';
5
6
 
6
7
  export default {
7
8
  title: 'Resource Picker container',
8
9
  component: ResourcePickerContainer,
9
10
  } as Meta<typeof ResourcePickerContainer>;
10
11
 
11
- const Template: StoryFn<typeof ResourcePickerContainer> = ({ title }) => {
12
+ const Template: StoryFn<typeof ResourcePickerContainer> = ({ title, preselectedSourceId, preselectedResource }) => {
12
13
  const { onRequestSources, onRequestChildren, onRequestResource, onChange } = createResourceBrowserCallbacks();
13
14
 
14
15
  return (
@@ -19,6 +20,8 @@ const Template: StoryFn<typeof ResourcePickerContainer> = ({ title }) => {
19
20
  onRequestSources={onRequestSources}
20
21
  onRequestChildren={onRequestChildren}
21
22
  onRequestResource={onRequestResource}
23
+ preselectedSourceId={preselectedSourceId}
24
+ preselectedResource={preselectedResource}
22
25
  onChange={onChange}
23
26
  onClose={() => {
24
27
  alert('Resource Browser closed');
@@ -32,3 +35,11 @@ export const Primary = Template.bind({});
32
35
  Primary.args = {
33
36
  title: 'Asset Picker',
34
37
  };
38
+
39
+ export const SourceListResourceSelected = Template.bind({});
40
+ // SourceList root node is selected
41
+ SourceListResourceSelected.args = {
42
+ title: 'Asset Picker',
43
+ preselectedSourceId: sampleSources[0].id,
44
+ preselectedResource: sampleSources[0].nodes[1],
45
+ };
@@ -20,6 +20,8 @@ import { useChildResources } from '../Hooks/useChildResources';
20
20
  import { useSources } from '../Hooks/useSources';
21
21
  import { usePreselectedResourcePath } from '../Hooks/usePreselectedResourcePath';
22
22
  import { useRecentLocations } from '../Hooks/useRecentLocations';
23
+ import { useResources } from '../Hooks/useResource';
24
+ import { RecentResourcesPaths, useRecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
23
25
 
24
26
  interface ResourcePickerContainerProps {
25
27
  title: string;
@@ -47,10 +49,29 @@ function ResourcePickerContainer({
47
49
  preselectedResource,
48
50
  }: ResourcePickerContainerProps) {
49
51
  const previewModalState = useOverlayTriggerState({});
52
+ const [selectedSource, setSelectedSource] = useState<Source | null>(null);
50
53
  const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
51
54
  const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes>({});
52
55
  const { source, currentResource, hierarchy, setSource, push, popUntil } = useResourcePath();
53
- const { addRecentLocation } = useRecentLocations();
56
+
57
+ // Recent locations relevant data
58
+ const { addRecentLocation, recentLocations } = useRecentLocations();
59
+
60
+ const { data: recentLocationsResources, isLoading: recentLocationsResourcesLoading } = useResources({
61
+ onRequestResource,
62
+ references: recentLocations,
63
+ });
64
+
65
+ const { data: recentLocationsSources, isLoading: recentLocationsLoading } = useRecentResourcesPaths({
66
+ sourceIds: recentLocations.map((item) => item.source),
67
+ resources: recentLocationsResources,
68
+ onRequestResource,
69
+ onRequestSources,
70
+ });
71
+
72
+ // Type check the returned values from recent locations requests
73
+ let recentSources: RecentResourcesPaths[] = [];
74
+ if (Array.isArray(recentLocationsSources)) recentSources = recentLocationsSources;
54
75
 
55
76
  const {
56
77
  data: sources,
@@ -58,12 +79,14 @@ function ResourcePickerContainer({
58
79
  reload: handleSourceReload,
59
80
  error: sourceError,
60
81
  } = useSources({ onRequestSources });
82
+
61
83
  const {
62
84
  data: resources,
63
85
  isLoading: isResourcesLoading,
64
86
  reload: handleResourceReload,
65
87
  error: resourceError,
66
88
  } = useChildResources({ source, currentResource, onRequestChildren });
89
+
67
90
  const {
68
91
  data: { source: preselectedSource, path: preselectedPath },
69
92
  isLoading: isPreselectedResourcePathLoading,
@@ -73,10 +96,13 @@ function ResourcePickerContainer({
73
96
  onRequestResource,
74
97
  onRequestSources,
75
98
  });
76
- const selectedResource = useMemo(
77
- () => resources.find((resource) => resource.id === selectedResourceId) || null,
78
- [selectedResourceId, resources],
79
- );
99
+
100
+ const selectedResource = useMemo(() => {
101
+ if (selectedSource) {
102
+ return selectedSource?.nodes.find((resource: Resource) => resource.id === selectedResourceId) || null;
103
+ }
104
+ return resources.find((resource: Resource) => resource.id === selectedResourceId) || null;
105
+ }, [selectedResourceId, resources, selectedSource]);
80
106
 
81
107
  const handleResourceDrillDown = useCallback(
82
108
  (resource: Resource) => {
@@ -87,45 +113,55 @@ function ResourcePickerContainer({
87
113
 
88
114
  const handleResourceSelected = useCallback((resource: Resource, overlayProps: DOMAttributes) => {
89
115
  setPreviewModalOverlayProps(overlayProps);
116
+ setSelectedSource(null);
90
117
  setSelectedResourceId(resource.id);
91
118
  }, []);
92
119
 
93
120
  const handleSourceDrilldown = useCallback(
94
121
  (source: ScopedSource) => {
122
+ setSelectedSource(null);
123
+ setSelectedResourceId(null);
95
124
  setSource(source);
96
125
  },
97
126
  [setSource],
98
127
  );
99
128
 
129
+ const handleSourceSelected = useCallback((node: ScopedSource, overlayProps: DOMAttributes) => {
130
+ const { source, resource } = node;
131
+ setPreviewModalOverlayProps(overlayProps);
132
+ setSelectedSource(source || null);
133
+ setSelectedResourceId(resource?.id || null);
134
+ }, []);
135
+
100
136
  const handleReturnToRoot = useCallback(() => {
137
+ setSelectedSource(null);
138
+ setSelectedResourceId(null);
101
139
  setSource(null);
102
140
  }, [setSource]);
103
141
 
104
142
  const handleDetailSelect = useCallback(
105
143
  (resource: Resource) => {
106
- onChange({ resource, source: source?.source as Source });
144
+ const detailSelectedSource = selectedSource ?? (source?.source as Source);
145
+ onChange({ resource, source: detailSelectedSource });
107
146
 
108
147
  // Find the path that got them to where they are
109
- const selectedPath = hierarchy.map((path) => {
110
- const pathNode = path.node as ScopedSource;
111
- return 'resource' in pathNode ? pathNode.resource : pathNode;
112
- });
113
-
114
- const [rootNode, ...path] = selectedPath;
148
+ const lastPathItem = hierarchy[hierarchy.length - 1]?.node as ScopedSource;
149
+ const lastPathResource = lastPathItem && 'resource' in lastPathItem ? lastPathItem?.resource : lastPathItem;
115
150
 
116
- // Update the recent locations in local storage
117
- addRecentLocation({
118
- rootNode,
119
- path: path as Resource[],
120
- source: source?.source as Source,
121
- });
151
+ if (lastPathResource) {
152
+ addRecentLocation({
153
+ resource: lastPathResource.id,
154
+ source: detailSelectedSource.id,
155
+ });
156
+ }
122
157
 
123
158
  onClose();
124
159
  },
125
- [source, currentResource],
160
+ [selectedSource, source, currentResource],
126
161
  );
127
162
 
128
163
  const handleDetailClose = useCallback(() => {
164
+ setSelectedSource(null);
129
165
  setSelectedResourceId(null);
130
166
  }, []);
131
167
 
@@ -133,6 +169,7 @@ function ResourcePickerContainer({
133
169
  // (eg. due to navigating up/down the tree).
134
170
  useEffect(() => {
135
171
  if (resources.length > 0 && selectedResourceId && !selectedResource) {
172
+ setSelectedSource(null);
136
173
  setSelectedResourceId(null);
137
174
  }
138
175
  }, [resources, selectedResourceId, selectedResource]);
@@ -142,16 +179,19 @@ function ResourcePickerContainer({
142
179
  const [rootNode, ...path] = preselectedPath;
143
180
  const leaf = path.pop();
144
181
 
145
- setSource(
146
- {
147
- source: preselectedSource,
148
- resource: rootNode,
149
- },
150
- path,
151
- );
152
-
153
182
  if (leaf) {
183
+ setSource(
184
+ {
185
+ source: preselectedSource,
186
+ resource: rootNode,
187
+ },
188
+ path,
189
+ );
190
+
154
191
  setSelectedResourceId(leaf.id);
192
+ } else {
193
+ setSelectedSource(preselectedSource);
194
+ setSelectedResourceId(rootNode.id);
155
195
  }
156
196
  }
157
197
  }, [preselectedSource, preselectedSource]);
@@ -166,11 +206,12 @@ function ResourcePickerContainer({
166
206
  <SourceDropdown
167
207
  sources={sources}
168
208
  selectedSource={source}
169
- isLoading={isSourceLoading}
209
+ isLoading={isSourceLoading || recentLocationsLoading || recentLocationsResourcesLoading}
170
210
  onSourceSelect={handleSourceDrilldown}
171
211
  onRootSelect={handleReturnToRoot}
172
212
  setSource={setSource}
173
213
  currentResource={currentResource}
214
+ recentSources={recentSources}
174
215
  />
175
216
  </div>
176
217
  <button
@@ -200,11 +241,19 @@ function ResourcePickerContainer({
200
241
  {!source && (
201
242
  <SourceList
202
243
  sources={sources}
244
+ selectedResource={selectedResource}
203
245
  previewModalState={previewModalState}
204
- isLoading={isSourceLoading || isPreselectedResourcePathLoading}
205
- onSourceSelect={handleSourceDrilldown}
246
+ isLoading={
247
+ isSourceLoading ||
248
+ isPreselectedResourcePathLoading ||
249
+ recentLocationsLoading ||
250
+ recentLocationsResourcesLoading
251
+ }
252
+ onSourceSelect={handleSourceSelected}
253
+ onSourceDrilldown={handleSourceDrilldown}
206
254
  handleReload={handleSourceReload}
207
255
  setSource={setSource}
256
+ recentSources={recentSources}
208
257
  error={sourceError}
209
258
  />
210
259
  )}