@squiz/resource-browser 1.66.3 → 1.67.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.
- package/CHANGELOG.md +13 -0
- package/lib/Hooks/usePreselectedResourcePath.d.ts +20 -0
- package/lib/Hooks/usePreselectedResourcePath.js +26 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Hooks/useResourcePath.js +2 -2
- package/lib/Hooks/useSources.d.ts +14 -0
- package/lib/Hooks/useSources.js +9 -0
- package/lib/Icons/CircledLoopIcon.d.ts +4 -0
- package/lib/Icons/CircledLoopIcon.js +12 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +12 -5
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +52 -2
- package/lib/ResourcePicker/ResourcePicker.js +1 -1
- package/lib/ResourcePicker/States/Selected.d.ts +2 -1
- package/lib/ResourcePicker/States/Selected.js +6 -2
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +7 -4
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +35 -11
- package/lib/SourceDropdown/SourceDropdown.js +1 -1
- package/lib/index.css +19 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -2
- package/lib/types.d.ts +7 -0
- package/lib/utils/findBestMatchLineage.d.ts +2 -0
- package/lib/utils/findBestMatchLineage.js +28 -0
- package/package.json +6 -4
- package/src/Hooks/usePreselectedResourcePath.ts +50 -0
- package/src/Hooks/useResourcePath.ts +2 -2
- package/src/Hooks/useSources.ts +1 -1
- package/src/Icons/CircledLoopIcon.tsx +14 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +93 -3
- package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +56 -0
- package/src/ResourceList/sample-resources.json +684 -439
- package/src/ResourcePicker/ResourcePicker.tsx +8 -1
- package/src/ResourcePicker/States/Selected.tsx +23 -3
- package/src/ResourcePicker/resource-picker.scss +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +146 -32
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +64 -18
- package/src/SourceDropdown/SourceDropdown.tsx +1 -1
- package/src/SourceList/sample-sources.json +4 -4
- package/src/__mocks__/MockModels.ts +1 -0
- package/src/__mocks__/StorybookHelpers.ts +33 -4
- package/src/__mocks__/renderWithContext.tsx +23 -0
- package/src/index.spec.tsx +81 -21
- package/src/index.stories.tsx +4 -4
- package/src/index.tsx +10 -2
- package/src/types.ts +9 -0
- package/src/utils/findBestMatchLineage.spec.ts +81 -0
- package/src/utils/findBestMatchLineage.ts +30 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +0 -20
- /package/lib/{uuid.d.ts → utils/uuid.d.ts} +0 -0
- /package/lib/{uuid.js → utils/uuid.js} +0 -0
- /package/src/{uuid.ts → utils/uuid.ts} +0 -0
@@ -59,7 +59,14 @@ const ResourcePicker = ({
|
|
59
59
|
<div className="resource-picker-info__layout">
|
60
60
|
{isLoading && <LoadingState />}
|
61
61
|
{error && <ErrorState error={error} isDisabled={isDisabled} onClear={onClear} />}
|
62
|
-
{resource &&
|
62
|
+
{resource && (
|
63
|
+
<SelectedState
|
64
|
+
resource={resource}
|
65
|
+
isDisabled={isDisabled}
|
66
|
+
onClear={onClear}
|
67
|
+
resourcePickerContainer={children}
|
68
|
+
/>
|
69
|
+
)}
|
63
70
|
</div>
|
64
71
|
</div>
|
65
72
|
)}
|
@@ -1,24 +1,41 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import prettyBytes from 'pretty-bytes';
|
3
|
-
import { Icon, IconOptions, ResetButton } from '@squiz/generic-browser-lib';
|
3
|
+
import { Icon, IconOptions, ModalTrigger, ResetButton } from '@squiz/generic-browser-lib';
|
4
4
|
|
5
5
|
import { Resource } from '../../types';
|
6
6
|
import StatusIndicator from '../../StatusIndicator/StatusIndicator';
|
7
|
+
import { CircledLoopIcon } from '../../Icons/CircledLoopIcon';
|
7
8
|
|
8
9
|
export type SelectedStateProps = {
|
9
10
|
resource: Resource;
|
10
11
|
isDisabled?: boolean;
|
11
12
|
onClear: () => void;
|
13
|
+
resourcePickerContainer: any;
|
12
14
|
};
|
13
15
|
|
14
16
|
export const SelectedState = ({
|
15
17
|
resource: { id, type, name, status, squizImage, url },
|
16
18
|
isDisabled,
|
17
19
|
onClear,
|
20
|
+
resourcePickerContainer,
|
18
21
|
}: SelectedStateProps) => {
|
19
22
|
const fileSize = squizImage?.imageVariations?.original?.byteSize;
|
20
23
|
const fileWidth = squizImage?.imageVariations?.original?.width;
|
21
24
|
const fileHeight = squizImage?.imageVariations?.original?.height;
|
25
|
+
|
26
|
+
const replaceAsset = (
|
27
|
+
<ModalTrigger
|
28
|
+
showLabel={false}
|
29
|
+
label="Replace selection"
|
30
|
+
containerClasses="text-gray-500 hover:text-gray-800 focus:text-gray-800 disabled:text-gray-500 disabled:cursor-not-allowed"
|
31
|
+
icon={<CircledLoopIcon aria-hidden className="m-1" />}
|
32
|
+
isDisabled={isDisabled}
|
33
|
+
scope="squiz-rb-scope"
|
34
|
+
>
|
35
|
+
{resourcePickerContainer}
|
36
|
+
</ModalTrigger>
|
37
|
+
);
|
38
|
+
|
22
39
|
return (
|
23
40
|
<>
|
24
41
|
{/* Left column */}
|
@@ -29,7 +46,7 @@ export const SelectedState = ({
|
|
29
46
|
) : (
|
30
47
|
<Icon icon={type.code as IconOptions} resourceSource="matrix" className="w-4 h-4 mt-1 flex self-start" />
|
31
48
|
)}
|
32
|
-
{/* Center
|
49
|
+
{/* Center columns */}
|
33
50
|
<div className="justify-self-start self-center w-full overflow-hidden break-words">
|
34
51
|
<span>{name}</span>
|
35
52
|
<dl className="col-start-2 col-end-2 flex flex-row gap-1 justify-self-start items-center font-normal text-sm">
|
@@ -58,7 +75,10 @@ export const SelectedState = ({
|
|
58
75
|
)}
|
59
76
|
</div>
|
60
77
|
{/* End column */}
|
61
|
-
<
|
78
|
+
<div className="flex">
|
79
|
+
{replaceAsset}
|
80
|
+
<ResetButton isDisabled={isDisabled} onClick={onClear} />
|
81
|
+
</div>
|
62
82
|
</>
|
63
83
|
);
|
64
84
|
};
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
3
3
|
import { screen, render, waitFor, within, act } from '@testing-library/react';
|
4
4
|
import userEvent from '@testing-library/user-event';
|
5
5
|
import { mockResource, mockSource } from '../__mocks__/MockModels';
|
6
|
-
import { Resource, Source, Hierarchy } from '../types';
|
6
|
+
import { Resource, Source, Hierarchy, ResourceReference } from '../types';
|
7
7
|
import { Context as ResponsiveContext } from 'react-responsive';
|
8
8
|
import { OverlayTriggerState } from 'react-stately';
|
9
9
|
|
@@ -38,39 +38,36 @@ const baseProps = {
|
|
38
38
|
titleAriaProps: {},
|
39
39
|
allowedTypes: undefined,
|
40
40
|
onClose: jest.fn(),
|
41
|
-
onRequestSources: ()
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
name: 'Site',
|
52
|
-
},
|
53
|
-
name: 'Test Website',
|
54
|
-
childCount: 21,
|
41
|
+
onRequestSources: jest.fn().mockResolvedValue([
|
42
|
+
mockSource({
|
43
|
+
id: '1',
|
44
|
+
name: 'Test system',
|
45
|
+
nodes: [
|
46
|
+
{
|
47
|
+
id: '1',
|
48
|
+
type: {
|
49
|
+
code: 'site',
|
50
|
+
name: 'Site',
|
55
51
|
},
|
56
|
-
|
57
|
-
|
58
|
-
]);
|
59
|
-
},
|
60
|
-
onRequestChildren: () => {
|
61
|
-
return Promise.resolve([
|
62
|
-
mockResource({
|
63
|
-
id: '123',
|
64
|
-
type: {
|
65
|
-
code: 'page',
|
66
|
-
name: 'Mocked Page',
|
52
|
+
name: 'Test Website',
|
53
|
+
childCount: 21,
|
67
54
|
},
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
55
|
+
],
|
56
|
+
}),
|
57
|
+
]),
|
58
|
+
onRequestResource: jest.fn().mockRejectedValue(new Error('onRequestResource has not been mocked.')),
|
59
|
+
onRequestChildren: jest.fn().mockResolvedValue([
|
60
|
+
mockResource({
|
61
|
+
id: '123',
|
62
|
+
type: {
|
63
|
+
code: 'page',
|
64
|
+
name: 'Mocked Page',
|
65
|
+
},
|
66
|
+
name: 'Test Page',
|
67
|
+
childCount: 0,
|
68
|
+
}),
|
69
|
+
]),
|
70
|
+
onChange: jest.fn(),
|
74
71
|
};
|
75
72
|
|
76
73
|
describe('ResourcePickerContainer', () => {
|
@@ -146,6 +143,123 @@ describe('ResourcePickerContainer', () => {
|
|
146
143
|
});
|
147
144
|
});
|
148
145
|
|
146
|
+
it('The preselected resource is selected', async () => {
|
147
|
+
const resources: Record<string, Resource> = {
|
148
|
+
1: mockResource({ id: '1', name: 'Root Folder' }),
|
149
|
+
100: mockResource({ id: '100', name: 'Source root node #1' }),
|
150
|
+
200: mockResource({ id: '200', name: 'Source root node #2' }),
|
151
|
+
201: mockResource({ id: '201', name: 'Child #1' }),
|
152
|
+
202: mockResource({ id: '202', name: 'Child #2' }),
|
153
|
+
203: mockResource({
|
154
|
+
id: '203',
|
155
|
+
name: 'Leaf',
|
156
|
+
lineages: [
|
157
|
+
{
|
158
|
+
resourceIds: ['1', '200', '201', '202', '203'],
|
159
|
+
},
|
160
|
+
],
|
161
|
+
}),
|
162
|
+
204: mockResource({ id: '204', name: 'Another leaf' }),
|
163
|
+
300: mockResource({ id: '300', name: 'Source root node #3' }),
|
164
|
+
};
|
165
|
+
const sources: Record<string, Source> = {
|
166
|
+
1: mockSource({
|
167
|
+
id: '1',
|
168
|
+
name: 'Test system 1',
|
169
|
+
nodes: [resources[100]],
|
170
|
+
}),
|
171
|
+
2: mockSource({
|
172
|
+
id: '2',
|
173
|
+
name: 'Test system 2',
|
174
|
+
nodes: [resources[200], resources[300]],
|
175
|
+
}),
|
176
|
+
};
|
177
|
+
|
178
|
+
const onRequestSources = jest.fn().mockResolvedValue(Object.values(sources));
|
179
|
+
const onRequestResource = jest.fn((reference: ResourceReference) => Promise.resolve(resources[reference.resource]));
|
180
|
+
const onRequestChildren = jest.fn().mockResolvedValue([resources[203], resources[204]]);
|
181
|
+
const preselectedSourceId = '2';
|
182
|
+
const preselectedResource = resources[203];
|
183
|
+
|
184
|
+
render(
|
185
|
+
<ResourcePickerContainer
|
186
|
+
{...baseProps}
|
187
|
+
onRequestSources={onRequestSources}
|
188
|
+
onRequestResource={onRequestResource}
|
189
|
+
onRequestChildren={onRequestChildren}
|
190
|
+
preselectedSourceId={preselectedSourceId}
|
191
|
+
preselectedResource={preselectedResource}
|
192
|
+
/>,
|
193
|
+
);
|
194
|
+
|
195
|
+
await waitFor(() => expect(screen.getByRole('button', { name: 'folder Leaf selected' })).toBeInTheDocument());
|
196
|
+
|
197
|
+
const breadcrumbs = within(screen.getByLabelText('Resource breadcrumb')).getAllByRole('listitem');
|
198
|
+
|
199
|
+
// Breadcrumbs should be populated, source should be populated.
|
200
|
+
// "Leaf" resource should be selected, "Another leaf" resource should not be selected.
|
201
|
+
expect(breadcrumbs.map((item) => item.textContent)).toEqual(['', 'Source root node #2', 'Child #1', 'Child #2']);
|
202
|
+
expect(screen.getByLabelText('Source quick select')).toHaveTextContent(/Source root node #2/);
|
203
|
+
expect(screen.getByRole('button', { name: 'folder Leaf selected' })).toBeInTheDocument();
|
204
|
+
expect(screen.getByRole('button', { name: 'folder Another leaf' })).toBeInTheDocument();
|
205
|
+
});
|
206
|
+
|
207
|
+
it.each([
|
208
|
+
['the preselected resource is a root node', 10],
|
209
|
+
['the preselected resource lineage does not exist under a root node', 100],
|
210
|
+
['the preselected resource lineage does not appear under a root node', 200],
|
211
|
+
])('The source list is displayed if %s', async (description: string, preselectedResourceId: number) => {
|
212
|
+
const resources: Record<string, Resource> = {
|
213
|
+
10: mockResource({
|
214
|
+
id: '100',
|
215
|
+
name: 'Source root node #1',
|
216
|
+
lineages: [{ resourceIds: ['1', '10'] }],
|
217
|
+
}),
|
218
|
+
100: mockResource({
|
219
|
+
id: '100',
|
220
|
+
name: 'Resource without lineages',
|
221
|
+
lineages: undefined,
|
222
|
+
}),
|
223
|
+
200: mockResource({
|
224
|
+
id: '200',
|
225
|
+
name: 'Resource not available under a root node',
|
226
|
+
lineages: [{ resourceIds: ['1', '20', '200'] }],
|
227
|
+
}),
|
228
|
+
};
|
229
|
+
const sources: Record<string, Source> = {
|
230
|
+
1: mockSource({
|
231
|
+
id: '1',
|
232
|
+
name: 'Test system 1',
|
233
|
+
nodes: [resources[10]],
|
234
|
+
}),
|
235
|
+
};
|
236
|
+
|
237
|
+
const onRequestSources = jest.fn().mockResolvedValue(Object.values(sources));
|
238
|
+
const onRequestResource = jest.fn((reference: ResourceReference) => Promise.resolve(resources[reference.resource]));
|
239
|
+
const onRequestChildren = jest.fn().mockResolvedValue([]);
|
240
|
+
const preselectedSourceId = '1';
|
241
|
+
const preselectedResource = resources[preselectedResourceId];
|
242
|
+
|
243
|
+
render(
|
244
|
+
<ResourcePickerContainer
|
245
|
+
{...baseProps}
|
246
|
+
onRequestSources={onRequestSources}
|
247
|
+
onRequestResource={onRequestResource}
|
248
|
+
onRequestChildren={onRequestChildren}
|
249
|
+
preselectedSourceId={preselectedSourceId}
|
250
|
+
preselectedResource={preselectedResource}
|
251
|
+
/>,
|
252
|
+
);
|
253
|
+
|
254
|
+
await waitFor(() => expect(screen.queryByRole('list', { name: 'Source list' })).toBeInTheDocument());
|
255
|
+
|
256
|
+
// Breadcrumbs should not be displayed.
|
257
|
+
// Source list should be displayed.
|
258
|
+
// "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();
|
261
|
+
});
|
262
|
+
|
149
263
|
it('Selecting a child count drills down', async () => {
|
150
264
|
const onRequestChildren = jest.fn(() => {
|
151
265
|
return Promise.resolve([]);
|
@@ -1,26 +1,36 @@
|
|
1
|
-
import React, { useState, useCallback, useEffect } from 'react';
|
1
|
+
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
2
2
|
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
3
|
import { useOverlayTriggerState } from 'react-stately';
|
4
|
-
import { useAsync } from '@squiz/generic-browser-lib';
|
5
|
-
|
6
4
|
import SourceList from '../SourceList/SourceList';
|
7
5
|
import ResourceList from '../ResourceList/ResourceList';
|
8
6
|
import ResourceBreadcrumb from '../ResourceBreadcrumb/ResourceBreadcrumb';
|
9
7
|
import PreviewPanel from '../PreviewPanel/PreviewPanel';
|
10
8
|
import SourceDropdown from '../SourceDropdown/SourceDropdown';
|
11
|
-
|
12
|
-
|
9
|
+
import {
|
10
|
+
Source,
|
11
|
+
Resource,
|
12
|
+
HydratedResourceReference,
|
13
|
+
ScopedSource,
|
14
|
+
OnRequestSources,
|
15
|
+
OnRequestChildren,
|
16
|
+
OnRequestResource,
|
17
|
+
} from '../types';
|
13
18
|
import { useResourcePath } from '../Hooks/useResourcePath';
|
14
19
|
import { useChildResources } from '../Hooks/useChildResources';
|
20
|
+
import { useSources } from '../Hooks/useSources';
|
21
|
+
import { usePreselectedResourcePath } from '../Hooks/usePreselectedResourcePath';
|
15
22
|
|
16
23
|
interface ResourcePickerContainerProps {
|
17
24
|
title: string;
|
18
25
|
titleAriaProps: DOMAttributes<FocusableElement>;
|
19
26
|
allowedTypes: string[] | undefined;
|
20
|
-
onRequestSources:
|
21
|
-
|
27
|
+
onRequestSources: OnRequestSources;
|
28
|
+
onRequestResource: OnRequestResource;
|
29
|
+
onRequestChildren: OnRequestChildren;
|
22
30
|
onChange(resource: HydratedResourceReference | null): void;
|
23
31
|
onClose: () => void;
|
32
|
+
preselectedSourceId?: string;
|
33
|
+
preselectedResource?: Resource | null;
|
24
34
|
}
|
25
35
|
|
26
36
|
function ResourcePickerContainer({
|
@@ -28,28 +38,42 @@ function ResourcePickerContainer({
|
|
28
38
|
titleAriaProps,
|
29
39
|
allowedTypes,
|
30
40
|
onRequestSources,
|
41
|
+
onRequestResource,
|
31
42
|
onRequestChildren,
|
32
43
|
onChange,
|
33
44
|
onClose,
|
45
|
+
preselectedSourceId,
|
46
|
+
preselectedResource,
|
34
47
|
}: ResourcePickerContainerProps) {
|
35
48
|
const previewModalState = useOverlayTriggerState({});
|
36
|
-
const [
|
49
|
+
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
|
37
50
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes>({});
|
38
51
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = useResourcePath();
|
39
|
-
|
40
52
|
const {
|
41
53
|
data: sources,
|
42
54
|
isLoading: isSourceLoading,
|
43
55
|
reload: handleSourceReload,
|
44
56
|
error: sourceError,
|
45
|
-
} =
|
46
|
-
|
57
|
+
} = useSources({ onRequestSources });
|
47
58
|
const {
|
48
59
|
data: resources,
|
49
60
|
isLoading: isResourcesLoading,
|
50
61
|
reload: handleResourceReload,
|
51
62
|
error: resourceError,
|
52
63
|
} = useChildResources({ source, currentResource, onRequestChildren });
|
64
|
+
const {
|
65
|
+
data: { source: preselectedSource, path: preselectedPath },
|
66
|
+
isLoading: isPreselectedResourcePathLoading,
|
67
|
+
} = usePreselectedResourcePath({
|
68
|
+
sourceId: preselectedSourceId,
|
69
|
+
resource: preselectedResource,
|
70
|
+
onRequestResource,
|
71
|
+
onRequestSources,
|
72
|
+
});
|
73
|
+
const selectedResource = useMemo(
|
74
|
+
() => resources.find((resource) => resource.id === selectedResourceId) || null,
|
75
|
+
[selectedResourceId, resources],
|
76
|
+
);
|
53
77
|
|
54
78
|
const handleResourceDrillDown = useCallback(
|
55
79
|
(resource: Resource) => {
|
@@ -60,7 +84,7 @@ function ResourcePickerContainer({
|
|
60
84
|
|
61
85
|
const handleResourceSelected = useCallback((resource: Resource, overlayProps: DOMAttributes) => {
|
62
86
|
setPreviewModalOverlayProps(overlayProps);
|
63
|
-
|
87
|
+
setSelectedResourceId(resource.id);
|
64
88
|
}, []);
|
65
89
|
|
66
90
|
const handleSourceDrilldown = useCallback(
|
@@ -83,13 +107,35 @@ function ResourcePickerContainer({
|
|
83
107
|
);
|
84
108
|
|
85
109
|
const handleDetailClose = useCallback(() => {
|
86
|
-
|
110
|
+
setSelectedResourceId(null);
|
87
111
|
}, []);
|
88
112
|
|
89
|
-
//
|
113
|
+
// Clear the selected resource if it no longer exists in the list of resources
|
114
|
+
// (eg. due to navigating up/down the tree).
|
115
|
+
useEffect(() => {
|
116
|
+
if (resources.length > 0 && selectedResourceId && !selectedResource) {
|
117
|
+
setSelectedResourceId(null);
|
118
|
+
}
|
119
|
+
}, [resources, selectedResourceId, selectedResource]);
|
120
|
+
|
90
121
|
useEffect(() => {
|
91
|
-
|
92
|
-
|
122
|
+
if (preselectedSource && preselectedPath?.length) {
|
123
|
+
const [rootNode, ...path] = preselectedPath;
|
124
|
+
const leaf = path.pop();
|
125
|
+
|
126
|
+
setSource(
|
127
|
+
{
|
128
|
+
source: preselectedSource,
|
129
|
+
resource: rootNode,
|
130
|
+
},
|
131
|
+
path,
|
132
|
+
);
|
133
|
+
|
134
|
+
if (leaf) {
|
135
|
+
setSelectedResourceId(leaf.id);
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}, [preselectedSource, preselectedSource]);
|
93
139
|
|
94
140
|
return (
|
95
141
|
<div className="relative flex flex-col h-full text-gray-800">
|
@@ -134,7 +180,7 @@ function ResourcePickerContainer({
|
|
134
180
|
<SourceList
|
135
181
|
sources={sources}
|
136
182
|
previewModalState={previewModalState}
|
137
|
-
isLoading={isSourceLoading}
|
183
|
+
isLoading={isSourceLoading || isPreselectedResourcePathLoading}
|
138
184
|
onSourceSelect={handleSourceDrilldown}
|
139
185
|
handleReload={handleSourceReload}
|
140
186
|
error={sourceError}
|
@@ -157,7 +203,7 @@ function ResourcePickerContainer({
|
|
157
203
|
</div>
|
158
204
|
<div className="sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white">
|
159
205
|
<PreviewPanel
|
160
|
-
resource={selectedResource}
|
206
|
+
resource={isResourcesLoading ? null : selectedResource}
|
161
207
|
modalState={previewModalState}
|
162
208
|
previewModalOverlayProps={previewModalOverlayProps}
|
163
209
|
allowedTypes={allowedTypes}
|
@@ -4,7 +4,7 @@ import { Icon, IconOptions, Spinner } from '@squiz/generic-browser-lib';
|
|
4
4
|
|
5
5
|
import type { Source, ScopedSource } from '../types';
|
6
6
|
|
7
|
-
import uuid from '../uuid';
|
7
|
+
import uuid from '../utils/uuid';
|
8
8
|
import { useCategorisedSources } from '../Hooks/useCategorisedSources';
|
9
9
|
|
10
10
|
export default function SourceDropdown({
|
@@ -17,7 +17,7 @@
|
|
17
17
|
"childCount": 21
|
18
18
|
},
|
19
19
|
{
|
20
|
-
"id": "
|
20
|
+
"id": "10",
|
21
21
|
"type": {
|
22
22
|
"code": "site",
|
23
23
|
"name": "Site"
|
@@ -30,7 +30,7 @@
|
|
30
30
|
"childCount": 135877
|
31
31
|
},
|
32
32
|
{
|
33
|
-
"id": "
|
33
|
+
"id": "20",
|
34
34
|
"type": {
|
35
35
|
"code": "folder",
|
36
36
|
"name": "Folder"
|
@@ -49,7 +49,7 @@
|
|
49
49
|
"name": "Acme internal system",
|
50
50
|
"nodes": [
|
51
51
|
{
|
52
|
-
"id": "
|
52
|
+
"id": "30",
|
53
53
|
"type": {
|
54
54
|
"code": "site",
|
55
55
|
"name": "Site"
|
@@ -62,7 +62,7 @@
|
|
62
62
|
"childCount": 15
|
63
63
|
},
|
64
64
|
{
|
65
|
-
"id": "
|
65
|
+
"id": "1",
|
66
66
|
"type": {
|
67
67
|
"code": "site",
|
68
68
|
"name": "Site"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import sampleSources from '../SourceList/sample-sources.json';
|
2
|
-
import { HydratedResourceReference, Resource, Source } from '../types';
|
2
|
+
import { HydratedResourceReference, Resource, ResourceReference, Source } from '../types';
|
3
3
|
import sampleResources from '../ResourceList/sample-resources.json';
|
4
4
|
|
5
5
|
type CreateCallbacksProps = Partial<{
|
@@ -9,14 +9,39 @@ type CreateCallbacksProps = Partial<{
|
|
9
9
|
error?: string;
|
10
10
|
}>;
|
11
11
|
|
12
|
+
const indexResources = (resources: Resource[]) => {
|
13
|
+
const indexed: Record<string, Resource> = {};
|
14
|
+
const pending = [...resources];
|
15
|
+
|
16
|
+
while (pending.length > 0) {
|
17
|
+
const resource = pending.shift();
|
18
|
+
|
19
|
+
if (!resource) {
|
20
|
+
continue;
|
21
|
+
}
|
22
|
+
|
23
|
+
indexed[resource.id] = resource;
|
24
|
+
|
25
|
+
if (resource && '_children' in resource) {
|
26
|
+
pending.push(...(resource._children as Resource[]));
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
return indexed;
|
31
|
+
};
|
32
|
+
|
12
33
|
export const createResourceBrowserCallbacks = ({
|
13
34
|
delay = 500,
|
14
35
|
sourceIsLoading = false,
|
15
36
|
resourceIsLoading = false,
|
16
37
|
error,
|
17
38
|
}: CreateCallbacksProps = {}) => {
|
39
|
+
const indexedResources = indexResources(sampleResources as Resource[]);
|
40
|
+
|
18
41
|
return {
|
19
42
|
onRequestSources: () => {
|
43
|
+
console.log('onRequestSources');
|
44
|
+
|
20
45
|
return new Promise((resolve, reject) => {
|
21
46
|
if (!sourceIsLoading) {
|
22
47
|
setTimeout(() => {
|
@@ -30,26 +55,30 @@ export const createResourceBrowserCallbacks = ({
|
|
30
55
|
});
|
31
56
|
},
|
32
57
|
onRequestChildren: (source: Source, resource: Resource | null) => {
|
58
|
+
console.log('onRequestChildren', source, resource);
|
59
|
+
|
33
60
|
return new Promise((resolve, reject) => {
|
34
61
|
if (!resourceIsLoading) {
|
35
62
|
setTimeout(() => {
|
36
63
|
if (error && Math.random() > 0.5) {
|
37
64
|
reject(new Error(error));
|
38
65
|
} else {
|
39
|
-
resolve((
|
66
|
+
resolve(resource ? (indexedResources[resource.id] as any)?._children || [] : sampleResources);
|
40
67
|
}
|
41
68
|
}, delay);
|
42
69
|
}
|
43
70
|
});
|
44
71
|
},
|
45
|
-
onRequestResource: () => {
|
72
|
+
onRequestResource: (reference: ResourceReference) => {
|
73
|
+
console.log('onRequestResource', reference);
|
74
|
+
|
46
75
|
return new Promise((resolve, reject) => {
|
47
76
|
if (!resourceIsLoading) {
|
48
77
|
setTimeout(() => {
|
49
78
|
if (error && Math.random() > 0.5) {
|
50
79
|
reject(new Error(error));
|
51
80
|
} else {
|
52
|
-
resolve(
|
81
|
+
resolve(indexedResources[reference.resource]);
|
53
82
|
}
|
54
83
|
}, delay);
|
55
84
|
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import React, { ReactElement, ReactNode } from 'react';
|
2
|
+
import {
|
3
|
+
ResourceBrowserContextProps,
|
4
|
+
ResourceBrowserContextProvider,
|
5
|
+
} from '../ResourceBrowserContext/ResourceBrowserContext';
|
6
|
+
import { render } from '@testing-library/react';
|
7
|
+
|
8
|
+
export const renderWithContext = (ui: ReactElement, context: Partial<ResourceBrowserContextProps> = {}) => {
|
9
|
+
return render(ui, {
|
10
|
+
wrapper: ({ children }: { children: ReactNode }): ReactElement => (
|
11
|
+
<ResourceBrowserContextProvider
|
12
|
+
value={{
|
13
|
+
onRequestSources: jest.fn(),
|
14
|
+
onRequestChildren: jest.fn(),
|
15
|
+
onRequestResource: jest.fn(),
|
16
|
+
...context,
|
17
|
+
}}
|
18
|
+
>
|
19
|
+
{children}
|
20
|
+
</ResourceBrowserContextProvider>
|
21
|
+
),
|
22
|
+
});
|
23
|
+
};
|