@squiz/resource-browser 1.32.1-alpha.20 → 1.32.1-alpha.21
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/lib/Hooks/useChildResources.d.ts +2 -1
- package/lib/Hooks/useChildResources.js +5 -4
- package/lib/Hooks/useSources.d.ts +1 -1
- package/lib/Hooks/useSources.js +4 -2
- package/lib/Icons/Generics/Error.d.ts +4 -0
- package/lib/Icons/Generics/Error.js +12 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
- package/lib/Icons/Generics/GenericIconMap.js +2 -0
- package/lib/Icons/Generics/Retry.d.ts +4 -0
- package/lib/Icons/Generics/Retry.js +12 -0
- package/lib/Icons/Generics/index.d.ts +2 -0
- package/lib/Icons/Generics/index.js +5 -1
- package/lib/Icons/Icon.d.ts +2 -0
- package/lib/ResourceError/ResourceError.d.ts +7 -0
- package/lib/ResourceError/ResourceError.js +16 -0
- package/lib/ResourceList/ResourceList.d.ts +3 -1
- package/lib/ResourceList/ResourceList.js +4 -1
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +4 -8
- package/lib/SourceList/SourceList.d.ts +3 -1
- package/lib/SourceList/SourceList.js +4 -1
- package/lib/index.css +36 -0
- package/package.json +3 -3
- package/src/Hooks/useChildResources.spec.ts +2 -1
- package/src/Hooks/useChildResources.ts +7 -5
- package/src/Hooks/useSources.spec.ts +0 -1
- package/src/Hooks/useSources.ts +4 -2
- package/src/Icons/Generics/Error.tsx +13 -0
- package/src/Icons/Generics/GenericIconMap.ts +3 -1
- package/src/Icons/Generics/Retry.tsx +13 -0
- package/src/Icons/Generics/index.tsx +2 -0
- package/src/ResourceError/ResourceError.spec.tsx +29 -0
- package/src/ResourceError/ResourceError.tsx +27 -0
- package/src/ResourceList/ResourceList.spec.tsx +45 -0
- package/src/ResourceList/ResourceList.tsx +8 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +18 -6
- package/src/SourceList/SourceList.spec.tsx +49 -0
- package/src/SourceList/SourceList.tsx +8 -0
- package/src/__mocks__/StorybookHelpers.ts +29 -10
- package/src/index.stories.tsx +3 -1
@@ -13,7 +13,8 @@ type UseChildResourcesProps = {
|
|
13
13
|
*/
|
14
14
|
export declare const useChildResources: ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) => {
|
15
15
|
isLoading: boolean;
|
16
|
-
error: Error | null;
|
17
16
|
resources: Resource[];
|
17
|
+
reloadResources: () => void;
|
18
|
+
error: Error | null;
|
18
19
|
};
|
19
20
|
export {};
|
@@ -10,11 +10,10 @@ const react_1 = require("react");
|
|
10
10
|
* @param {Function} onRequestChildren
|
11
11
|
*/
|
12
12
|
const useChildResources = ({ source, currentResource, onRequestChildren }) => {
|
13
|
-
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
14
13
|
const [error, setError] = (0, react_1.useState)(null);
|
14
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
15
15
|
const [resources, setResources] = (0, react_1.useState)([]);
|
16
|
-
|
17
|
-
(0, react_1.useEffect)(() => {
|
16
|
+
const loadResource = (0, react_1.useCallback)(() => {
|
18
17
|
setError(null);
|
19
18
|
setResources([]);
|
20
19
|
if (source) {
|
@@ -30,6 +29,8 @@ const useChildResources = ({ source, currentResource, onRequestChildren }) => {
|
|
30
29
|
});
|
31
30
|
}
|
32
31
|
}, [source, currentResource]);
|
33
|
-
|
32
|
+
// trigger a reload of the resources when the source or the current resource changes.
|
33
|
+
(0, react_1.useEffect)(loadResource, [source, currentResource]);
|
34
|
+
return { isLoading, resources, reloadResources: loadResource, error };
|
34
35
|
};
|
35
36
|
exports.useChildResources = useChildResources;
|
package/lib/Hooks/useSources.js
CHANGED
@@ -8,14 +8,16 @@ const react_1 = require("react");
|
|
8
8
|
* @param {Function} onRequestSources
|
9
9
|
*/
|
10
10
|
const useSources = ({ onRequestSources }) => {
|
11
|
-
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
12
11
|
const [error, setError] = (0, react_1.useState)(null);
|
12
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
13
13
|
const [sources, setSources] = (0, react_1.useState)([]);
|
14
14
|
const loadSources = (0, react_1.useCallback)(() => {
|
15
|
+
setIsLoading(true);
|
15
16
|
onRequestSources()
|
16
17
|
.then((sources) => {
|
17
18
|
setIsLoading(false);
|
18
19
|
setSources(sources);
|
20
|
+
setError(null);
|
19
21
|
})
|
20
22
|
.catch((error) => {
|
21
23
|
setIsLoading(false);
|
@@ -24,6 +26,6 @@ const useSources = ({ onRequestSources }) => {
|
|
24
26
|
}, []);
|
25
27
|
// trigger a load of the sources when the component using the hook is initially rendered.
|
26
28
|
(0, react_1.useEffect)(loadSources, []);
|
27
|
-
return { isLoading,
|
29
|
+
return { isLoading, sources, reload: loadSources, error };
|
28
30
|
};
|
29
31
|
exports.useSources = useSources;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const react_1 = __importDefault(require("react"));
|
7
|
+
function Error({ isDecorative, ...props }) {
|
8
|
+
return (react_1.default.createElement("svg", { width: "48", height: "48", viewBox: "0 0 48 48", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props },
|
9
|
+
react_1.default.createElement("path", { d: "M30.6498 6H17.3698C16.8498 6 16.3298 6.22 15.9698 6.58L6.58977 15.96C6.22977 16.32 6.00977 16.84 6.00977 17.36V30.62C6.00977 31.16 6.22977 31.66 6.58977 32.04L15.9498 41.4C16.3298 41.78 16.8498 42 17.3698 42H30.6298C31.1698 42 31.6698 41.78 32.0498 41.42L41.4098 32.06C41.7898 31.68 41.9898 31.18 41.9898 30.64V17.36C41.9898 16.82 41.7698 16.32 41.4098 15.94L32.0498 6.58C31.6898 6.22 31.1698 6 30.6498 6ZM24.0098 34.6C22.5698 34.6 21.4098 33.44 21.4098 32C21.4098 30.56 22.5698 29.4 24.0098 29.4C25.4498 29.4 26.6098 30.56 26.6098 32C26.6098 33.44 25.4498 34.6 24.0098 34.6ZM24.0098 26C22.9098 26 22.0098 25.1 22.0098 24V16C22.0098 14.9 22.9098 14 24.0098 14C25.1098 14 26.0098 14.9 26.0098 16V24C26.0098 25.1 25.1098 26 24.0098 26Z", fill: "#D72321" }),
|
10
|
+
!isDecorative && react_1.default.createElement("title", null, "error icon")));
|
11
|
+
}
|
12
|
+
exports.default = Error;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close } from '.';
|
1
|
+
import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry } from '.';
|
2
2
|
declare const GenericIconMap: {
|
3
3
|
'arrow-right': typeof ArrowRight;
|
4
4
|
'arrow-down': typeof ArrowDown;
|
@@ -6,5 +6,7 @@ declare const GenericIconMap: {
|
|
6
6
|
selected: typeof Selected;
|
7
7
|
root: typeof Root;
|
8
8
|
close: typeof Close;
|
9
|
+
error: typeof Error;
|
10
|
+
retry: typeof Retry;
|
9
11
|
};
|
10
12
|
export default GenericIconMap;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const react_1 = __importDefault(require("react"));
|
7
|
+
function Retry({ isDecorative, ...props }) {
|
8
|
+
return (react_1.default.createElement("svg", { width: "17", height: "20", viewBox: "0 0 17 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props },
|
9
|
+
react_1.default.createElement("path", { d: "M8.46194 3.64249V0.852487C8.46194 0.402487 7.92194 0.182487 7.61194 0.502487L3.81194 4.29249C3.61194 4.49249 3.61194 4.80249 3.81194 5.00249L7.60194 8.79249C7.92194 9.10249 8.46194 8.88249 8.46194 8.43249V5.64249C12.1919 5.64249 15.1419 9.06249 14.3219 12.9325C13.8519 15.2025 12.0119 17.0325 9.75194 17.5025C6.18194 18.2525 3.00194 15.8025 2.52194 12.4925C2.45194 12.0125 2.03194 11.6425 1.54194 11.6425C0.941942 11.6425 0.461942 12.1725 0.541942 12.7725C1.16194 17.1625 5.34194 20.4125 10.0719 19.4925C13.1919 18.8825 15.7019 16.3725 16.3119 13.2525C17.3019 8.12249 13.4019 3.64249 8.46194 3.64249Z", fill: "#3D3D3D" }),
|
10
|
+
!isDecorative && react_1.default.createElement("title", null, "retry icon")));
|
11
|
+
}
|
12
|
+
exports.default = Retry;
|
@@ -4,3 +4,5 @@ export { default as Selected } from './Selected';
|
|
4
4
|
export { default as Root } from './Root';
|
5
5
|
export { default as ResourceSelect } from './ResourceSelect';
|
6
6
|
export { default as Close } from './Close';
|
7
|
+
export { default as Error } from './Error';
|
8
|
+
export { default as Retry } from './Retry';
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
exports.Close = exports.ResourceSelect = exports.Root = exports.Selected = exports.ArrowDown = exports.ArrowRight = void 0;
|
6
|
+
exports.Retry = exports.Error = exports.Close = exports.ResourceSelect = exports.Root = exports.Selected = exports.ArrowDown = exports.ArrowRight = void 0;
|
7
7
|
// Exports all icons from the Generics folder
|
8
8
|
var ArrowRight_1 = require("./ArrowRight");
|
9
9
|
Object.defineProperty(exports, "ArrowRight", { enumerable: true, get: function () { return __importDefault(ArrowRight_1).default; } });
|
@@ -17,3 +17,7 @@ var ResourceSelect_1 = require("./ResourceSelect");
|
|
17
17
|
Object.defineProperty(exports, "ResourceSelect", { enumerable: true, get: function () { return __importDefault(ResourceSelect_1).default; } });
|
18
18
|
var Close_1 = require("./Close");
|
19
19
|
Object.defineProperty(exports, "Close", { enumerable: true, get: function () { return __importDefault(Close_1).default; } });
|
20
|
+
var Error_1 = require("./Error");
|
21
|
+
Object.defineProperty(exports, "Error", { enumerable: true, get: function () { return __importDefault(Error_1).default; } });
|
22
|
+
var Retry_1 = require("./Retry");
|
23
|
+
Object.defineProperty(exports, "Retry", { enumerable: true, get: function () { return __importDefault(Retry_1).default; } });
|
package/lib/Icons/Icon.d.ts
CHANGED
@@ -7,6 +7,8 @@ export declare const iconSources: {
|
|
7
7
|
selected: typeof import("./Generics").Selected;
|
8
8
|
root: typeof import("./Generics").Root;
|
9
9
|
close: typeof import("./Generics").Close;
|
10
|
+
error: typeof import("./Generics").Error;
|
11
|
+
retry: typeof import("./Generics").Retry;
|
10
12
|
};
|
11
13
|
matrix: {
|
12
14
|
audio_file: typeof import("./MatrixResources").Audio;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const react_1 = __importDefault(require("react"));
|
7
|
+
const Icon_1 = __importDefault(require("../Icons/Icon"));
|
8
|
+
const ResourceError = function ({ errorMessage, handleReload }) {
|
9
|
+
return (react_1.default.createElement("div", { className: "flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3" },
|
10
|
+
react_1.default.createElement(Icon_1.default, { icon: 'error', "aria-hidden": true }),
|
11
|
+
react_1.default.createElement("span", { className: "text-md text-gray-800 font-semibold leading-5" }, errorMessage),
|
12
|
+
react_1.default.createElement("button", { type: "button", onClick: handleReload, className: "flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 w-[119px] h-9 mt-3 rounded text-md font-bold text-gray-700" },
|
13
|
+
react_1.default.createElement(Icon_1.default, { icon: 'retry', "aria-hidden": true }),
|
14
|
+
" Try again")));
|
15
|
+
};
|
16
|
+
exports.default = ResourceError;
|
@@ -10,6 +10,8 @@ export interface ResourceListProps {
|
|
10
10
|
onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
|
11
11
|
onResourceDrillDown: (resource: Resource) => void;
|
12
12
|
allowedTypes?: string[] | undefined;
|
13
|
+
error: Error | null;
|
14
|
+
handleReload: () => void;
|
13
15
|
}
|
14
|
-
declare const ResourceList: ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }: ResourceListProps) => JSX.Element;
|
16
|
+
declare const ResourceList: ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, error, handleReload, }: ResourceListProps) => JSX.Element;
|
15
17
|
export default ResourceList;
|
@@ -29,7 +29,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
const react_1 = __importStar(require("react"));
|
30
30
|
const ResourceItem_1 = __importDefault(require("../ResourceItem/ResourceItem"));
|
31
31
|
const SkeletonListItem_1 = require("../Skeleton/ListItem/SkeletonListItem");
|
32
|
-
const
|
32
|
+
const ResourceError_1 = __importDefault(require("../ResourceError/ResourceError"));
|
33
|
+
const ResourceList = function ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, error, handleReload, }) {
|
33
34
|
const listRef = (0, react_1.useRef)(null);
|
34
35
|
// When resources change, because we are on a new page, reset focus to the list
|
35
36
|
(0, react_1.useEffect)(() => {
|
@@ -43,7 +44,9 @@ const ResourceList = function ({ resources, selectedResource, previewModalState,
|
|
43
44
|
isLoading && (react_1.default.createElement(react_1.default.Fragment, null, [...Array(8)].map((_item, index) => {
|
44
45
|
return react_1.default.createElement(SkeletonListItem_1.SkeletonListItem, { key: index });
|
45
46
|
}))),
|
47
|
+
!isLoading && error && react_1.default.createElement(ResourceError_1.default, { errorMessage: error.message, handleReload: handleReload }),
|
46
48
|
!isLoading &&
|
49
|
+
!error &&
|
47
50
|
resources.map((resource) => {
|
48
51
|
return (react_1.default.createElement(ResourceItem_1.default, { key: resource.id, item: resource, selected: resource.id == selectedResource?.id, label: resource.name, type: resource.type.code, childCount: resource.childCount, previewModalState: previewModalState, onSelect: onResourceSelect, onDrillDown: onResourceDrillDown, className: "border-b-0 first:mt-0 first:rounded-t-lg last:rounded-b-lg last:border-b", allowedTypes: allowedTypes }));
|
49
52
|
})));
|
@@ -41,12 +41,8 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
41
41
|
const [selectedResource, setSelectedResource] = (0, react_1.useState)(null);
|
42
42
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = (0, react_1.useState)({});
|
43
43
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = (0, useResourcePath_1.useResourcePath)();
|
44
|
-
const { isLoading: isSourceLoading,
|
45
|
-
const { isLoading: isResourcesLoading,
|
46
|
-
source,
|
47
|
-
currentResource,
|
48
|
-
onRequestChildren,
|
49
|
-
});
|
44
|
+
const { sources, isLoading: isSourceLoading, reload: handleSourceReload, error: sourceError, } = (0, useSources_1.useSources)({ onRequestSources });
|
45
|
+
const { resources, isLoading: isResourcesLoading, reloadResources: handleResourceReload, error: resourceError, } = (0, useChildResources_1.useChildResources)({ source, currentResource, onRequestChildren });
|
50
46
|
const handleResourceDrillDown = (0, react_1.useCallback)((resource) => {
|
51
47
|
push(resource);
|
52
48
|
}, [push]);
|
@@ -83,8 +79,8 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
83
79
|
react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
|
84
80
|
react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
|
85
81
|
hierarchy.length > 0 && (react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: popUntil, onReturnToRoot: handleReturnToRoot })),
|
86
|
-
!source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, onSourceDrillDown: handleSourceDrilldown, allowedTypes: allowedTypes })),
|
87
|
-
source && (react_1.default.createElement(ResourceList_1.default, { previewModalState: previewModalState, resources: resources, selectedResource: selectedResource, isLoading: isResourcesLoading, onResourceSelect: handleResourceSelected, onResourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes }))),
|
82
|
+
!source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, onSourceDrillDown: handleSourceDrilldown, allowedTypes: allowedTypes, handleReload: handleSourceReload, error: sourceError })),
|
83
|
+
source && (react_1.default.createElement(ResourceList_1.default, { previewModalState: previewModalState, resources: resources, selectedResource: selectedResource, isLoading: isResourcesLoading, onResourceSelect: handleResourceSelected, onResourceDrillDown: handleResourceDrillDown, allowedTypes: allowedTypes, handleReload: handleResourceReload, error: resourceError }))),
|
88
84
|
react_1.default.createElement(PreviewPanel_1.default, { resource: selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose }))));
|
89
85
|
}
|
90
86
|
exports.default = ResourcePickerContainer;
|
@@ -9,6 +9,8 @@ export interface SourceListProps {
|
|
9
9
|
onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
|
10
10
|
onSourceDrillDown: (node: ScopedSource) => void;
|
11
11
|
allowedTypes?: string[] | undefined;
|
12
|
+
handleReload: () => void;
|
13
|
+
error: Error | null;
|
12
14
|
}
|
13
|
-
declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, }: SourceListProps) => JSX.Element;
|
15
|
+
declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, handleReload, error, }: SourceListProps) => JSX.Element;
|
14
16
|
export default SourceList;
|
@@ -31,7 +31,8 @@ const ResourceItem_1 = __importDefault(require("../ResourceItem/ResourceItem"));
|
|
31
31
|
const SkeletonList_1 = require("../Skeleton/List/SkeletonList");
|
32
32
|
const clsx_1 = __importDefault(require("clsx"));
|
33
33
|
const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
|
34
|
-
const
|
34
|
+
const ResourceError_1 = __importDefault(require("../ResourceError/ResourceError"));
|
35
|
+
const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, handleReload, error, }) {
|
35
36
|
const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
|
36
37
|
const listRef = (0, react_1.useRef)(null);
|
37
38
|
(0, react_1.useEffect)(() => {
|
@@ -47,7 +48,9 @@ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSe
|
|
47
48
|
react_1.default.createElement(SkeletonList_1.SkeletonList, { itemCount: 3 })),
|
48
49
|
react_1.default.createElement("li", null,
|
49
50
|
react_1.default.createElement(SkeletonList_1.SkeletonList, { itemCount: 3 })))),
|
51
|
+
!isLoading && error && react_1.default.createElement(ResourceError_1.default, { errorMessage: error.message, handleReload: handleReload }),
|
50
52
|
!isLoading &&
|
53
|
+
!error &&
|
51
54
|
categorisedSources.map(({ key, label, sources }, index) => {
|
52
55
|
return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}` },
|
53
56
|
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" },
|
package/lib/index.css
CHANGED
@@ -529,6 +529,9 @@
|
|
529
529
|
.squiz-rb-scope .h-6 {
|
530
530
|
height: 1.5rem;
|
531
531
|
}
|
532
|
+
.squiz-rb-scope .h-9 {
|
533
|
+
height: 2.25rem;
|
534
|
+
}
|
532
535
|
.squiz-rb-scope .h-\[50vh\] {
|
533
536
|
height: 50vh;
|
534
537
|
}
|
@@ -574,6 +577,9 @@
|
|
574
577
|
.squiz-rb-scope .w-72 {
|
575
578
|
width: 18rem;
|
576
579
|
}
|
580
|
+
.squiz-rb-scope .w-\[119px\] {
|
581
|
+
width: 119px;
|
582
|
+
}
|
577
583
|
.squiz-rb-scope .w-\[60px\] {
|
578
584
|
width: 60px;
|
579
585
|
}
|
@@ -613,6 +619,9 @@
|
|
613
619
|
.squiz-rb-scope .grid-cols-\[24px_1fr_45px\] {
|
614
620
|
grid-template-columns: 24px 1fr 45px;
|
615
621
|
}
|
622
|
+
.squiz-rb-scope .flex-row {
|
623
|
+
flex-direction: row;
|
624
|
+
}
|
616
625
|
.squiz-rb-scope .flex-col {
|
617
626
|
flex-direction: column;
|
618
627
|
}
|
@@ -631,6 +640,9 @@
|
|
631
640
|
.squiz-rb-scope .justify-center {
|
632
641
|
justify-content: center;
|
633
642
|
}
|
643
|
+
.squiz-rb-scope .gap-3 {
|
644
|
+
gap: 0.75rem;
|
645
|
+
}
|
634
646
|
.squiz-rb-scope .overflow-y-scroll {
|
635
647
|
overflow-y: scroll;
|
636
648
|
}
|
@@ -686,6 +698,10 @@
|
|
686
698
|
.squiz-rb-scope .border-opacity-20 {
|
687
699
|
--tw-border-opacity: 0.2;
|
688
700
|
}
|
701
|
+
.squiz-rb-scope .bg-black {
|
702
|
+
--tw-bg-opacity: 1;
|
703
|
+
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
704
|
+
}
|
689
705
|
.squiz-rb-scope .bg-blue-100 {
|
690
706
|
--tw-bg-opacity: 1;
|
691
707
|
background-color: rgb(230 241 250 / var(--tw-bg-opacity));
|
@@ -709,6 +725,9 @@
|
|
709
725
|
--tw-bg-opacity: 1;
|
710
726
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
711
727
|
}
|
728
|
+
.squiz-rb-scope .bg-opacity-10 {
|
729
|
+
--tw-bg-opacity: 0.1;
|
730
|
+
}
|
712
731
|
.squiz-rb-scope .p-1 {
|
713
732
|
padding: 0.25rem;
|
714
733
|
}
|
@@ -748,6 +767,10 @@
|
|
748
767
|
padding-top: 1rem;
|
749
768
|
padding-bottom: 1rem;
|
750
769
|
}
|
770
|
+
.squiz-rb-scope .py-8 {
|
771
|
+
padding-top: 2rem;
|
772
|
+
padding-bottom: 2rem;
|
773
|
+
}
|
751
774
|
.squiz-rb-scope .pb-4 {
|
752
775
|
padding-bottom: 1rem;
|
753
776
|
}
|
@@ -763,6 +786,9 @@
|
|
763
786
|
.squiz-rb-scope .text-base {
|
764
787
|
font-size: 1rem;
|
765
788
|
}
|
789
|
+
.squiz-rb-scope .text-md {
|
790
|
+
font-size: 0.875rem;
|
791
|
+
}
|
766
792
|
.squiz-rb-scope .text-sm {
|
767
793
|
font-size: 0.8125rem;
|
768
794
|
}
|
@@ -770,12 +796,18 @@
|
|
770
796
|
font-size: 1.25rem;
|
771
797
|
line-height: 1.75rem;
|
772
798
|
}
|
799
|
+
.squiz-rb-scope .font-bold {
|
800
|
+
font-weight: 700;
|
801
|
+
}
|
773
802
|
.squiz-rb-scope .font-normal {
|
774
803
|
font-weight: 400;
|
775
804
|
}
|
776
805
|
.squiz-rb-scope .font-semibold {
|
777
806
|
font-weight: 600;
|
778
807
|
}
|
808
|
+
.squiz-rb-scope .leading-5 {
|
809
|
+
line-height: 1.25rem;
|
810
|
+
}
|
779
811
|
.squiz-rb-scope .leading-6 {
|
780
812
|
line-height: 1.5rem;
|
781
813
|
}
|
@@ -791,6 +823,10 @@
|
|
791
823
|
--tw-text-opacity: 1;
|
792
824
|
color: rgb(112 112 112 / var(--tw-text-opacity));
|
793
825
|
}
|
826
|
+
.squiz-rb-scope .text-gray-700 {
|
827
|
+
--tw-text-opacity: 1;
|
828
|
+
color: rgb(79 79 79 / var(--tw-text-opacity));
|
829
|
+
}
|
794
830
|
.squiz-rb-scope .text-gray-800 {
|
795
831
|
--tw-text-opacity: 1;
|
796
832
|
color: rgb(61 61 61 / var(--tw-text-opacity));
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/resource-browser",
|
3
|
-
"version": "1.32.1-alpha.
|
3
|
+
"version": "1.32.1-alpha.21",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"scripts": {
|
@@ -14,7 +14,7 @@
|
|
14
14
|
"clean": "rimraf ./lib"
|
15
15
|
},
|
16
16
|
"dependencies": {
|
17
|
-
"@squiz/dx-json-schema-lib": "1.32.1-alpha.
|
17
|
+
"@squiz/dx-json-schema-lib": "1.32.1-alpha.21",
|
18
18
|
"react-aria": "3.23.1",
|
19
19
|
"react-responsive": "9.0.2",
|
20
20
|
"react-stately": "3.21.0"
|
@@ -71,5 +71,5 @@
|
|
71
71
|
"volta": {
|
72
72
|
"node": "18.15.0"
|
73
73
|
},
|
74
|
-
"gitHead": "
|
74
|
+
"gitHead": "c197f8d0d9e326d6a3fcaa5c417d3918a4a62234"
|
75
75
|
}
|
@@ -8,6 +8,7 @@ describe('useChildResources', () => {
|
|
8
8
|
const currentResource = mockResource({ name: 'Current resource' });
|
9
9
|
const children = [mockResource({ name: 'Child 1' })];
|
10
10
|
const onRequestChildren = jest.fn().mockResolvedValue(children);
|
11
|
+
|
11
12
|
const { result } = renderHook(() =>
|
12
13
|
useChildResources({
|
13
14
|
source,
|
@@ -23,7 +24,6 @@ describe('useChildResources', () => {
|
|
23
24
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
24
25
|
|
25
26
|
expect(result.current.isLoading).toBe(false);
|
26
|
-
expect(result.current.error).toBe(null);
|
27
27
|
expect(result.current.resources).toBe(children);
|
28
28
|
});
|
29
29
|
|
@@ -32,6 +32,7 @@ describe('useChildResources', () => {
|
|
32
32
|
const currentResource = mockResource({ name: 'Current resource' });
|
33
33
|
const error = new Error('Loading the resources failed.');
|
34
34
|
const onRequestChildren = jest.fn().mockRejectedValue(error);
|
35
|
+
|
35
36
|
const { result } = renderHook(() =>
|
36
37
|
useChildResources({
|
37
38
|
source,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { useEffect, useState } from 'react';
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
2
2
|
import { Resource, ScopedSource, Source } from '../types';
|
3
3
|
|
4
4
|
type UseChildResourcesProps = {
|
@@ -15,12 +15,11 @@ type UseChildResourcesProps = {
|
|
15
15
|
* @param {Function} onRequestChildren
|
16
16
|
*/
|
17
17
|
export const useChildResources = ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) => {
|
18
|
-
const [isLoading, setIsLoading] = useState(false);
|
19
18
|
const [error, setError] = useState<Error | null>(null);
|
19
|
+
const [isLoading, setIsLoading] = useState(false);
|
20
20
|
const [resources, setResources] = useState<Resource[]>([]);
|
21
21
|
|
22
|
-
|
23
|
-
useEffect(() => {
|
22
|
+
const loadResource = useCallback(() => {
|
24
23
|
setError(null);
|
25
24
|
setResources([]);
|
26
25
|
|
@@ -39,5 +38,8 @@ export const useChildResources = ({ source, currentResource, onRequestChildren }
|
|
39
38
|
}
|
40
39
|
}, [source, currentResource]);
|
41
40
|
|
42
|
-
|
41
|
+
// trigger a reload of the resources when the source or the current resource changes.
|
42
|
+
useEffect(loadResource, [source, currentResource]);
|
43
|
+
|
44
|
+
return { isLoading, resources, reloadResources: loadResource, error };
|
43
45
|
};
|
package/src/Hooks/useSources.ts
CHANGED
@@ -11,14 +11,16 @@ type UseSourcesProps = {
|
|
11
11
|
* @param {Function} onRequestSources
|
12
12
|
*/
|
13
13
|
export const useSources = ({ onRequestSources }: UseSourcesProps) => {
|
14
|
-
const [isLoading, setIsLoading] = useState(true);
|
15
14
|
const [error, setError] = useState<Error | null>(null);
|
15
|
+
const [isLoading, setIsLoading] = useState(true);
|
16
16
|
const [sources, setSources] = useState<Source[]>([]);
|
17
17
|
const loadSources = useCallback(() => {
|
18
|
+
setIsLoading(true);
|
18
19
|
onRequestSources()
|
19
20
|
.then((sources) => {
|
20
21
|
setIsLoading(false);
|
21
22
|
setSources(sources);
|
23
|
+
setError(null);
|
22
24
|
})
|
23
25
|
.catch((error) => {
|
24
26
|
setIsLoading(false);
|
@@ -29,5 +31,5 @@ export const useSources = ({ onRequestSources }: UseSourcesProps) => {
|
|
29
31
|
// trigger a load of the sources when the component using the hook is initially rendered.
|
30
32
|
useEffect(loadSources, []);
|
31
33
|
|
32
|
-
return { isLoading,
|
34
|
+
return { isLoading, sources, reload: loadSources, error };
|
33
35
|
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export default function Error({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
|
4
|
+
return (
|
5
|
+
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
6
|
+
<path
|
7
|
+
d="M30.6498 6H17.3698C16.8498 6 16.3298 6.22 15.9698 6.58L6.58977 15.96C6.22977 16.32 6.00977 16.84 6.00977 17.36V30.62C6.00977 31.16 6.22977 31.66 6.58977 32.04L15.9498 41.4C16.3298 41.78 16.8498 42 17.3698 42H30.6298C31.1698 42 31.6698 41.78 32.0498 41.42L41.4098 32.06C41.7898 31.68 41.9898 31.18 41.9898 30.64V17.36C41.9898 16.82 41.7698 16.32 41.4098 15.94L32.0498 6.58C31.6898 6.22 31.1698 6 30.6498 6ZM24.0098 34.6C22.5698 34.6 21.4098 33.44 21.4098 32C21.4098 30.56 22.5698 29.4 24.0098 29.4C25.4498 29.4 26.6098 30.56 26.6098 32C26.6098 33.44 25.4498 34.6 24.0098 34.6ZM24.0098 26C22.9098 26 22.0098 25.1 22.0098 24V16C22.0098 14.9 22.9098 14 24.0098 14C25.1098 14 26.0098 14.9 26.0098 16V24C26.0098 25.1 25.1098 26 24.0098 26Z"
|
8
|
+
fill="#D72321"
|
9
|
+
/>
|
10
|
+
{!isDecorative && <title>error icon</title>}
|
11
|
+
</svg>
|
12
|
+
);
|
13
|
+
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close } from '.';
|
1
|
+
import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry } from '.';
|
2
2
|
|
3
3
|
// Define our map of matrix types to icons
|
4
4
|
const GenericIconMap = {
|
@@ -8,6 +8,8 @@ const GenericIconMap = {
|
|
8
8
|
selected: Selected,
|
9
9
|
root: Root,
|
10
10
|
close: Close,
|
11
|
+
error: Error,
|
12
|
+
retry: Retry,
|
11
13
|
};
|
12
14
|
|
13
15
|
// Export our map
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
export default function Retry({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
|
4
|
+
return (
|
5
|
+
<svg width="17" height="20" viewBox="0 0 17 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
6
|
+
<path
|
7
|
+
d="M8.46194 3.64249V0.852487C8.46194 0.402487 7.92194 0.182487 7.61194 0.502487L3.81194 4.29249C3.61194 4.49249 3.61194 4.80249 3.81194 5.00249L7.60194 8.79249C7.92194 9.10249 8.46194 8.88249 8.46194 8.43249V5.64249C12.1919 5.64249 15.1419 9.06249 14.3219 12.9325C13.8519 15.2025 12.0119 17.0325 9.75194 17.5025C6.18194 18.2525 3.00194 15.8025 2.52194 12.4925C2.45194 12.0125 2.03194 11.6425 1.54194 11.6425C0.941942 11.6425 0.461942 12.1725 0.541942 12.7725C1.16194 17.1625 5.34194 20.4125 10.0719 19.4925C13.1919 18.8825 15.7019 16.3725 16.3119 13.2525C17.3019 8.12249 13.4019 3.64249 8.46194 3.64249Z"
|
8
|
+
fill="#3D3D3D"
|
9
|
+
/>
|
10
|
+
{!isDecorative && <title>retry icon</title>}
|
11
|
+
</svg>
|
12
|
+
);
|
13
|
+
}
|
@@ -5,3 +5,5 @@ export { default as Selected } from './Selected';
|
|
5
5
|
export { default as Root } from './Root';
|
6
6
|
export { default as ResourceSelect } from './ResourceSelect';
|
7
7
|
export { default as Close } from './Close';
|
8
|
+
export { default as Error } from './Error';
|
9
|
+
export { default as Retry } from './Retry';
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import ResourceError from './ResourceError';
|
4
|
+
|
5
|
+
const defaultProps: any = {
|
6
|
+
errorMessage: 'This is a test error!',
|
7
|
+
handleReload: jest.fn(),
|
8
|
+
};
|
9
|
+
|
10
|
+
describe('ResourceError', () => {
|
11
|
+
afterEach(() => {
|
12
|
+
jest.clearAllMocks();
|
13
|
+
});
|
14
|
+
|
15
|
+
it('should render the component with the correct error message', () => {
|
16
|
+
const { getByText } = render(<ResourceError {...defaultProps} />);
|
17
|
+
const errorMessage = getByText(defaultProps.errorMessage);
|
18
|
+
|
19
|
+
expect(errorMessage).toBeInTheDocument();
|
20
|
+
});
|
21
|
+
|
22
|
+
it('should call the reload function when the button is pressed', () => {
|
23
|
+
const { getByRole } = render(<ResourceError {...defaultProps} />);
|
24
|
+
const buttonElement = getByRole('button');
|
25
|
+
fireEvent.click(buttonElement);
|
26
|
+
|
27
|
+
expect(defaultProps.handleReload).toHaveBeenCalledTimes(1);
|
28
|
+
});
|
29
|
+
});
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import Icon, { IconOptions } from '../Icons/Icon';
|
3
|
+
|
4
|
+
interface ResourceError {
|
5
|
+
errorMessage: string;
|
6
|
+
handleReload: () => void;
|
7
|
+
}
|
8
|
+
|
9
|
+
const ResourceError = function ({ errorMessage, handleReload }: ResourceError) {
|
10
|
+
return (
|
11
|
+
<div className="flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
|
12
|
+
<Icon icon={'error' as IconOptions} aria-hidden />
|
13
|
+
{/* Error message */}
|
14
|
+
<span className="text-md text-gray-800 font-semibold leading-5">{errorMessage}</span>
|
15
|
+
{/* Retry button */}
|
16
|
+
<button
|
17
|
+
type="button"
|
18
|
+
onClick={handleReload}
|
19
|
+
className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 w-[119px] h-9 mt-3 rounded text-md font-bold text-gray-700"
|
20
|
+
>
|
21
|
+
<Icon icon={'retry' as IconOptions} aria-hidden /> Try again
|
22
|
+
</button>
|
23
|
+
</div>
|
24
|
+
);
|
25
|
+
};
|
26
|
+
|
27
|
+
export default ResourceError;
|
@@ -25,6 +25,8 @@ function ResourceListTestWrapper({
|
|
25
25
|
|
26
26
|
describe('ResourceList', () => {
|
27
27
|
it('Shows loading when isLoading true', async () => {
|
28
|
+
const reload = jest.fn();
|
29
|
+
|
28
30
|
render(
|
29
31
|
<ResourceListTestWrapper
|
30
32
|
constructFunction={(previewModalState) => {
|
@@ -35,6 +37,8 @@ describe('ResourceList', () => {
|
|
35
37
|
isLoading={true}
|
36
38
|
onResourceSelect={() => {}}
|
37
39
|
onResourceDrillDown={() => {}}
|
40
|
+
error={null}
|
41
|
+
handleReload={reload}
|
38
42
|
/>
|
39
43
|
);
|
40
44
|
}}
|
@@ -47,6 +51,8 @@ describe('ResourceList', () => {
|
|
47
51
|
});
|
48
52
|
|
49
53
|
it('Focus is moved to the resource list', async () => {
|
54
|
+
const reload = jest.fn();
|
55
|
+
|
50
56
|
render(
|
51
57
|
<ResourceListTestWrapper
|
52
58
|
constructFunction={(previewModalState) => {
|
@@ -57,6 +63,8 @@ describe('ResourceList', () => {
|
|
57
63
|
isLoading={false}
|
58
64
|
onResourceSelect={() => {}}
|
59
65
|
onResourceDrillDown={() => {}}
|
66
|
+
error={null}
|
67
|
+
handleReload={reload}
|
60
68
|
/>
|
61
69
|
);
|
62
70
|
}}
|
@@ -69,6 +77,8 @@ describe('ResourceList', () => {
|
|
69
77
|
});
|
70
78
|
|
71
79
|
it('Resource list render each resource', async () => {
|
80
|
+
const reload = jest.fn();
|
81
|
+
|
72
82
|
render(
|
73
83
|
<ResourceListTestWrapper
|
74
84
|
constructFunction={(previewModalState) => {
|
@@ -79,6 +89,8 @@ describe('ResourceList', () => {
|
|
79
89
|
isLoading={false}
|
80
90
|
onResourceSelect={() => {}}
|
81
91
|
onResourceDrillDown={() => {}}
|
92
|
+
error={null}
|
93
|
+
handleReload={reload}
|
82
94
|
/>
|
83
95
|
);
|
84
96
|
}}
|
@@ -96,6 +108,7 @@ describe('ResourceList', () => {
|
|
96
108
|
|
97
109
|
it('Clicking resource body triggers correct onResourceSelect', async () => {
|
98
110
|
const onResourceSelect = jest.fn();
|
111
|
+
const reload = jest.fn();
|
99
112
|
|
100
113
|
render(
|
101
114
|
<ResourceListTestWrapper
|
@@ -107,6 +120,8 @@ describe('ResourceList', () => {
|
|
107
120
|
isLoading={false}
|
108
121
|
onResourceSelect={onResourceSelect}
|
109
122
|
onResourceDrillDown={() => {}}
|
123
|
+
error={null}
|
124
|
+
handleReload={reload}
|
110
125
|
/>
|
111
126
|
);
|
112
127
|
}}
|
@@ -124,6 +139,7 @@ describe('ResourceList', () => {
|
|
124
139
|
|
125
140
|
it('Clicking node child count triggers correct onResourceDrillDown', async () => {
|
126
141
|
const onResourceDrillDown = jest.fn();
|
142
|
+
const reload = jest.fn();
|
127
143
|
|
128
144
|
render(
|
129
145
|
<ResourceListTestWrapper
|
@@ -135,6 +151,8 @@ describe('ResourceList', () => {
|
|
135
151
|
isLoading={false}
|
136
152
|
onResourceSelect={() => {}}
|
137
153
|
onResourceDrillDown={onResourceDrillDown}
|
154
|
+
error={null}
|
155
|
+
handleReload={reload}
|
138
156
|
/>
|
139
157
|
);
|
140
158
|
}}
|
@@ -148,4 +166,31 @@ describe('ResourceList', () => {
|
|
148
166
|
expect(onResourceDrillDown).toHaveBeenCalledWith(resources[2]);
|
149
167
|
});
|
150
168
|
});
|
169
|
+
|
170
|
+
it('Renders error state when an error occurs loading resource list', async () => {
|
171
|
+
const reload = jest.fn();
|
172
|
+
|
173
|
+
const { getByText } = render(
|
174
|
+
<ResourceListTestWrapper
|
175
|
+
constructFunction={(previewModalState) => {
|
176
|
+
return (
|
177
|
+
<ResourceList
|
178
|
+
resources={resources}
|
179
|
+
previewModalState={previewModalState}
|
180
|
+
isLoading={false}
|
181
|
+
onResourceSelect={() => {}}
|
182
|
+
onResourceDrillDown={() => {}}
|
183
|
+
error={new Error('This is a resource error!')}
|
184
|
+
handleReload={reload}
|
185
|
+
/>
|
186
|
+
);
|
187
|
+
}}
|
188
|
+
/>,
|
189
|
+
);
|
190
|
+
|
191
|
+
await waitFor(() => {
|
192
|
+
const errorMessage = getByText('This is a resource error!');
|
193
|
+
expect(errorMessage).toBeInTheDocument();
|
194
|
+
});
|
195
|
+
});
|
151
196
|
});
|
@@ -5,6 +5,7 @@ import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
|
5
5
|
import ResourceItem from '../ResourceItem/ResourceItem';
|
6
6
|
import { Resource } from '../types';
|
7
7
|
import { SkeletonListItem } from '../Skeleton/ListItem/SkeletonListItem';
|
8
|
+
import ResourceError from '../ResourceError/ResourceError';
|
8
9
|
|
9
10
|
export interface ResourceListProps {
|
10
11
|
resources: Array<Resource>;
|
@@ -14,6 +15,8 @@ export interface ResourceListProps {
|
|
14
15
|
onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
|
15
16
|
onResourceDrillDown: (resource: Resource) => void;
|
16
17
|
allowedTypes?: string[] | undefined;
|
18
|
+
error: Error | null;
|
19
|
+
handleReload: () => void;
|
17
20
|
}
|
18
21
|
|
19
22
|
const ResourceList = function ({
|
@@ -24,6 +27,8 @@ const ResourceList = function ({
|
|
24
27
|
onResourceSelect,
|
25
28
|
onResourceDrillDown,
|
26
29
|
allowedTypes,
|
30
|
+
error,
|
31
|
+
handleReload,
|
27
32
|
}: ResourceListProps) {
|
28
33
|
const listRef = useRef<HTMLUListElement>(null);
|
29
34
|
|
@@ -51,7 +56,10 @@ const ResourceList = function ({
|
|
51
56
|
</>
|
52
57
|
)}
|
53
58
|
|
59
|
+
{!isLoading && error && <ResourceError errorMessage={error.message} handleReload={handleReload} />}
|
60
|
+
|
54
61
|
{!isLoading &&
|
62
|
+
!error &&
|
55
63
|
resources.map((resource) => {
|
56
64
|
return (
|
57
65
|
<ResourceItem
|
@@ -36,12 +36,20 @@ function ResourcePickerContainer({
|
|
36
36
|
const [selectedResource, setSelectedResource] = useState<Resource | null>(null);
|
37
37
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = useState<DOMAttributes>({});
|
38
38
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = useResourcePath();
|
39
|
-
|
40
|
-
const {
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
|
40
|
+
const {
|
41
|
+
sources,
|
42
|
+
isLoading: isSourceLoading,
|
43
|
+
reload: handleSourceReload,
|
44
|
+
error: sourceError,
|
45
|
+
} = useSources({ onRequestSources });
|
46
|
+
|
47
|
+
const {
|
48
|
+
resources,
|
49
|
+
isLoading: isResourcesLoading,
|
50
|
+
reloadResources: handleResourceReload,
|
51
|
+
error: resourceError,
|
52
|
+
} = useChildResources({ source, currentResource, onRequestChildren });
|
45
53
|
|
46
54
|
const handleResourceDrillDown = useCallback(
|
47
55
|
(resource: Resource) => {
|
@@ -130,6 +138,8 @@ function ResourcePickerContainer({
|
|
130
138
|
onSourceSelect={handleSourceDrilldown}
|
131
139
|
onSourceDrillDown={handleSourceDrilldown}
|
132
140
|
allowedTypes={allowedTypes}
|
141
|
+
handleReload={handleSourceReload}
|
142
|
+
error={sourceError}
|
133
143
|
/>
|
134
144
|
)}
|
135
145
|
{source && (
|
@@ -141,6 +151,8 @@ function ResourcePickerContainer({
|
|
141
151
|
onResourceSelect={handleResourceSelected}
|
142
152
|
onResourceDrillDown={handleResourceDrillDown}
|
143
153
|
allowedTypes={allowedTypes}
|
154
|
+
handleReload={handleResourceReload}
|
155
|
+
error={resourceError}
|
144
156
|
/>
|
145
157
|
)}
|
146
158
|
</div>
|
@@ -69,6 +69,8 @@ function SourceListTestWrapper({
|
|
69
69
|
|
70
70
|
describe('SourceList', () => {
|
71
71
|
it('Shows loading when isLoading is true', async () => {
|
72
|
+
const reload = jest.fn();
|
73
|
+
|
72
74
|
render(
|
73
75
|
<SourceListTestWrapper
|
74
76
|
constructFunction={(previewModalState) => {
|
@@ -79,6 +81,8 @@ describe('SourceList', () => {
|
|
79
81
|
isLoading={true}
|
80
82
|
onSourceSelect={() => {}}
|
81
83
|
onSourceDrillDown={() => {}}
|
84
|
+
error={null}
|
85
|
+
handleReload={reload}
|
82
86
|
/>
|
83
87
|
);
|
84
88
|
}}
|
@@ -91,6 +95,8 @@ describe('SourceList', () => {
|
|
91
95
|
});
|
92
96
|
|
93
97
|
it('Source list render each source', async () => {
|
98
|
+
const reload = jest.fn();
|
99
|
+
|
94
100
|
render(
|
95
101
|
<SourceListTestWrapper
|
96
102
|
constructFunction={(previewModalState) => {
|
@@ -101,6 +107,8 @@ describe('SourceList', () => {
|
|
101
107
|
isLoading={false}
|
102
108
|
onSourceSelect={() => {}}
|
103
109
|
onSourceDrillDown={() => {}}
|
110
|
+
error={null}
|
111
|
+
handleReload={reload}
|
104
112
|
/>
|
105
113
|
);
|
106
114
|
}}
|
@@ -114,6 +122,8 @@ describe('SourceList', () => {
|
|
114
122
|
});
|
115
123
|
|
116
124
|
it('Focus is moved to the source list', async () => {
|
125
|
+
const reload = jest.fn();
|
126
|
+
|
117
127
|
render(
|
118
128
|
<SourceListTestWrapper
|
119
129
|
constructFunction={(previewModalState) => {
|
@@ -124,6 +134,8 @@ describe('SourceList', () => {
|
|
124
134
|
isLoading={false}
|
125
135
|
onSourceSelect={() => {}}
|
126
136
|
onSourceDrillDown={() => {}}
|
137
|
+
error={null}
|
138
|
+
handleReload={reload}
|
127
139
|
/>
|
128
140
|
);
|
129
141
|
}}
|
@@ -136,6 +148,8 @@ describe('SourceList', () => {
|
|
136
148
|
});
|
137
149
|
|
138
150
|
it('Source list renders each sources nodes', async () => {
|
151
|
+
const reload = jest.fn();
|
152
|
+
|
139
153
|
render(
|
140
154
|
<SourceListTestWrapper
|
141
155
|
constructFunction={(previewModalState) => {
|
@@ -146,6 +160,8 @@ describe('SourceList', () => {
|
|
146
160
|
isLoading={false}
|
147
161
|
onSourceSelect={() => {}}
|
148
162
|
onSourceDrillDown={() => {}}
|
163
|
+
error={null}
|
164
|
+
handleReload={reload}
|
149
165
|
/>
|
150
166
|
);
|
151
167
|
}}
|
@@ -165,6 +181,7 @@ describe('SourceList', () => {
|
|
165
181
|
|
166
182
|
it('Clicking node body triggers correct onSourceSelect', async () => {
|
167
183
|
const onSourceSelect = jest.fn();
|
184
|
+
const reload = jest.fn();
|
168
185
|
|
169
186
|
render(
|
170
187
|
<SourceListTestWrapper
|
@@ -176,6 +193,8 @@ describe('SourceList', () => {
|
|
176
193
|
isLoading={false}
|
177
194
|
onSourceSelect={onSourceSelect}
|
178
195
|
onSourceDrillDown={() => {}}
|
196
|
+
error={null}
|
197
|
+
handleReload={reload}
|
179
198
|
/>
|
180
199
|
);
|
181
200
|
}}
|
@@ -200,6 +219,7 @@ describe('SourceList', () => {
|
|
200
219
|
|
201
220
|
it('Clicking node child count triggers correct onSourceDrillDown', async () => {
|
202
221
|
const onSourceDrillDown = jest.fn();
|
222
|
+
const reload = jest.fn();
|
203
223
|
|
204
224
|
render(
|
205
225
|
<SourceListTestWrapper
|
@@ -211,6 +231,8 @@ describe('SourceList', () => {
|
|
211
231
|
isLoading={false}
|
212
232
|
onSourceSelect={() => {}}
|
213
233
|
onSourceDrillDown={onSourceDrillDown}
|
234
|
+
error={null}
|
235
|
+
handleReload={reload}
|
214
236
|
/>
|
215
237
|
);
|
216
238
|
}}
|
@@ -227,4 +249,31 @@ describe('SourceList', () => {
|
|
227
249
|
});
|
228
250
|
});
|
229
251
|
});
|
252
|
+
|
253
|
+
it('Renders error state when an error occurs loading source list', async () => {
|
254
|
+
const reload = jest.fn();
|
255
|
+
|
256
|
+
const { getByText } = render(
|
257
|
+
<SourceListTestWrapper
|
258
|
+
constructFunction={(previewModalState) => {
|
259
|
+
return (
|
260
|
+
<SourceList
|
261
|
+
sources={sources}
|
262
|
+
previewModalState={previewModalState}
|
263
|
+
isLoading={false}
|
264
|
+
onSourceSelect={() => {}}
|
265
|
+
onSourceDrillDown={() => {}}
|
266
|
+
error={new Error('Source list error!')}
|
267
|
+
handleReload={reload}
|
268
|
+
/>
|
269
|
+
);
|
270
|
+
}}
|
271
|
+
/>,
|
272
|
+
);
|
273
|
+
|
274
|
+
await waitFor(() => {
|
275
|
+
const errorMessage = getByText('Source list error!');
|
276
|
+
expect(errorMessage).toBeInTheDocument();
|
277
|
+
});
|
278
|
+
});
|
230
279
|
});
|
@@ -7,6 +7,7 @@ import { Source, ScopedSource } from '../types';
|
|
7
7
|
import { SkeletonList } from '../Skeleton/List/SkeletonList';
|
8
8
|
import clsx from 'clsx';
|
9
9
|
import { useCategorisedSources } from '../Hooks/useCategorisedSources';
|
10
|
+
import ResourceError from '../ResourceError/ResourceError';
|
10
11
|
|
11
12
|
export interface SourceListProps {
|
12
13
|
sources: Source[];
|
@@ -15,6 +16,8 @@ export interface SourceListProps {
|
|
15
16
|
onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
|
16
17
|
onSourceDrillDown: (node: ScopedSource) => void;
|
17
18
|
allowedTypes?: string[] | undefined;
|
19
|
+
handleReload: () => void;
|
20
|
+
error: Error | null;
|
18
21
|
}
|
19
22
|
|
20
23
|
const SourceList = function ({
|
@@ -24,6 +27,8 @@ const SourceList = function ({
|
|
24
27
|
onSourceSelect,
|
25
28
|
onSourceDrillDown,
|
26
29
|
allowedTypes,
|
30
|
+
handleReload,
|
31
|
+
error,
|
27
32
|
}: SourceListProps) {
|
28
33
|
const categorisedSources = useCategorisedSources(sources);
|
29
34
|
const listRef = useRef<HTMLUListElement>(null);
|
@@ -54,7 +59,10 @@ const SourceList = function ({
|
|
54
59
|
</>
|
55
60
|
)}
|
56
61
|
|
62
|
+
{!isLoading && error && <ResourceError errorMessage={error.message} handleReload={handleReload} />}
|
63
|
+
|
57
64
|
{!isLoading &&
|
65
|
+
!error &&
|
58
66
|
categorisedSources.map(({ key, label, sources }, index) => {
|
59
67
|
return (
|
60
68
|
<li key={key} className={`flex flex-col text-sm font-semibold text-grey-800 ${index > 0 ? 'mt-3' : ''}`}>
|
@@ -6,34 +6,53 @@ type CreateCallbacksProps = Partial<{
|
|
6
6
|
delay: number;
|
7
7
|
sourceIsLoading: boolean;
|
8
8
|
resourceIsLoading: boolean;
|
9
|
+
error?: string;
|
9
10
|
}>;
|
10
11
|
|
11
12
|
export const createResourceBrowserCallbacks = ({
|
12
13
|
delay = 500,
|
13
14
|
sourceIsLoading = false,
|
14
15
|
resourceIsLoading = false,
|
16
|
+
error,
|
15
17
|
}: CreateCallbacksProps = {}) => {
|
16
18
|
return {
|
17
19
|
onRequestSources: () => {
|
18
|
-
return new Promise((resolve) => {
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
return new Promise((resolve, reject) => {
|
21
|
+
setTimeout(() => {
|
22
|
+
if (error && Math.random() > 0.5) {
|
23
|
+
reject(new Error(error));
|
24
|
+
} else {
|
25
|
+
if (!sourceIsLoading) {
|
26
|
+
resolve(sampleSources);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}, delay);
|
22
30
|
});
|
23
31
|
},
|
24
32
|
onRequestChildren: (source: Source, resource: Resource | null) => {
|
25
|
-
return new Promise((resolve) => {
|
33
|
+
return new Promise((resolve, reject) => {
|
26
34
|
if (!resourceIsLoading) {
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
setTimeout(() => {
|
36
|
+
if (error && Math.random() > 0.5) {
|
37
|
+
reject(new Error(error));
|
38
|
+
} else {
|
39
|
+
const children = resource ? (((resource as any)?._children || []) as Resource[]) : sampleResources;
|
40
|
+
resolve(children);
|
41
|
+
}
|
42
|
+
}, delay);
|
30
43
|
}
|
31
44
|
});
|
32
45
|
},
|
33
46
|
onRequestResource: () => {
|
34
|
-
return new Promise((resolve) => {
|
47
|
+
return new Promise((resolve, reject) => {
|
35
48
|
if (!resourceIsLoading) {
|
36
|
-
setTimeout(
|
49
|
+
setTimeout(() => {
|
50
|
+
if (error && Math.random() > 0.5) {
|
51
|
+
reject(new Error(error));
|
52
|
+
} else {
|
53
|
+
resolve(sampleResources[0]);
|
54
|
+
}
|
55
|
+
}, delay);
|
37
56
|
}
|
38
57
|
});
|
39
58
|
},
|
package/src/index.stories.tsx
CHANGED
@@ -8,10 +8,11 @@ export default {
|
|
8
8
|
} as Meta<typeof RelatedAssetPicker>;
|
9
9
|
|
10
10
|
const Template: StoryFn<typeof RelatedAssetPicker> = (props) => {
|
11
|
-
const { sourceIsLoading, resourceIsLoading } = props;
|
11
|
+
const { sourceIsLoading, resourceIsLoading, error } = props;
|
12
12
|
const { onRequestSources, onRequestChildren, onRequestResource, onChange } = createResourceBrowserCallbacks({
|
13
13
|
sourceIsLoading: !!sourceIsLoading,
|
14
14
|
resourceIsLoading: !!resourceIsLoading,
|
15
|
+
error,
|
15
16
|
});
|
16
17
|
|
17
18
|
return (
|
@@ -41,5 +42,6 @@ Primary.args = {
|
|
41
42
|
modalTitle: 'Asset Picker',
|
42
43
|
sourceIsLoading: false,
|
43
44
|
resourceIsLoading: false,
|
45
|
+
error: '',
|
44
46
|
allowedTypes: ['site', 'image', 'physical_file'],
|
45
47
|
};
|