@squiz/resource-browser 1.32.1-alpha.12
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/.storybook/main.ts +23 -0
- package/.storybook/preview-head.html +15 -0
- package/.storybook/preview.ts +16 -0
- package/build.js +21 -0
- package/jest.config.ts +18 -0
- package/lib/Icons/Generics/ArrowDown.d.ts +15 -0
- package/lib/Icons/Generics/ArrowDown.js +23 -0
- package/lib/Icons/Generics/ArrowRight.d.ts +15 -0
- package/lib/Icons/Generics/ArrowRight.js +23 -0
- package/lib/Icons/Generics/Close.d.ts +15 -0
- package/lib/Icons/Generics/Close.js +23 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +10 -0
- package/lib/Icons/Generics/GenericIconMap.js +14 -0
- package/lib/Icons/Generics/ResourceSelect.d.ts +15 -0
- package/lib/Icons/Generics/ResourceSelect.js +28 -0
- package/lib/Icons/Generics/Root.d.ts +15 -0
- package/lib/Icons/Generics/Root.js +23 -0
- package/lib/Icons/Generics/Selected.d.ts +15 -0
- package/lib/Icons/Generics/Selected.js +23 -0
- package/lib/Icons/Generics/index.d.ts +6 -0
- package/lib/Icons/Generics/index.js +19 -0
- package/lib/Icons/Icon.d.ts +47 -0
- package/lib/Icons/Icon.js +44 -0
- package/lib/Icons/MatrixResources/Audio.d.ts +15 -0
- package/lib/Icons/MatrixResources/Audio.js +28 -0
- package/lib/Icons/MatrixResources/Excel.d.ts +15 -0
- package/lib/Icons/MatrixResources/Excel.js +27 -0
- package/lib/Icons/MatrixResources/Folder.d.ts +15 -0
- package/lib/Icons/MatrixResources/Folder.js +24 -0
- package/lib/Icons/MatrixResources/GenericFile.d.ts +15 -0
- package/lib/Icons/MatrixResources/GenericFile.js +28 -0
- package/lib/Icons/MatrixResources/Image.d.ts +15 -0
- package/lib/Icons/MatrixResources/Image.js +26 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +15 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.js +19 -0
- package/lib/Icons/MatrixResources/Page.d.ts +15 -0
- package/lib/Icons/MatrixResources/Page.js +30 -0
- package/lib/Icons/MatrixResources/Pdf.d.ts +15 -0
- package/lib/Icons/MatrixResources/Pdf.js +31 -0
- package/lib/Icons/MatrixResources/Powerpoint.d.ts +15 -0
- package/lib/Icons/MatrixResources/Powerpoint.js +28 -0
- package/lib/Icons/MatrixResources/Site.d.ts +15 -0
- package/lib/Icons/MatrixResources/Site.js +30 -0
- package/lib/Icons/MatrixResources/Video.d.ts +15 -0
- package/lib/Icons/MatrixResources/Video.js +24 -0
- package/lib/Icons/MatrixResources/Word.d.ts +17 -0
- package/lib/Icons/MatrixResources/Word.js +28 -0
- package/lib/Icons/MatrixResources/index.d.ts +11 -0
- package/lib/Icons/MatrixResources/index.js +29 -0
- package/lib/Modal/Modal.d.ts +11 -0
- package/lib/Modal/Modal.js +46 -0
- package/lib/Modal/ModalOpeningButton.d.ts +10 -0
- package/lib/Modal/ModalOpeningButton.js +13 -0
- package/lib/Modal/ModalTrigger.d.ts +9 -0
- package/lib/Modal/ModalTrigger.js +24 -0
- package/lib/PreviewPanel/PreviewModal.d.ts +11 -0
- package/lib/PreviewPanel/PreviewModal.js +81 -0
- package/lib/PreviewPanel/PreviewPanel.d.ts +16 -0
- package/lib/PreviewPanel/PreviewPanel.js +87 -0
- package/lib/PreviewPanel/details/MatrixResource.d.ts +12 -0
- package/lib/PreviewPanel/details/MatrixResource.js +41 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +9 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +20 -0
- package/lib/ResourceItem/ResourceItem.d.ts +19 -0
- package/lib/ResourceItem/ResourceItem.js +26 -0
- package/lib/ResourceList/ResourceList.d.ts +14 -0
- package/lib/ResourceList/ResourceList.js +51 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +15 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +145 -0
- package/lib/Skeleton/List/SkeletonList.d.ts +6 -0
- package/lib/Skeleton/List/SkeletonList.js +13 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.d.ts +2 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.js +15 -0
- package/lib/SourceDropdown/SourceDropdown.d.ts +9 -0
- package/lib/SourceDropdown/SourceDropdown.js +106 -0
- package/lib/SourceList/SourceList.d.ts +14 -0
- package/lib/SourceList/SourceList.js +58 -0
- package/lib/Spinner/Spinner.d.ts +8 -0
- package/lib/Spinner/Spinner.js +12 -0
- package/lib/index.css +968 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +15 -0
- package/lib/uuid.d.ts +1 -0
- package/lib/uuid.js +8 -0
- package/package.json +74 -0
- package/postcss.config.js +11 -0
- package/src/Icons/Generics/ArrowDown.tsx +27 -0
- package/src/Icons/Generics/ArrowRight.tsx +27 -0
- package/src/Icons/Generics/Close.tsx +26 -0
- package/src/Icons/Generics/GenericIconMap.ts +14 -0
- package/src/Icons/Generics/ResourceSelect.tsx +40 -0
- package/src/Icons/Generics/Root.tsx +24 -0
- package/src/Icons/Generics/Selected.tsx +27 -0
- package/src/Icons/Generics/index.tsx +7 -0
- package/src/Icons/Icon.spec.tsx +62 -0
- package/src/Icons/Icon.stories.tsx +105 -0
- package/src/Icons/Icon.tsx +61 -0
- package/src/Icons/MatrixResources/Audio.tsx +30 -0
- package/src/Icons/MatrixResources/Excel.tsx +29 -0
- package/src/Icons/MatrixResources/Folder.tsx +29 -0
- package/src/Icons/MatrixResources/GenericFile.tsx +34 -0
- package/src/Icons/MatrixResources/Image.tsx +36 -0
- package/src/Icons/MatrixResources/MatrixResourceMap.ts +19 -0
- package/src/Icons/MatrixResources/Page.tsx +33 -0
- package/src/Icons/MatrixResources/Pdf.tsx +34 -0
- package/src/Icons/MatrixResources/Powerpoint.tsx +34 -0
- package/src/Icons/MatrixResources/Site.tsx +37 -0
- package/src/Icons/MatrixResources/Video.tsx +27 -0
- package/src/Icons/MatrixResources/Word.tsx +30 -0
- package/src/Icons/MatrixResources/index.tsx +12 -0
- package/src/Modal/Modal.spec.tsx +244 -0
- package/src/Modal/Modal.tsx +58 -0
- package/src/Modal/ModalContainer.stories.tsx +33 -0
- package/src/Modal/ModalOpeningButton.tsx +20 -0
- package/src/Modal/ModalTrigger.tsx +45 -0
- package/src/PreviewPanel/PreviewModal.spec.tsx +164 -0
- package/src/PreviewPanel/PreviewModal.tsx +92 -0
- package/src/PreviewPanel/PreviewPanel.spec.tsx +197 -0
- package/src/PreviewPanel/PreviewPanel.stories.tsx +61 -0
- package/src/PreviewPanel/PreviewPanel.tsx +123 -0
- package/src/PreviewPanel/details/MatrixResource.tsx +59 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +76 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +24 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +39 -0
- package/src/ResourceBreadcrumb/sample-hierarchy.json +23 -0
- package/src/ResourceItem/ResourceItem.spec.tsx +69 -0
- package/src/ResourceItem/ResourceItem.tsx +82 -0
- package/src/ResourceList/ResourceList.spec.tsx +196 -0
- package/src/ResourceList/ResourceList.stories.tsx +40 -0
- package/src/ResourceList/ResourceList.tsx +74 -0
- package/src/ResourceList/sample-resources.json +75 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +706 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +56 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +224 -0
- package/src/Skeleton/List/SkeletonList.spec.tsx +18 -0
- package/src/Skeleton/List/SkeletonList.stories.tsx +15 -0
- package/src/Skeleton/List/SkeletonList.tsx +16 -0
- package/src/Skeleton/ListItem/SkeletonListItem.stories.tsx +15 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +14 -0
- package/src/SourceDropdown/SourceDropdown.spec.tsx +263 -0
- package/src/SourceDropdown/SourceDropdown.stories.tsx +36 -0
- package/src/SourceDropdown/SourceDropdown.tsx +175 -0
- package/src/SourceDropdown/sample-sources.json +110 -0
- package/src/SourceList/SourceList.spec.tsx +224 -0
- package/src/SourceList/SourceList.stories.tsx +40 -0
- package/src/SourceList/SourceList.tsx +93 -0
- package/src/SourceList/sample-sources.json +110 -0
- package/src/Spinner/Spinner.spec.tsx +18 -0
- package/src/Spinner/Spinner.stories.tsx +26 -0
- package/src/Spinner/Spinner.tsx +18 -0
- package/src/Spinner/_spinner.scss +11 -0
- package/src/__mocks__/JestHelpers.ts +65 -0
- package/src/__mocks__/jestHelpers.spec.ts +38 -0
- package/src/__mocks__/styleMock.ts +1 -0
- package/src/index.scss +7 -0
- package/src/index.stories.tsx +70 -0
- package/src/index.tsx +71 -0
- package/src/uuid.ts +7 -0
- package/tailwind.config.cjs +84 -0
- package/tsconfig.json +22 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import { NodeIdentifier } from '../index';
|
5
|
+
import ResourcePickerContainer from './ResourcePickerContainer';
|
6
|
+
import sampleSources from '../SourceList/sample-sources.json';
|
7
|
+
import sampleResources from '../ResourceList/sample-resources.json';
|
8
|
+
|
9
|
+
export default {
|
10
|
+
title: 'Resource Picker',
|
11
|
+
component: ResourcePickerContainer,
|
12
|
+
} as Meta<typeof ResourcePickerContainer>;
|
13
|
+
|
14
|
+
const Template: StoryFn<typeof ResourcePickerContainer> = ({ title }) => {
|
15
|
+
return (
|
16
|
+
<ResourcePickerContainer
|
17
|
+
title={title}
|
18
|
+
titleAriaProps={{}}
|
19
|
+
allowedTypes={undefined}
|
20
|
+
onRequestSources={() => {
|
21
|
+
return new Promise((resolve) => {
|
22
|
+
setTimeout(resolve, 500, sampleSources);
|
23
|
+
});
|
24
|
+
}}
|
25
|
+
onRequestChildren={() => {
|
26
|
+
return new Promise((resolve) => {
|
27
|
+
setTimeout(resolve, 500, sampleResources);
|
28
|
+
});
|
29
|
+
}}
|
30
|
+
onRequestResource={() => {
|
31
|
+
return new Promise((resolve) => {
|
32
|
+
setTimeout(resolve, 500, {
|
33
|
+
type: 'page',
|
34
|
+
name: 'Products',
|
35
|
+
properties: new Map([
|
36
|
+
['assetId', '12345'],
|
37
|
+
['status', 'UnderConstruction'],
|
38
|
+
]),
|
39
|
+
});
|
40
|
+
});
|
41
|
+
}}
|
42
|
+
onChange={(nodeId: NodeIdentifier) => {
|
43
|
+
alert(`Resource Browser has selected ${nodeId.source} ${nodeId.id}`);
|
44
|
+
}}
|
45
|
+
onClose={() => {
|
46
|
+
alert('Resource Browser closed');
|
47
|
+
}}
|
48
|
+
/>
|
49
|
+
);
|
50
|
+
};
|
51
|
+
|
52
|
+
export const Primary = Template.bind({});
|
53
|
+
|
54
|
+
Primary.args = {
|
55
|
+
title: 'Asset Picker',
|
56
|
+
};
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
2
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
|
+
import { useOverlayTriggerState } from 'react-stately';
|
4
|
+
|
5
|
+
import SourceList from '../SourceList/SourceList';
|
6
|
+
import ResourceList from '../ResourceList/ResourceList';
|
7
|
+
import ResourceBreadcrumb from '../ResourceBreadcrumb/ResourceBreadcrumb';
|
8
|
+
import PreviewPanel from '../PreviewPanel/PreviewPanel';
|
9
|
+
import SourceDropdown from '../SourceDropdown/SourceDropdown';
|
10
|
+
|
11
|
+
import { NodeIdentifier, Source, Resource, ResourceDetail, Hierarchy } from '../index';
|
12
|
+
|
13
|
+
interface ResourcePickerContainerProps {
|
14
|
+
title: string;
|
15
|
+
titleAriaProps: DOMAttributes<FocusableElement>;
|
16
|
+
allowedTypes: string[] | undefined;
|
17
|
+
onRequestSources: () => Promise<Source[]>;
|
18
|
+
onRequestChildren(id: NodeIdentifier): Promise<Resource[]>;
|
19
|
+
onRequestResource(id: NodeIdentifier): Promise<ResourceDetail | null>;
|
20
|
+
onChange(resource: NodeIdentifier | null): void;
|
21
|
+
onClose: () => void;
|
22
|
+
}
|
23
|
+
|
24
|
+
function ResourcePickerContainer({
|
25
|
+
title,
|
26
|
+
titleAriaProps,
|
27
|
+
allowedTypes,
|
28
|
+
onRequestSources,
|
29
|
+
onRequestChildren,
|
30
|
+
onRequestResource,
|
31
|
+
onChange,
|
32
|
+
onClose,
|
33
|
+
}: ResourcePickerContainerProps) {
|
34
|
+
const previewModalState = useOverlayTriggerState({});
|
35
|
+
|
36
|
+
const [isSourceLoading, setIsSourceLoading] = useState<boolean>(false);
|
37
|
+
const [isMainLoading, setIsMainLoading] = useState<boolean>(false);
|
38
|
+
const [isSecondaryLoading, setIsSecondaryLoading] = useState<boolean>(false);
|
39
|
+
|
40
|
+
const [currentNode, setCurrentNode] = useState<NodeIdentifier | null>(null);
|
41
|
+
const [selectedId, setSelectedId] = useState<NodeIdentifier | null>(null);
|
42
|
+
const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes<FocusableElement>>({});
|
43
|
+
const [hierarchy, setHierarchy] = useState<Array<Hierarchy>>([]);
|
44
|
+
|
45
|
+
const [sources, setSources] = useState<Array<Source>>([]);
|
46
|
+
const [resources, setResources] = useState<Array<Resource>>([]);
|
47
|
+
const [selectedNodeDetails, setSelectedNodeDetails] = useState<ResourceDetail | null>(null);
|
48
|
+
|
49
|
+
const adjustHierarchy = (node: NodeIdentifier, resetHierarchy: boolean) => {
|
50
|
+
const isInHierarchy = hierarchy.find((hNode) => hNode.id === node);
|
51
|
+
let newHierarchy: Array<Hierarchy> = [];
|
52
|
+
|
53
|
+
// If the node is already in the hierarchy we need to 'jump' back to it
|
54
|
+
if (isInHierarchy) {
|
55
|
+
let reachedNode = false;
|
56
|
+
// Read though the hierarchy and add any nodes before and including the current to the array
|
57
|
+
hierarchy.forEach((hNode) => {
|
58
|
+
if (reachedNode === false) {
|
59
|
+
newHierarchy.push(hNode);
|
60
|
+
if (hNode.id === node) {
|
61
|
+
reachedNode = true;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
});
|
65
|
+
} else {
|
66
|
+
let label: string = resources.find((resource) => resource.id === node)?.label || '';
|
67
|
+
|
68
|
+
// Might be a source
|
69
|
+
if (!label) {
|
70
|
+
const source = sources.find((source) => source.id === node.source);
|
71
|
+
if (source) {
|
72
|
+
label = source.nodes.find((resource) => resource.id === node)?.label || '';
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
// If we are jumping to a complete other spot and the container knows it, it can request a complete reset
|
77
|
+
if (!resetHierarchy) {
|
78
|
+
newHierarchy = hierarchy.slice();
|
79
|
+
}
|
80
|
+
|
81
|
+
newHierarchy.push({
|
82
|
+
id: node,
|
83
|
+
label,
|
84
|
+
});
|
85
|
+
}
|
86
|
+
|
87
|
+
setHierarchy(newHierarchy);
|
88
|
+
};
|
89
|
+
|
90
|
+
const handleResourceSelected = useCallback((node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => {
|
91
|
+
setPreviewModalOverlayProps(overlayProps);
|
92
|
+
setIsSecondaryLoading(true);
|
93
|
+
setSelectedId(node);
|
94
|
+
|
95
|
+
setSelectedNodeDetails(null);
|
96
|
+
|
97
|
+
onRequestResource(node).then((detail: ResourceDetail | null) => {
|
98
|
+
setSelectedNodeDetails(detail);
|
99
|
+
setIsSecondaryLoading(false);
|
100
|
+
});
|
101
|
+
}, []);
|
102
|
+
|
103
|
+
const handleResourceDrillDown = useCallback(
|
104
|
+
(node: NodeIdentifier, resetHierarchy?: boolean) => {
|
105
|
+
setIsMainLoading(true);
|
106
|
+
setCurrentNode(node);
|
107
|
+
adjustHierarchy(node, resetHierarchy || false);
|
108
|
+
|
109
|
+
setSelectedId(null);
|
110
|
+
setSelectedNodeDetails(null);
|
111
|
+
setResources([]);
|
112
|
+
|
113
|
+
onRequestChildren(node).then((resources: Array<Resource>) => {
|
114
|
+
setResources(resources);
|
115
|
+
setIsMainLoading(false);
|
116
|
+
});
|
117
|
+
},
|
118
|
+
[resources, sources],
|
119
|
+
);
|
120
|
+
|
121
|
+
const handleReturnToRoot = useCallback(() => {
|
122
|
+
setCurrentNode(null);
|
123
|
+
setSelectedId(null);
|
124
|
+
setResources([]);
|
125
|
+
setHierarchy([]);
|
126
|
+
|
127
|
+
setSelectedNodeDetails(null);
|
128
|
+
}, []);
|
129
|
+
|
130
|
+
const handleDetailSelect = useCallback((node: NodeIdentifier) => {
|
131
|
+
onChange(node);
|
132
|
+
onClose();
|
133
|
+
}, []);
|
134
|
+
|
135
|
+
const handleDetailClose = useCallback(() => {
|
136
|
+
setSelectedId(null);
|
137
|
+
}, []);
|
138
|
+
|
139
|
+
// On load of component fetch the list of sources
|
140
|
+
useEffect(() => {
|
141
|
+
setIsSourceLoading(true);
|
142
|
+
onRequestSources().then((sources: Array<Source>) => {
|
143
|
+
setSources(sources);
|
144
|
+
setIsSourceLoading(false);
|
145
|
+
});
|
146
|
+
}, []);
|
147
|
+
|
148
|
+
return (
|
149
|
+
<div className="relative flex flex-col h-full">
|
150
|
+
<div className="flex items-center p-6">
|
151
|
+
<h2 {...titleAriaProps} className="text-xl leading-6 text-gray-800 font-semibold mr-6">
|
152
|
+
{title}
|
153
|
+
</h2>
|
154
|
+
<div className="px-3 border-l border-grey-300 w-300px">
|
155
|
+
<SourceDropdown
|
156
|
+
sources={sources}
|
157
|
+
currentSource={hierarchy[0]?.id}
|
158
|
+
isLoading={isSourceLoading}
|
159
|
+
onSourceSelect={handleResourceDrillDown}
|
160
|
+
onRootSelect={handleReturnToRoot}
|
161
|
+
/>
|
162
|
+
</div>
|
163
|
+
<button
|
164
|
+
type="button"
|
165
|
+
aria-label={`Close ${title} dialog`}
|
166
|
+
onClick={onClose}
|
167
|
+
className="absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100"
|
168
|
+
>
|
169
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
170
|
+
<path
|
171
|
+
d="M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z"
|
172
|
+
fill="currentColor"
|
173
|
+
/>
|
174
|
+
</svg>
|
175
|
+
</button>
|
176
|
+
</div>
|
177
|
+
<div className="flex border-t border-grey-300 h-[calc(100%-92px)]">
|
178
|
+
<div className="overflow-y-scroll flex-1 grow-[3] border-r border-gray-300">
|
179
|
+
<h3 className="sr-only">Resource List</h3>
|
180
|
+
{currentNode === null && (
|
181
|
+
<SourceList
|
182
|
+
sources={sources}
|
183
|
+
previewModalState={previewModalState}
|
184
|
+
isLoading={isSourceLoading}
|
185
|
+
onSourceSelect={handleResourceSelected}
|
186
|
+
onSourceDrillDown={handleResourceDrillDown}
|
187
|
+
allowedTypes={allowedTypes}
|
188
|
+
/>
|
189
|
+
)}
|
190
|
+
|
191
|
+
{currentNode && (
|
192
|
+
<>
|
193
|
+
<ResourceBreadcrumb
|
194
|
+
hierarchy={hierarchy}
|
195
|
+
onBreadcrumbSelect={handleResourceDrillDown}
|
196
|
+
onReturnToRoot={handleReturnToRoot}
|
197
|
+
/>
|
198
|
+
<ResourceList
|
199
|
+
previewModalState={previewModalState}
|
200
|
+
resources={resources}
|
201
|
+
isLoading={isMainLoading}
|
202
|
+
onResourceSelect={handleResourceSelected}
|
203
|
+
onResourceDrillDown={handleResourceDrillDown}
|
204
|
+
allowedTypes={allowedTypes}
|
205
|
+
/>
|
206
|
+
</>
|
207
|
+
)}
|
208
|
+
</div>
|
209
|
+
<PreviewPanel
|
210
|
+
node={selectedId}
|
211
|
+
resourceDetail={selectedNodeDetails}
|
212
|
+
modalState={previewModalState}
|
213
|
+
isLoading={isSecondaryLoading}
|
214
|
+
previewModalOverlayProps={previewModalOverlayProps}
|
215
|
+
allowedTypes={allowedTypes}
|
216
|
+
onSelect={handleDetailSelect}
|
217
|
+
onClose={handleDetailClose}
|
218
|
+
/>
|
219
|
+
</div>
|
220
|
+
</div>
|
221
|
+
);
|
222
|
+
}
|
223
|
+
|
224
|
+
export default ResourcePickerContainer;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { screen, render } from '@testing-library/react';
|
3
|
+
|
4
|
+
import { SkeletonList } from './SkeletonList';
|
5
|
+
|
6
|
+
describe('SkeletonList', () => {
|
7
|
+
it('Should render the skeleton list', async () => {
|
8
|
+
render(<SkeletonList itemCount={17} />);
|
9
|
+
|
10
|
+
expect(screen.getByLabelText('Skeleton loader list')).toBeInTheDocument();
|
11
|
+
});
|
12
|
+
|
13
|
+
it('Should list out the amount of items we set as the count', async () => {
|
14
|
+
render(<SkeletonList itemCount={5} />);
|
15
|
+
|
16
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(5);
|
17
|
+
});
|
18
|
+
});
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import { SkeletonList, SkeletonListProps } from './SkeletonList';
|
5
|
+
|
6
|
+
export default {
|
7
|
+
title: 'Global/Skeleton/List',
|
8
|
+
component: SkeletonList,
|
9
|
+
} as Meta<typeof SkeletonList>;
|
10
|
+
|
11
|
+
const Template: StoryFn<SkeletonListProps> = (args: SkeletonListProps) => <SkeletonList {...args} />;
|
12
|
+
|
13
|
+
export const Default = Template.bind({});
|
14
|
+
|
15
|
+
Default.args = {};
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { SkeletonListItem } from '../ListItem/SkeletonListItem';
|
3
|
+
import clsx from 'clsx';
|
4
|
+
|
5
|
+
export type SkeletonListProps = {
|
6
|
+
itemCount: number;
|
7
|
+
className?: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
export const SkeletonList = ({ itemCount = 8, className }: SkeletonListProps) => (
|
11
|
+
<ul className={clsx(`flex flex-col px-7 my-4`, className)} aria-label="Skeleton loader list">
|
12
|
+
{[...Array(itemCount)].map((_item, index: number) => {
|
13
|
+
return <SkeletonListItem key={index} />;
|
14
|
+
})}
|
15
|
+
</ul>
|
16
|
+
);
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import { SkeletonListItem } from './SkeletonListItem';
|
5
|
+
|
6
|
+
export default {
|
7
|
+
title: 'Global/Skeleton/List item',
|
8
|
+
component: SkeletonListItem,
|
9
|
+
} as Meta<typeof SkeletonListItem>;
|
10
|
+
|
11
|
+
const Template: StoryFn<typeof SkeletonListItem> = () => <SkeletonListItem />;
|
12
|
+
|
13
|
+
export const Default = Template.bind({});
|
14
|
+
|
15
|
+
Default.args = {};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export const SkeletonListItem = () => (
|
4
|
+
<li className="flex items-center p-1 first:mt-0 bg-white border border-b-0 border-grey-200 first:rounded-t-lg last:rounded-b-lg last:border-b">
|
5
|
+
<div className="grid grid-cols-[24px_1fr_45px] w-full flex items-center p-4 rounded">
|
6
|
+
<span className="w-6 h-6 bg-gray-200 rounded-full" />
|
7
|
+
<div className="w-full d-flex flex-col mx-4">
|
8
|
+
<div className="mb-1 w-3/4 h-2 bg-gray-200 rounded" />
|
9
|
+
<div className="w-1/2 h-2 bg-gray-200 rounded" />
|
10
|
+
</div>
|
11
|
+
<div className="ml-auto mx-4 w-12 h-2 bg-gray-200 rounded" />
|
12
|
+
</div>
|
13
|
+
</li>
|
14
|
+
);
|
@@ -0,0 +1,263 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
2
|
+
import React from 'react';
|
3
|
+
import { screen, render, waitFor } from '@testing-library/react';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
5
|
+
|
6
|
+
import SourceDropdown from './SourceDropdown';
|
7
|
+
|
8
|
+
const sources = [
|
9
|
+
{
|
10
|
+
id: '1',
|
11
|
+
name: 'Source 1',
|
12
|
+
nodes: [
|
13
|
+
{
|
14
|
+
id: {
|
15
|
+
id: '1',
|
16
|
+
source: '1',
|
17
|
+
},
|
18
|
+
type: 'site',
|
19
|
+
selected: false,
|
20
|
+
label: 'Node 1',
|
21
|
+
childCount: 21,
|
22
|
+
},
|
23
|
+
{
|
24
|
+
id: {
|
25
|
+
id: '2',
|
26
|
+
source: '1',
|
27
|
+
},
|
28
|
+
type: 'site',
|
29
|
+
selected: false,
|
30
|
+
label: 'Node 2',
|
31
|
+
childCount: 13,
|
32
|
+
},
|
33
|
+
],
|
34
|
+
},
|
35
|
+
{
|
36
|
+
id: '2',
|
37
|
+
name: 'Source 2',
|
38
|
+
nodes: [
|
39
|
+
{
|
40
|
+
id: {
|
41
|
+
id: '1',
|
42
|
+
source: '2',
|
43
|
+
},
|
44
|
+
type: 'site',
|
45
|
+
selected: false,
|
46
|
+
label: 'Node 3',
|
47
|
+
childCount: 15,
|
48
|
+
},
|
49
|
+
{
|
50
|
+
id: {
|
51
|
+
id: '2',
|
52
|
+
source: '2',
|
53
|
+
},
|
54
|
+
type: 'site',
|
55
|
+
selected: false,
|
56
|
+
label: 'Node 4',
|
57
|
+
childCount: 10,
|
58
|
+
},
|
59
|
+
],
|
60
|
+
},
|
61
|
+
];
|
62
|
+
|
63
|
+
describe('SourceDropdown', () => {
|
64
|
+
it('Render generic label if no source provided', async () => {
|
65
|
+
render(
|
66
|
+
<SourceDropdown
|
67
|
+
sources={sources}
|
68
|
+
currentSource={null}
|
69
|
+
isLoading={false}
|
70
|
+
onRootSelect={() => {}}
|
71
|
+
onSourceSelect={() => {}}
|
72
|
+
/>,
|
73
|
+
);
|
74
|
+
|
75
|
+
await waitFor(() => {
|
76
|
+
expect(screen.getByRole('button', { name: 'Source quick select' })).toHaveTextContent(
|
77
|
+
'view All available sources',
|
78
|
+
);
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
it('Show loading state if isLoading true', async () => {
|
83
|
+
render(
|
84
|
+
<SourceDropdown
|
85
|
+
sources={sources}
|
86
|
+
currentSource={null}
|
87
|
+
isLoading={true}
|
88
|
+
onRootSelect={() => {}}
|
89
|
+
onSourceSelect={() => {}}
|
90
|
+
/>,
|
91
|
+
);
|
92
|
+
|
93
|
+
await waitFor(() => {
|
94
|
+
expect(screen.getByLabelText('Loading sources')).toBeTruthy();
|
95
|
+
});
|
96
|
+
});
|
97
|
+
|
98
|
+
it('Render title of currentSource if provided', async () => {
|
99
|
+
render(
|
100
|
+
<SourceDropdown
|
101
|
+
sources={sources}
|
102
|
+
currentSource={{
|
103
|
+
id: '2',
|
104
|
+
source: '2',
|
105
|
+
}}
|
106
|
+
isLoading={false}
|
107
|
+
onRootSelect={() => {}}
|
108
|
+
onSourceSelect={() => {}}
|
109
|
+
/>,
|
110
|
+
);
|
111
|
+
|
112
|
+
await waitFor(() => {
|
113
|
+
expect(screen.getByRole('button', { name: 'Source quick select' })).toHaveTextContent('current source Node 4');
|
114
|
+
});
|
115
|
+
});
|
116
|
+
|
117
|
+
it('Sources are rendered when dropdown clicked', async () => {
|
118
|
+
render(
|
119
|
+
<SourceDropdown
|
120
|
+
sources={sources}
|
121
|
+
currentSource={null}
|
122
|
+
isLoading={false}
|
123
|
+
onRootSelect={() => {}}
|
124
|
+
onSourceSelect={() => {}}
|
125
|
+
/>,
|
126
|
+
);
|
127
|
+
|
128
|
+
const user = userEvent.setup();
|
129
|
+
user.click(screen.getByRole('button', { name: 'Source quick select' }));
|
130
|
+
|
131
|
+
await waitFor(() => {
|
132
|
+
expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
|
133
|
+
expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
|
134
|
+
expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
|
135
|
+
expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
|
136
|
+
expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
|
137
|
+
});
|
138
|
+
});
|
139
|
+
|
140
|
+
it('Source menu closes on focus loss', async () => {
|
141
|
+
render(
|
142
|
+
<div>
|
143
|
+
<SourceDropdown
|
144
|
+
sources={sources}
|
145
|
+
currentSource={null}
|
146
|
+
isLoading={false}
|
147
|
+
onRootSelect={() => {}}
|
148
|
+
onSourceSelect={() => {}}
|
149
|
+
/>
|
150
|
+
<input />
|
151
|
+
</div>,
|
152
|
+
);
|
153
|
+
|
154
|
+
const user = userEvent.setup();
|
155
|
+
const buttonDropdown = screen.getByRole('button', { name: 'Source quick select' });
|
156
|
+
user.click(buttonDropdown);
|
157
|
+
|
158
|
+
await waitFor(() => {
|
159
|
+
expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
|
160
|
+
expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
|
161
|
+
expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
|
162
|
+
expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
|
163
|
+
expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
|
164
|
+
});
|
165
|
+
|
166
|
+
user.type(buttonDropdown, '{tab}');
|
167
|
+
user.type(buttonDropdown, '{tab}');
|
168
|
+
user.type(buttonDropdown, '{tab}');
|
169
|
+
user.type(buttonDropdown, '{tab}');
|
170
|
+
user.type(buttonDropdown, '{tab}');
|
171
|
+
|
172
|
+
await waitFor(() => {
|
173
|
+
expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
|
174
|
+
expect(screen.queryByRole('button', { name: 'site Node 1' })).toBeFalsy();
|
175
|
+
expect(screen.queryByRole('button', { name: 'site Node 2' })).toBeFalsy();
|
176
|
+
expect(screen.queryByRole('button', { name: 'site Node 3' })).toBeFalsy();
|
177
|
+
expect(screen.queryByRole('button', { name: 'site Node 4' })).toBeFalsy();
|
178
|
+
});
|
179
|
+
});
|
180
|
+
|
181
|
+
it('Source menu closes on {esc}', async () => {
|
182
|
+
render(
|
183
|
+
<div>
|
184
|
+
<SourceDropdown
|
185
|
+
sources={sources}
|
186
|
+
currentSource={null}
|
187
|
+
isLoading={false}
|
188
|
+
onRootSelect={() => {}}
|
189
|
+
onSourceSelect={() => {}}
|
190
|
+
/>
|
191
|
+
<input />
|
192
|
+
</div>,
|
193
|
+
);
|
194
|
+
|
195
|
+
const user = userEvent.setup();
|
196
|
+
const buttonDropdown = screen.getByRole('button', { name: 'Source quick select' });
|
197
|
+
user.click(buttonDropdown);
|
198
|
+
|
199
|
+
await waitFor(() => {
|
200
|
+
expect(screen.getByRole('button', { name: 'All available sources' })).toBeTruthy();
|
201
|
+
expect(screen.getByRole('button', { name: 'site Node 1' })).toBeTruthy();
|
202
|
+
expect(screen.getByRole('button', { name: 'site Node 2' })).toBeTruthy();
|
203
|
+
expect(screen.getByRole('button', { name: 'site Node 3' })).toBeTruthy();
|
204
|
+
expect(screen.getByRole('button', { name: 'site Node 4' })).toBeTruthy();
|
205
|
+
});
|
206
|
+
|
207
|
+
user.type(buttonDropdown, '{escape}');
|
208
|
+
|
209
|
+
await waitFor(() => {
|
210
|
+
expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
|
211
|
+
expect(screen.queryByRole('button', { name: 'site Node 1' })).toBeFalsy();
|
212
|
+
expect(screen.queryByRole('button', { name: 'site Node 2' })).toBeFalsy();
|
213
|
+
expect(screen.queryByRole('button', { name: 'site Node 3' })).toBeFalsy();
|
214
|
+
expect(screen.queryByRole('button', { name: 'site Node 4' })).toBeFalsy();
|
215
|
+
});
|
216
|
+
});
|
217
|
+
|
218
|
+
it('Selecting all available sources calls onRootSelect ', async () => {
|
219
|
+
const onRootSelect = jest.fn();
|
220
|
+
|
221
|
+
render(
|
222
|
+
<SourceDropdown
|
223
|
+
sources={sources}
|
224
|
+
currentSource={null}
|
225
|
+
isLoading={false}
|
226
|
+
onRootSelect={onRootSelect}
|
227
|
+
onSourceSelect={() => {}}
|
228
|
+
/>,
|
229
|
+
);
|
230
|
+
|
231
|
+
const user = userEvent.setup();
|
232
|
+
await user.click(screen.getByRole('button', { name: 'Source quick select' }));
|
233
|
+
await user.click(screen.getByRole('button', { name: 'All available sources' }));
|
234
|
+
|
235
|
+
await waitFor(() => {
|
236
|
+
expect(onRootSelect).toHaveBeenCalled();
|
237
|
+
expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
|
238
|
+
});
|
239
|
+
});
|
240
|
+
|
241
|
+
it('Selecting node calls onSourceSelect ', async () => {
|
242
|
+
const onSourceSelect = jest.fn();
|
243
|
+
|
244
|
+
render(
|
245
|
+
<SourceDropdown
|
246
|
+
sources={sources}
|
247
|
+
currentSource={null}
|
248
|
+
isLoading={false}
|
249
|
+
onRootSelect={() => {}}
|
250
|
+
onSourceSelect={onSourceSelect}
|
251
|
+
/>,
|
252
|
+
);
|
253
|
+
|
254
|
+
const user = userEvent.setup();
|
255
|
+
await user.click(screen.getByRole('button', { name: 'Source quick select' }));
|
256
|
+
await user.click(screen.getByRole('button', { name: 'site Node 1' }));
|
257
|
+
|
258
|
+
await waitFor(() => {
|
259
|
+
expect(onSourceSelect).toHaveBeenCalledWith({ id: '1', source: '1' }, true);
|
260
|
+
expect(screen.queryByRole('button', { name: 'All available sources' })).toBeFalsy();
|
261
|
+
});
|
262
|
+
});
|
263
|
+
});
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import SourceDropdown from './SourceDropdown';
|
5
|
+
import sampleSources from './sample-sources.json';
|
6
|
+
|
7
|
+
export default {
|
8
|
+
title: 'Source Dropdown',
|
9
|
+
component: SourceDropdown,
|
10
|
+
} as Meta<typeof SourceDropdown>;
|
11
|
+
|
12
|
+
const Template: StoryFn<typeof SourceDropdown> = ({ sources, currentSource, isLoading }) => (
|
13
|
+
<SourceDropdown
|
14
|
+
sources={sources}
|
15
|
+
currentSource={currentSource}
|
16
|
+
isLoading={isLoading}
|
17
|
+
onSourceSelect={({ source, id }) => alert(`Source Select: ${source} - ${id}`)}
|
18
|
+
onRootSelect={() => alert(`Root Select`)}
|
19
|
+
/>
|
20
|
+
);
|
21
|
+
|
22
|
+
export const Primary = Template.bind({});
|
23
|
+
Primary.args = {
|
24
|
+
sources: sampleSources,
|
25
|
+
currentSource: {
|
26
|
+
id: '1',
|
27
|
+
source: '1',
|
28
|
+
},
|
29
|
+
isLoading: false,
|
30
|
+
};
|
31
|
+
|
32
|
+
export const Loading = Template.bind({});
|
33
|
+
Loading.args = {
|
34
|
+
...Primary.args,
|
35
|
+
isLoading: true,
|
36
|
+
};
|