@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.
Files changed (39) hide show
  1. package/lib/Hooks/useChildResources.d.ts +2 -1
  2. package/lib/Hooks/useChildResources.js +5 -4
  3. package/lib/Hooks/useSources.d.ts +1 -1
  4. package/lib/Hooks/useSources.js +4 -2
  5. package/lib/Icons/Generics/Error.d.ts +4 -0
  6. package/lib/Icons/Generics/Error.js +12 -0
  7. package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
  8. package/lib/Icons/Generics/GenericIconMap.js +2 -0
  9. package/lib/Icons/Generics/Retry.d.ts +4 -0
  10. package/lib/Icons/Generics/Retry.js +12 -0
  11. package/lib/Icons/Generics/index.d.ts +2 -0
  12. package/lib/Icons/Generics/index.js +5 -1
  13. package/lib/Icons/Icon.d.ts +2 -0
  14. package/lib/ResourceError/ResourceError.d.ts +7 -0
  15. package/lib/ResourceError/ResourceError.js +16 -0
  16. package/lib/ResourceList/ResourceList.d.ts +3 -1
  17. package/lib/ResourceList/ResourceList.js +4 -1
  18. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +4 -8
  19. package/lib/SourceList/SourceList.d.ts +3 -1
  20. package/lib/SourceList/SourceList.js +4 -1
  21. package/lib/index.css +36 -0
  22. package/package.json +3 -3
  23. package/src/Hooks/useChildResources.spec.ts +2 -1
  24. package/src/Hooks/useChildResources.ts +7 -5
  25. package/src/Hooks/useSources.spec.ts +0 -1
  26. package/src/Hooks/useSources.ts +4 -2
  27. package/src/Icons/Generics/Error.tsx +13 -0
  28. package/src/Icons/Generics/GenericIconMap.ts +3 -1
  29. package/src/Icons/Generics/Retry.tsx +13 -0
  30. package/src/Icons/Generics/index.tsx +2 -0
  31. package/src/ResourceError/ResourceError.spec.tsx +29 -0
  32. package/src/ResourceError/ResourceError.tsx +27 -0
  33. package/src/ResourceList/ResourceList.spec.tsx +45 -0
  34. package/src/ResourceList/ResourceList.tsx +8 -0
  35. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +18 -6
  36. package/src/SourceList/SourceList.spec.tsx +49 -0
  37. package/src/SourceList/SourceList.tsx +8 -0
  38. package/src/__mocks__/StorybookHelpers.ts +29 -10
  39. 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
- // trigger a reload of the resources when the source or the current resource changes.
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
- return { isLoading, error, resources };
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;
@@ -9,8 +9,8 @@ type UseSourcesProps = {
9
9
  */
10
10
  export declare const useSources: ({ onRequestSources }: UseSourcesProps) => {
11
11
  isLoading: boolean;
12
- error: Error | null;
13
12
  sources: Source[];
14
13
  reload: () => void;
14
+ error: Error | null;
15
15
  };
16
16
  export {};
@@ -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, error, sources, reload: loadSources };
29
+ return { isLoading, sources, reload: loadSources, error };
28
30
  };
29
31
  exports.useSources = useSources;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export default function Error({ isDecorative, ...props }: {
3
+ isDecorative: boolean;
4
+ } & React.SVGProps<SVGSVGElement>): JSX.Element;
@@ -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;
@@ -9,6 +9,8 @@ const GenericIconMap = {
9
9
  selected: _1.Selected,
10
10
  root: _1.Root,
11
11
  close: _1.Close,
12
+ error: _1.Error,
13
+ retry: _1.Retry,
12
14
  };
13
15
  // Export our map
14
16
  exports.default = GenericIconMap;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export default function Retry({ isDecorative, ...props }: {
3
+ isDecorative: boolean;
4
+ } & React.SVGProps<SVGSVGElement>): JSX.Element;
@@ -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; } });
@@ -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,7 @@
1
+ /// <reference types="react" />
2
+ interface ResourceError {
3
+ errorMessage: string;
4
+ handleReload: () => void;
5
+ }
6
+ declare const ResourceError: ({ errorMessage, handleReload }: ResourceError) => JSX.Element;
7
+ export default ResourceError;
@@ -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 ResourceList = function ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, }) {
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, sources } = (0, useSources_1.useSources)({ onRequestSources });
45
- const { isLoading: isResourcesLoading, resources } = (0, useChildResources_1.useChildResources)({
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 SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, }) {
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.20",
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.20",
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": "80da37a08710a54a9bab23b1f741299d157f56e2"
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
- // trigger a reload of the resources when the source or the current resource changes.
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
- return { isLoading, error, resources };
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
  };
@@ -15,7 +15,6 @@ describe('useSources', () => {
15
15
  await waitFor(() => expect(result.current.isLoading).toBe(false));
16
16
 
17
17
  expect(result.current.isLoading).toBe(false);
18
- expect(result.current.error).toBe(null);
19
18
  expect(result.current.sources).toBe(sources);
20
19
  });
21
20
 
@@ -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, error, sources, reload: loadSources };
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
- const { isLoading: isSourceLoading, sources } = useSources({ onRequestSources });
40
- const { isLoading: isResourcesLoading, resources } = useChildResources({
41
- source,
42
- currentResource,
43
- onRequestChildren,
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
- if (!sourceIsLoading) {
20
- setTimeout(resolve, delay, sampleSources);
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
- const children = resource ? (((resource as any)?._children || []) as Resource[]) : sampleResources;
28
-
29
- setTimeout(resolve, delay, children);
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(resolve, delay, sampleResources[0]);
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
  },
@@ -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
  };