@squiz/resource-browser 1.68.1 → 1.69.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 +6 -0
- package/lib/Hooks/usePreselectedResourcePath.js +8 -3
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +39 -14
- package/lib/SourceList/SourceList.d.ts +3 -1
- package/lib/SourceList/SourceList.js +5 -2
- package/package.json +1 -1
- package/src/Hooks/usePreselectedResourcePath.ts +9 -5
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +46 -19
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +12 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +45 -21
- package/src/SourceList/SourceList.spec.tsx +46 -1
- package/src/SourceList/SourceList.stories.tsx +14 -6
- package/src/SourceList/SourceList.tsx +21 -0
- package/src/SourceList/sample-sources.json +34 -2
- package/src/__mocks__/StorybookHelpers.ts +30 -1
- package/src/index.stories.tsx +8 -2
package/CHANGELOG.md
CHANGED
@@ -14,9 +14,14 @@ const usePreselectedResourcePath = ({ sourceId, resource, onRequestResource, onR
|
|
14
14
|
}
|
15
15
|
if (sourceId && source && resource) {
|
16
16
|
const bestMatchLineage = (0, findBestMatchLineage_1.findBestMatchLineage)(source, resource);
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
if (Array.isArray(bestMatchLineage) && bestMatchLineage.length > 0) {
|
18
|
+
path = await Promise.all(bestMatchLineage.map(async (resourceId) => {
|
19
|
+
return onRequestResource({ source: sourceId, resource: resourceId });
|
20
|
+
}));
|
21
|
+
}
|
22
|
+
else {
|
23
|
+
path = [resource];
|
24
|
+
}
|
20
25
|
}
|
21
26
|
return { source, path };
|
22
27
|
},
|
@@ -40,6 +40,7 @@ const usePreselectedResourcePath_1 = require("../Hooks/usePreselectedResourcePat
|
|
40
40
|
const useRecentLocations_1 = require("../Hooks/useRecentLocations");
|
41
41
|
function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestResource, onRequestChildren, onChange, onClose, preselectedSourceId, preselectedResource, }) {
|
42
42
|
const previewModalState = (0, react_stately_1.useOverlayTriggerState)({});
|
43
|
+
const [selectedSource, setSelectedSource] = (0, react_1.useState)(null);
|
43
44
|
const [selectedResourceId, setSelectedResourceId] = (0, react_1.useState)(null);
|
44
45
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = (0, react_1.useState)({});
|
45
46
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = (0, useResourcePath_1.useResourcePath)();
|
@@ -52,43 +53,63 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
52
53
|
onRequestResource,
|
53
54
|
onRequestSources,
|
54
55
|
});
|
55
|
-
const selectedResource = (0, react_1.useMemo)(() =>
|
56
|
+
const selectedResource = (0, react_1.useMemo)(() => {
|
57
|
+
if (selectedSource) {
|
58
|
+
return selectedSource?.nodes.find((resource) => resource.id === selectedResourceId) || null;
|
59
|
+
}
|
60
|
+
return resources.find((resource) => resource.id === selectedResourceId) || null;
|
61
|
+
}, [selectedResourceId, resources, selectedSource]);
|
56
62
|
const handleResourceDrillDown = (0, react_1.useCallback)((resource) => {
|
57
63
|
push(resource);
|
58
64
|
}, [push]);
|
59
65
|
const handleResourceSelected = (0, react_1.useCallback)((resource, overlayProps) => {
|
60
66
|
setPreviewModalOverlayProps(overlayProps);
|
67
|
+
setSelectedSource(null);
|
61
68
|
setSelectedResourceId(resource.id);
|
62
69
|
}, []);
|
63
70
|
const handleSourceDrilldown = (0, react_1.useCallback)((source) => {
|
71
|
+
setSelectedSource(null);
|
72
|
+
setSelectedResourceId(null);
|
64
73
|
setSource(source);
|
65
74
|
}, [setSource]);
|
75
|
+
const handleSourceSelected = (0, react_1.useCallback)((node, overlayProps) => {
|
76
|
+
const { source, resource } = node;
|
77
|
+
setPreviewModalOverlayProps(overlayProps);
|
78
|
+
setSelectedSource(source || null);
|
79
|
+
setSelectedResourceId(resource?.id || null);
|
80
|
+
}, []);
|
66
81
|
const handleReturnToRoot = (0, react_1.useCallback)(() => {
|
82
|
+
setSelectedSource(null);
|
83
|
+
setSelectedResourceId(null);
|
67
84
|
setSource(null);
|
68
85
|
}, [setSource]);
|
69
86
|
const handleDetailSelect = (0, react_1.useCallback)((resource) => {
|
70
|
-
onChange({ resource, source: source?.source });
|
87
|
+
onChange({ resource, source: selectedSource ?? source?.source });
|
71
88
|
// Find the path that got them to where they are
|
72
89
|
const selectedPath = hierarchy.map((path) => {
|
73
90
|
const pathNode = path.node;
|
74
91
|
return 'resource' in pathNode ? pathNode.resource : pathNode;
|
75
92
|
});
|
76
93
|
const [rootNode, ...path] = selectedPath;
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
94
|
+
if (rootNode) {
|
95
|
+
// Update the recent locations in local storage
|
96
|
+
addRecentLocation({
|
97
|
+
rootNode,
|
98
|
+
path: path,
|
99
|
+
source: selectedSource ?? source?.source,
|
100
|
+
});
|
101
|
+
}
|
83
102
|
onClose();
|
84
|
-
}, [source, currentResource]);
|
103
|
+
}, [selectedSource, source, currentResource]);
|
85
104
|
const handleDetailClose = (0, react_1.useCallback)(() => {
|
105
|
+
setSelectedSource(null);
|
86
106
|
setSelectedResourceId(null);
|
87
107
|
}, []);
|
88
108
|
// Clear the selected resource if it no longer exists in the list of resources
|
89
109
|
// (eg. due to navigating up/down the tree).
|
90
110
|
(0, react_1.useEffect)(() => {
|
91
111
|
if (resources.length > 0 && selectedResourceId && !selectedResource) {
|
112
|
+
setSelectedSource(null);
|
92
113
|
setSelectedResourceId(null);
|
93
114
|
}
|
94
115
|
}, [resources, selectedResourceId, selectedResource]);
|
@@ -96,13 +117,17 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
96
117
|
if (preselectedSource && preselectedPath?.length) {
|
97
118
|
const [rootNode, ...path] = preselectedPath;
|
98
119
|
const leaf = path.pop();
|
99
|
-
setSource({
|
100
|
-
source: preselectedSource,
|
101
|
-
resource: rootNode,
|
102
|
-
}, path);
|
103
120
|
if (leaf) {
|
121
|
+
setSource({
|
122
|
+
source: preselectedSource,
|
123
|
+
resource: rootNode,
|
124
|
+
}, path);
|
104
125
|
setSelectedResourceId(leaf.id);
|
105
126
|
}
|
127
|
+
else {
|
128
|
+
setSelectedSource(preselectedSource);
|
129
|
+
setSelectedResourceId(rootNode.id);
|
130
|
+
}
|
106
131
|
}
|
107
132
|
}, [preselectedSource, preselectedSource]);
|
108
133
|
return (react_1.default.createElement("div", { className: "relative flex flex-col h-full text-gray-800" },
|
@@ -117,7 +142,7 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
117
142
|
react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
|
118
143
|
react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
|
119
144
|
hierarchy.length > 0 && (react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: popUntil, onReturnToRoot: handleReturnToRoot })),
|
120
|
-
!source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading || isPreselectedResourcePathLoading, onSourceSelect: handleSourceDrilldown, handleReload: handleSourceReload, setSource: setSource, error: sourceError })),
|
145
|
+
!source && (react_1.default.createElement(SourceList_1.default, { sources: sources, selectedResource: selectedResource, previewModalState: previewModalState, isLoading: isSourceLoading || isPreselectedResourcePathLoading, onSourceSelect: handleSourceSelected, onSourceDrilldown: handleSourceDrilldown, handleReload: handleSourceReload, setSource: setSource, error: sourceError })),
|
121
146
|
source && (react_1.default.createElement(ResourceList_1.default, { previewModalState: previewModalState, resources: resources, selectedResource: selectedResource, isLoading: isResourcesLoading, onResourceSelect: handleResourceSelected, onResourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes, handleReturnToRoot: handleReturnToRoot, handleReload: handleResourceReload, error: resourceError }))),
|
122
147
|
react_1.default.createElement("div", { className: "sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white" },
|
123
148
|
react_1.default.createElement(PreviewPanel_1.default, { resource: isResourcesLoading ? null : selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose })))));
|
@@ -4,12 +4,14 @@ import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
|
4
4
|
import { Source, ScopedSource, Resource } from '../types';
|
5
5
|
export interface SourceListProps {
|
6
6
|
sources: Source[];
|
7
|
+
selectedResource?: Resource | null;
|
7
8
|
previewModalState: OverlayTriggerState;
|
8
9
|
isLoading: boolean;
|
9
10
|
onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
|
11
|
+
onSourceDrilldown: (source: ScopedSource) => void;
|
10
12
|
handleReload: () => void;
|
11
13
|
setSource: (source: ScopedSource | null, path?: Resource[]) => void;
|
12
14
|
error: Error | null;
|
13
15
|
}
|
14
|
-
declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, handleReload, setSource, error, }: SourceListProps) => React.JSX.Element;
|
16
|
+
declare const SourceList: ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, error, }: SourceListProps) => React.JSX.Element;
|
15
17
|
export default SourceList;
|
@@ -32,7 +32,7 @@ const clsx_1 = __importDefault(require("clsx"));
|
|
32
32
|
const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
|
33
33
|
const useRecentLocations_1 = require("../Hooks/useRecentLocations");
|
34
34
|
const HistoryIcon_1 = require("../Icons/HistoryIcon");
|
35
|
-
const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, handleReload, setSource, error, }) {
|
35
|
+
const SourceList = function ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, error, }) {
|
36
36
|
const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
|
37
37
|
const listRef = (0, react_1.useRef)(null);
|
38
38
|
const { recentLocations } = (0, useRecentLocations_1.useRecentLocations)();
|
@@ -70,7 +70,10 @@ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSe
|
|
70
70
|
react_1.default.createElement("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" },
|
71
71
|
react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
|
72
72
|
sources.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col" }, sources.map(({ source, resource }) => {
|
73
|
-
|
73
|
+
if (!resource || resource.childCount === 0) {
|
74
|
+
return (react_1.default.createElement(generic_browser_lib_1.ResourceItem, { key: `${source.id}-${resource?.id}`, item: { source, resource }, label: resource?.name || source.name, type: resource?.type.code || 'folder', previewModalState: previewModalState, onSelect: onSourceDrilldown, className: "mt-3 rounded-lg", showChevron: true }));
|
75
|
+
}
|
76
|
+
return (react_1.default.createElement(generic_browser_lib_1.ResourceItem, { key: `${source.id}-${resource?.id}`, item: { source, resource }, selected: resource?.id == selectedResource?.id && resource != null, label: resource?.name || source.name, type: resource?.type.code || 'folder', childCount: resource?.childCount || undefined, previewModalState: previewModalState, onSelect: onSourceSelect, onDrillDown: onSourceDrilldown, className: "mt-3 rounded-lg", showChevron: true }));
|
74
77
|
})))));
|
75
78
|
})));
|
76
79
|
};
|
package/package.json
CHANGED
@@ -34,11 +34,15 @@ export const usePreselectedResourcePath = ({
|
|
34
34
|
if (sourceId && source && resource) {
|
35
35
|
const bestMatchLineage = findBestMatchLineage(source, resource);
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
if (Array.isArray(bestMatchLineage) && bestMatchLineage.length > 0) {
|
38
|
+
path = await Promise.all(
|
39
|
+
bestMatchLineage.map(async (resourceId) => {
|
40
|
+
return onRequestResource({ source: sourceId, resource: resourceId });
|
41
|
+
}),
|
42
|
+
);
|
43
|
+
} else {
|
44
|
+
path = [resource];
|
45
|
+
}
|
42
46
|
}
|
43
47
|
|
44
48
|
return { source, path };
|
@@ -708,31 +708,58 @@ describe('ResourcePickerContainer', () => {
|
|
708
708
|
]);
|
709
709
|
});
|
710
710
|
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
711
|
+
describe('handleDetailSelect() tests', () => {
|
712
|
+
it('Source select works', async () => {
|
713
|
+
const onChangeMock = jest.fn();
|
714
|
+
const onCloseMock = jest.fn();
|
715
|
+
const { getAllByText } = render(
|
716
|
+
<ResourcePickerContainer {...baseProps} onChange={onChangeMock} onClose={onCloseMock} />,
|
717
|
+
);
|
717
718
|
|
718
|
-
|
719
|
-
|
719
|
+
await waitFor(() => {
|
720
|
+
expect(getAllByText('Test system')[0]).toBeInTheDocument();
|
721
|
+
});
|
722
|
+
|
723
|
+
const user = userEvent.setup();
|
724
|
+
|
725
|
+
// Select the resource
|
726
|
+
user.click(screen.getByRole('button', { name: 'site Test Website' }));
|
727
|
+
|
728
|
+
// Wait for the preview panel to open
|
729
|
+
await waitFor(() => expect(screen.getByText('Site')).toBeInTheDocument());
|
730
|
+
await waitFor(() => expect(screen.getByText('#1')).toBeInTheDocument());
|
731
|
+
|
732
|
+
user.click(screen.getByRole('button', { name: 'Select' }));
|
733
|
+
|
734
|
+
await waitFor(() => expect(onChangeMock).toHaveBeenCalled());
|
735
|
+
await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
|
720
736
|
});
|
737
|
+
it('Resource select works', async () => {
|
738
|
+
const onChangeMock = jest.fn();
|
739
|
+
const onCloseMock = jest.fn();
|
740
|
+
const { getAllByText } = render(
|
741
|
+
<ResourcePickerContainer {...baseProps} onChange={onChangeMock} onClose={onCloseMock} />,
|
742
|
+
);
|
721
743
|
|
722
|
-
|
723
|
-
|
724
|
-
|
744
|
+
await waitFor(() => {
|
745
|
+
expect(getAllByText('Test system')[0]).toBeInTheDocument();
|
746
|
+
});
|
725
747
|
|
726
|
-
|
727
|
-
|
748
|
+
const user = userEvent.setup();
|
749
|
+
user.click(screen.getByRole('button', { name: 'Drill down to Test Website children' }));
|
750
|
+
await waitFor(() => expect(screen.getByRole('button', { name: 'page Test Page' })).toBeInTheDocument());
|
728
751
|
|
729
|
-
|
730
|
-
|
731
|
-
|
752
|
+
// Select the resource
|
753
|
+
user.click(screen.getByRole('button', { name: 'page Test Page' }));
|
754
|
+
|
755
|
+
// Wait for the preview panel to open
|
756
|
+
await waitFor(() => expect(screen.getByText('Mocked Page')).toBeInTheDocument());
|
757
|
+
await waitFor(() => expect(screen.getByText('#123')).toBeInTheDocument());
|
732
758
|
|
733
|
-
|
759
|
+
user.click(screen.getByRole('button', { name: 'Select' }));
|
734
760
|
|
735
|
-
|
736
|
-
|
761
|
+
await waitFor(() => expect(onChangeMock).toHaveBeenCalled());
|
762
|
+
await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
|
763
|
+
});
|
737
764
|
});
|
738
765
|
});
|
@@ -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
|
+
};
|
@@ -47,6 +47,7 @@ function ResourcePickerContainer({
|
|
47
47
|
preselectedResource,
|
48
48
|
}: ResourcePickerContainerProps) {
|
49
49
|
const previewModalState = useOverlayTriggerState({});
|
50
|
+
const [selectedSource, setSelectedSource] = useState<Source | null>(null);
|
50
51
|
const [selectedResourceId, setSelectedResourceId] = useState<string | null>(null);
|
51
52
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes>({});
|
52
53
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = useResourcePath();
|
@@ -73,10 +74,12 @@ function ResourcePickerContainer({
|
|
73
74
|
onRequestResource,
|
74
75
|
onRequestSources,
|
75
76
|
});
|
76
|
-
const selectedResource = useMemo(
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
const selectedResource = useMemo(() => {
|
78
|
+
if (selectedSource) {
|
79
|
+
return selectedSource?.nodes.find((resource: Resource) => resource.id === selectedResourceId) || null;
|
80
|
+
}
|
81
|
+
return resources.find((resource: Resource) => resource.id === selectedResourceId) || null;
|
82
|
+
}, [selectedResourceId, resources, selectedSource]);
|
80
83
|
|
81
84
|
const handleResourceDrillDown = useCallback(
|
82
85
|
(resource: Resource) => {
|
@@ -87,23 +90,35 @@ function ResourcePickerContainer({
|
|
87
90
|
|
88
91
|
const handleResourceSelected = useCallback((resource: Resource, overlayProps: DOMAttributes) => {
|
89
92
|
setPreviewModalOverlayProps(overlayProps);
|
93
|
+
setSelectedSource(null);
|
90
94
|
setSelectedResourceId(resource.id);
|
91
95
|
}, []);
|
92
96
|
|
93
97
|
const handleSourceDrilldown = useCallback(
|
94
98
|
(source: ScopedSource) => {
|
99
|
+
setSelectedSource(null);
|
100
|
+
setSelectedResourceId(null);
|
95
101
|
setSource(source);
|
96
102
|
},
|
97
103
|
[setSource],
|
98
104
|
);
|
99
105
|
|
106
|
+
const handleSourceSelected = useCallback((node: ScopedSource, overlayProps: DOMAttributes) => {
|
107
|
+
const { source, resource } = node;
|
108
|
+
setPreviewModalOverlayProps(overlayProps);
|
109
|
+
setSelectedSource(source || null);
|
110
|
+
setSelectedResourceId(resource?.id || null);
|
111
|
+
}, []);
|
112
|
+
|
100
113
|
const handleReturnToRoot = useCallback(() => {
|
114
|
+
setSelectedSource(null);
|
115
|
+
setSelectedResourceId(null);
|
101
116
|
setSource(null);
|
102
117
|
}, [setSource]);
|
103
118
|
|
104
119
|
const handleDetailSelect = useCallback(
|
105
120
|
(resource: Resource) => {
|
106
|
-
onChange({ resource, source: source?.source as Source });
|
121
|
+
onChange({ resource, source: selectedSource ?? (source?.source as Source) });
|
107
122
|
|
108
123
|
// Find the path that got them to where they are
|
109
124
|
const selectedPath = hierarchy.map((path) => {
|
@@ -113,19 +128,22 @@ function ResourcePickerContainer({
|
|
113
128
|
|
114
129
|
const [rootNode, ...path] = selectedPath;
|
115
130
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
131
|
+
if (rootNode) {
|
132
|
+
// Update the recent locations in local storage
|
133
|
+
addRecentLocation({
|
134
|
+
rootNode,
|
135
|
+
path: path as Resource[],
|
136
|
+
source: selectedSource ?? (source?.source as Source),
|
137
|
+
});
|
138
|
+
}
|
122
139
|
|
123
140
|
onClose();
|
124
141
|
},
|
125
|
-
[source, currentResource],
|
142
|
+
[selectedSource, source, currentResource],
|
126
143
|
);
|
127
144
|
|
128
145
|
const handleDetailClose = useCallback(() => {
|
146
|
+
setSelectedSource(null);
|
129
147
|
setSelectedResourceId(null);
|
130
148
|
}, []);
|
131
149
|
|
@@ -133,6 +151,7 @@ function ResourcePickerContainer({
|
|
133
151
|
// (eg. due to navigating up/down the tree).
|
134
152
|
useEffect(() => {
|
135
153
|
if (resources.length > 0 && selectedResourceId && !selectedResource) {
|
154
|
+
setSelectedSource(null);
|
136
155
|
setSelectedResourceId(null);
|
137
156
|
}
|
138
157
|
}, [resources, selectedResourceId, selectedResource]);
|
@@ -142,16 +161,19 @@ function ResourcePickerContainer({
|
|
142
161
|
const [rootNode, ...path] = preselectedPath;
|
143
162
|
const leaf = path.pop();
|
144
163
|
|
145
|
-
setSource(
|
146
|
-
{
|
147
|
-
source: preselectedSource,
|
148
|
-
resource: rootNode,
|
149
|
-
},
|
150
|
-
path,
|
151
|
-
);
|
152
|
-
|
153
164
|
if (leaf) {
|
165
|
+
setSource(
|
166
|
+
{
|
167
|
+
source: preselectedSource,
|
168
|
+
resource: rootNode,
|
169
|
+
},
|
170
|
+
path,
|
171
|
+
);
|
172
|
+
|
154
173
|
setSelectedResourceId(leaf.id);
|
174
|
+
} else {
|
175
|
+
setSelectedSource(preselectedSource);
|
176
|
+
setSelectedResourceId(rootNode.id);
|
155
177
|
}
|
156
178
|
}
|
157
179
|
}, [preselectedSource, preselectedSource]);
|
@@ -200,9 +222,11 @@ function ResourcePickerContainer({
|
|
200
222
|
{!source && (
|
201
223
|
<SourceList
|
202
224
|
sources={sources}
|
225
|
+
selectedResource={selectedResource}
|
203
226
|
previewModalState={previewModalState}
|
204
227
|
isLoading={isSourceLoading || isPreselectedResourcePathLoading}
|
205
|
-
onSourceSelect={
|
228
|
+
onSourceSelect={handleSourceSelected}
|
229
|
+
onSourceDrilldown={handleSourceDrilldown}
|
206
230
|
handleReload={handleSourceReload}
|
207
231
|
setSource={setSource}
|
208
232
|
error={sourceError}
|
@@ -107,6 +107,7 @@ describe('SourceList', () => {
|
|
107
107
|
previewModalState={previewModalState}
|
108
108
|
isLoading={true}
|
109
109
|
onSourceSelect={() => {}}
|
110
|
+
onSourceDrilldown={() => {}}
|
110
111
|
error={null}
|
111
112
|
handleReload={reload}
|
112
113
|
setSource={() => {}}
|
@@ -133,6 +134,7 @@ describe('SourceList', () => {
|
|
133
134
|
previewModalState={previewModalState}
|
134
135
|
isLoading={false}
|
135
136
|
onSourceSelect={() => {}}
|
137
|
+
onSourceDrilldown={() => {}}
|
136
138
|
error={null}
|
137
139
|
handleReload={reload}
|
138
140
|
setSource={() => {}}
|
@@ -160,6 +162,7 @@ describe('SourceList', () => {
|
|
160
162
|
previewModalState={previewModalState}
|
161
163
|
isLoading={false}
|
162
164
|
onSourceSelect={() => {}}
|
165
|
+
onSourceDrilldown={() => {}}
|
163
166
|
error={null}
|
164
167
|
handleReload={reload}
|
165
168
|
setSource={() => {}}
|
@@ -186,6 +189,7 @@ describe('SourceList', () => {
|
|
186
189
|
previewModalState={previewModalState}
|
187
190
|
isLoading={false}
|
188
191
|
onSourceSelect={() => {}}
|
192
|
+
onSourceDrilldown={() => {}}
|
189
193
|
error={null}
|
190
194
|
handleReload={reload}
|
191
195
|
setSource={() => {}}
|
@@ -219,6 +223,7 @@ describe('SourceList', () => {
|
|
219
223
|
previewModalState={previewModalState}
|
220
224
|
isLoading={false}
|
221
225
|
onSourceSelect={onSourceSelect}
|
226
|
+
onSourceDrilldown={() => {}}
|
222
227
|
error={null}
|
223
228
|
handleReload={reload}
|
224
229
|
setSource={() => {}}
|
@@ -229,7 +234,7 @@ describe('SourceList', () => {
|
|
229
234
|
);
|
230
235
|
|
231
236
|
const user = userEvent.setup();
|
232
|
-
const itemButton = screen.getByRole('button', { name: '
|
237
|
+
const itemButton = screen.getByRole('button', { name: 'site Node 1' });
|
233
238
|
user.click(itemButton);
|
234
239
|
|
235
240
|
await waitFor(() => {
|
@@ -244,6 +249,42 @@ describe('SourceList', () => {
|
|
244
249
|
});
|
245
250
|
});
|
246
251
|
|
252
|
+
it('Clicking node child count button triggers correct onSourceDrilldown', async () => {
|
253
|
+
const onSourceDrilldown = jest.fn();
|
254
|
+
const reload = jest.fn();
|
255
|
+
|
256
|
+
render(
|
257
|
+
<SourceListTestWrapper
|
258
|
+
constructFunction={(previewModalState) => {
|
259
|
+
return (
|
260
|
+
<SourceList
|
261
|
+
sources={sources}
|
262
|
+
previewModalState={previewModalState}
|
263
|
+
isLoading={false}
|
264
|
+
onSourceSelect={() => {}}
|
265
|
+
onSourceDrilldown={onSourceDrilldown}
|
266
|
+
error={null}
|
267
|
+
handleReload={reload}
|
268
|
+
setSource={() => {}}
|
269
|
+
/>
|
270
|
+
);
|
271
|
+
}}
|
272
|
+
/>,
|
273
|
+
);
|
274
|
+
|
275
|
+
const user = userEvent.setup();
|
276
|
+
const itemButton = screen.getByRole('button', { name: 'Drill down to Node 1 children' });
|
277
|
+
user.click(itemButton);
|
278
|
+
|
279
|
+
await waitFor(() => {
|
280
|
+
// Provides the item that was clicked and an id reference to the button that was clicked
|
281
|
+
expect(onSourceDrilldown).toHaveBeenCalledWith({
|
282
|
+
source: sources[0],
|
283
|
+
resource: sources[0].nodes[0],
|
284
|
+
});
|
285
|
+
});
|
286
|
+
});
|
287
|
+
|
247
288
|
it('Renders error state when an error occurs loading source list', async () => {
|
248
289
|
const reload = jest.fn();
|
249
290
|
|
@@ -256,6 +297,7 @@ describe('SourceList', () => {
|
|
256
297
|
previewModalState={previewModalState}
|
257
298
|
isLoading={false}
|
258
299
|
onSourceSelect={() => {}}
|
300
|
+
onSourceDrilldown={() => {}}
|
259
301
|
error={new Error('Source list error!')}
|
260
302
|
handleReload={reload}
|
261
303
|
setSource={() => {}}
|
@@ -286,6 +328,7 @@ describe('SourceList', () => {
|
|
286
328
|
previewModalState={previewModalState}
|
287
329
|
isLoading={false}
|
288
330
|
onSourceSelect={() => {}}
|
331
|
+
onSourceDrilldown={() => {}}
|
289
332
|
error={null}
|
290
333
|
handleReload={reload}
|
291
334
|
setSource={setSource}
|
@@ -316,6 +359,7 @@ describe('SourceList', () => {
|
|
316
359
|
previewModalState={previewModalState}
|
317
360
|
isLoading={false}
|
318
361
|
onSourceSelect={() => {}}
|
362
|
+
onSourceDrilldown={() => {}}
|
319
363
|
error={null}
|
320
364
|
handleReload={reload}
|
321
365
|
setSource={setSource}
|
@@ -369,6 +413,7 @@ describe('SourceList', () => {
|
|
369
413
|
previewModalState={previewModalState}
|
370
414
|
isLoading={false}
|
371
415
|
onSourceSelect={() => {}}
|
416
|
+
onSourceDrilldown={() => {}}
|
372
417
|
error={null}
|
373
418
|
handleReload={reload}
|
374
419
|
setSource={setSource}
|
@@ -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
|
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,
|
22
|
-
|
23
|
-
|
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
|
};
|
@@ -11,9 +11,11 @@ import { HistoryIcon } from '../Icons/HistoryIcon';
|
|
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;
|
19
21
|
error: Error | null;
|
@@ -21,9 +23,11 @@ export interface SourceListProps {
|
|
21
23
|
|
22
24
|
const SourceList = function ({
|
23
25
|
sources,
|
26
|
+
selectedResource,
|
24
27
|
previewModalState,
|
25
28
|
isLoading,
|
26
29
|
onSourceSelect,
|
30
|
+
onSourceDrilldown,
|
27
31
|
handleReload,
|
28
32
|
setSource,
|
29
33
|
error,
|
@@ -112,14 +116,31 @@ const SourceList = function ({
|
|
112
116
|
{sources.length > 0 && (
|
113
117
|
<ul aria-label={`${label} nodes`} className="flex flex-col">
|
114
118
|
{sources.map(({ source, resource }) => {
|
119
|
+
if (!resource || resource.childCount === 0) {
|
120
|
+
return (
|
121
|
+
<ResourceItem
|
122
|
+
key={`${source.id}-${resource?.id}`}
|
123
|
+
item={{ source, resource }}
|
124
|
+
label={resource?.name || source.name}
|
125
|
+
type={resource?.type.code || 'folder'}
|
126
|
+
previewModalState={previewModalState}
|
127
|
+
onSelect={onSourceDrilldown}
|
128
|
+
className="mt-3 rounded-lg"
|
129
|
+
showChevron
|
130
|
+
/>
|
131
|
+
);
|
132
|
+
}
|
115
133
|
return (
|
116
134
|
<ResourceItem
|
117
135
|
key={`${source.id}-${resource?.id}`}
|
118
136
|
item={{ source, resource }}
|
137
|
+
selected={resource?.id == selectedResource?.id && resource != null}
|
119
138
|
label={resource?.name || source.name}
|
120
139
|
type={resource?.type.code || 'folder'}
|
140
|
+
childCount={resource?.childCount || undefined}
|
121
141
|
previewModalState={previewModalState}
|
122
142
|
onSelect={onSourceSelect}
|
143
|
+
onDrillDown={onSourceDrilldown}
|
123
144
|
className="mt-3 rounded-lg"
|
124
145
|
showChevron
|
125
146
|
/>
|
@@ -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": "
|
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": "
|
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
|
-
|
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
|
}
|
package/src/index.stories.tsx
CHANGED
@@ -33,8 +33,14 @@ Primary.args = {
|
|
33
33
|
error: '',
|
34
34
|
};
|
35
35
|
|
36
|
-
export const
|
37
|
-
|
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
|
};
|