@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.
- package/CHANGELOG.md +12 -0
- package/lib/Hooks/usePreselectedResourcePath.js +8 -3
- package/lib/Hooks/useRecentLocations.d.ts +3 -8
- package/lib/Hooks/useRecentLocations.js +5 -1
- package/lib/Hooks/useRecentResourcesPaths.d.ts +20 -0
- package/lib/Hooks/useRecentResourcesPaths.js +30 -0
- package/lib/Hooks/useResource.d.ts +13 -0
- package/lib/Hooks/useResource.js +12 -1
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +62 -21
- package/lib/SourceDropdown/SourceDropdown.d.ts +3 -1
- package/lib/SourceDropdown/SourceDropdown.js +24 -22
- package/lib/SourceList/SourceList.d.ts +5 -1
- package/lib/SourceList/SourceList.js +19 -14
- package/lib/index.css +3 -0
- package/package.json +1 -1
- package/src/Hooks/usePreselectedResourcePath.ts +9 -5
- package/src/Hooks/useRecentLocations.spec.ts +36 -40
- package/src/Hooks/useRecentLocations.ts +10 -11
- package/src/Hooks/useRecentResourcesPaths.ts +54 -0
- package/src/Hooks/useResource.spec.ts +30 -1
- package/src/Hooks/useResource.ts +19 -0
- package/src/ResourcePicker/ResourcePicker.spec.tsx +18 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +63 -21
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +12 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +79 -30
- package/src/SourceDropdown/SourceDropdown.spec.tsx +92 -27
- package/src/SourceDropdown/SourceDropdown.tsx +33 -29
- package/src/SourceList/SourceList.spec.tsx +133 -71
- package/src/SourceList/SourceList.stories.tsx +14 -6
- package/src/SourceList/SourceList.tsx +55 -29
- package/src/SourceList/sample-sources.json +34 -2
- package/src/__mocks__/StorybookHelpers.ts +30 -1
- 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
|
-
|
49
|
-
|
50
|
-
rootNode: mockResource(),
|
21
|
+
source: '1',
|
22
|
+
resource: '32',
|
51
23
|
});
|
52
24
|
});
|
53
25
|
|
54
|
-
expect(result.current.recentLocations).toEqual(
|
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
|
-
|
63
|
-
|
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
|
-
|
51
|
+
act(() => {
|
68
52
|
result.current.addRecentLocation({
|
69
|
-
|
70
|
-
|
71
|
-
rootNode: mockResource(),
|
53
|
+
source: '1',
|
54
|
+
resource: '55',
|
72
55
|
});
|
73
56
|
});
|
74
57
|
|
75
|
-
expect(result.current.recentLocations).toEqual(
|
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 {
|
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
|
-
|
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:
|
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 (
|
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
|
+
});
|
package/src/Hooks/useResource.ts
CHANGED
@@ -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
|
-
|
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
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
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
|
-
|
719
|
-
|
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
|
-
|
723
|
-
|
724
|
-
|
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
|
-
|
727
|
-
|
759
|
+
await waitFor(() => {
|
760
|
+
expect(getAllByText('Test system')[0]).toBeInTheDocument();
|
761
|
+
});
|
728
762
|
|
729
|
-
|
730
|
-
|
731
|
-
|
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
|
-
|
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
|
-
|
736
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
110
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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={
|
205
|
-
|
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
|
)}
|