@squiz/resource-browser 1.32.1-alpha.32 → 1.32.1-alpha.33

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 (36) hide show
  1. package/lib/Icons/Generics/Back.d.ts +4 -0
  2. package/lib/Icons/Generics/Back.js +12 -0
  3. package/lib/Icons/Generics/Empty.d.ts +4 -0
  4. package/lib/Icons/Generics/Empty.js +12 -0
  5. package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
  6. package/lib/Icons/Generics/GenericIconMap.js +2 -0
  7. package/lib/Icons/Generics/index.d.ts +2 -0
  8. package/lib/Icons/Generics/index.js +5 -1
  9. package/lib/Icons/Icon.d.ts +2 -0
  10. package/lib/ResourceItem/ResourceItem.d.ts +2 -2
  11. package/lib/ResourceItem/ResourceItem.js +5 -4
  12. package/lib/ResourceList/ResourceList.d.ts +3 -2
  13. package/lib/ResourceList/ResourceList.js +4 -3
  14. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +2 -2
  15. package/lib/ResourceState/ResourceState.d.ts +7 -0
  16. package/lib/{ResourceError/ResourceError.js → ResourceState/ResourceState.js} +7 -7
  17. package/lib/SourceList/SourceList.d.ts +1 -3
  18. package/lib/SourceList/SourceList.js +4 -4
  19. package/lib/index.css +3 -3
  20. package/package.json +3 -3
  21. package/src/Icons/Generics/Back.tsx +13 -0
  22. package/src/Icons/Generics/Empty.tsx +13 -0
  23. package/src/Icons/Generics/GenericIconMap.ts +3 -1
  24. package/src/Icons/Generics/index.tsx +2 -0
  25. package/src/ResourceItem/ResourceItem.tsx +6 -4
  26. package/src/ResourceList/ResourceList.spec.tsx +6 -0
  27. package/src/ResourceList/ResourceList.tsx +12 -4
  28. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +1 -1
  29. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +1 -2
  30. package/src/{ResourceError/ResourceError.spec.tsx → ResourceState/ResourceState.spec.tsx} +6 -5
  31. package/src/ResourceState/ResourceState.stories.tsx +24 -0
  32. package/src/ResourceState/ResourceState.tsx +31 -0
  33. package/src/SourceList/SourceList.spec.tsx +1 -40
  34. package/src/SourceList/SourceList.tsx +2 -9
  35. package/lib/ResourceError/ResourceError.d.ts +0 -6
  36. package/src/ResourceError/ResourceError.tsx +0 -27
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export default function Back({ 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 Back({ isDecorative, ...props }) {
8
+ return (react_1.default.createElement("svg", { width: "17", height: "16", viewBox: "0 0 17 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props },
9
+ react_1.default.createElement("path", { d: "M15.2912 7.00501H4.12124L9.00124 2.12501C9.39124 1.73501 9.39124 1.09501 9.00124 0.705006C8.61124 0.315006 7.98124 0.315006 7.59124 0.705006L1.00124 7.29501C0.61124 7.68501 0.61124 8.31501 1.00124 8.70501L7.59124 15.295C7.98124 15.685 8.61124 15.685 9.00124 15.295C9.39124 14.905 9.39124 14.275 9.00124 13.885L4.12124 9.00501H15.2912C15.8412 9.00501 16.2912 8.55501 16.2912 8.00501C16.2912 7.45501 15.8412 7.00501 15.2912 7.00501Z", fill: "#3D3D3D" }),
10
+ !isDecorative && react_1.default.createElement("title", null, "back icon")));
11
+ }
12
+ exports.default = Back;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export default function Empty({ 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 Empty({ isDecorative, ...props }) {
8
+ return (react_1.default.createElement("svg", { width: "42", height: "42", viewBox: "0 0 42 42", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props },
9
+ react_1.default.createElement("path", { d: "M1.3925 3.82749C0.612495 4.60749 0.612495 5.88749 1.3925 6.66749L4.5125 9.78749C2.0125 13.5475 0.752495 18.1875 1.3325 23.1675C2.37249 32.2475 9.75249 39.6275 18.8325 40.6675C23.8125 41.2475 28.4525 39.9875 32.2125 37.4875L35.3325 40.6075C36.1125 41.3875 37.3725 41.3875 38.1525 40.6075C38.9325 39.8275 38.9325 38.5675 38.1525 37.7875L4.21249 3.82749C3.4325 3.04749 2.17249 3.04749 1.3925 3.82749ZM21.1925 36.8075C12.3725 36.8075 5.19249 29.6275 5.19249 20.8075C5.19249 17.8475 6.01249 15.0875 7.43249 12.6875L29.3125 34.5675C26.9125 35.9875 24.1525 36.8075 21.1925 36.8075ZM13.0725 7.04749L10.1725 4.12749C13.3325 2.0275 17.1125 0.807495 21.1925 0.807495C32.2325 0.807495 41.1925 9.76749 41.1925 20.8075C41.1925 24.8875 39.9725 28.6675 37.8725 31.8275L34.9525 28.9075C36.3725 26.5275 37.1925 23.7675 37.1925 20.8075C37.1925 11.9875 30.0125 4.80749 21.1925 4.80749C18.2325 4.80749 15.4725 5.62749 13.0725 7.04749Z", fill: "#949494" }),
10
+ !isDecorative && react_1.default.createElement("title", null, "empty icon")));
11
+ }
12
+ exports.default = Empty;
@@ -1,4 +1,4 @@
1
- import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry } from '.';
1
+ import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry, Empty, Back } from '.';
2
2
  declare const GenericIconMap: {
3
3
  'arrow-right': typeof ArrowRight;
4
4
  'arrow-down': typeof ArrowDown;
@@ -8,5 +8,7 @@ declare const GenericIconMap: {
8
8
  close: typeof Close;
9
9
  error: typeof Error;
10
10
  retry: typeof Retry;
11
+ empty: typeof Empty;
12
+ back: typeof Back;
11
13
  };
12
14
  export default GenericIconMap;
@@ -11,6 +11,8 @@ const GenericIconMap = {
11
11
  close: _1.Close,
12
12
  error: _1.Error,
13
13
  retry: _1.Retry,
14
+ empty: _1.Empty,
15
+ back: _1.Back,
14
16
  };
15
17
  // Export our map
16
18
  exports.default = GenericIconMap;
@@ -6,3 +6,5 @@ export { default as ResourceSelect } from './ResourceSelect';
6
6
  export { default as Close } from './Close';
7
7
  export { default as Error } from './Error';
8
8
  export { default as Retry } from './Retry';
9
+ export { default as Empty } from './Empty';
10
+ export { default as Back } from './Back';
@@ -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.Retry = exports.Error = exports.Close = exports.ResourceSelect = exports.Root = exports.Selected = exports.ArrowDown = exports.ArrowRight = void 0;
6
+ exports.Back = exports.Empty = 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; } });
@@ -21,3 +21,7 @@ var Error_1 = require("./Error");
21
21
  Object.defineProperty(exports, "Error", { enumerable: true, get: function () { return __importDefault(Error_1).default; } });
22
22
  var Retry_1 = require("./Retry");
23
23
  Object.defineProperty(exports, "Retry", { enumerable: true, get: function () { return __importDefault(Retry_1).default; } });
24
+ var Empty_1 = require("./Empty");
25
+ Object.defineProperty(exports, "Empty", { enumerable: true, get: function () { return __importDefault(Empty_1).default; } });
26
+ var Back_1 = require("./Back");
27
+ Object.defineProperty(exports, "Back", { enumerable: true, get: function () { return __importDefault(Back_1).default; } });
@@ -9,6 +9,8 @@ export declare const iconSources: {
9
9
  close: typeof import("./Generics").Close;
10
10
  error: typeof import("./Generics").Error;
11
11
  retry: typeof import("./Generics").Retry;
12
+ empty: typeof import("./Generics").Empty;
13
+ back: typeof import("./Generics").Back;
12
14
  };
13
15
  matrix: {
14
16
  audio_file: typeof import("./MatrixResources").Audio;
@@ -5,10 +5,10 @@ interface ResourceItem<T> {
5
5
  selected?: boolean;
6
6
  label: string;
7
7
  type: string;
8
- childCount: number;
8
+ childCount?: number;
9
9
  previewModalState: OverlayTriggerState;
10
10
  onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
11
- onDrillDown: (node: T) => void;
11
+ onDrillDown?: (node: T) => void;
12
12
  className: string;
13
13
  allowedTypes?: string[] | undefined;
14
14
  }
@@ -12,14 +12,15 @@ const ResourceItem = ({ item, selected, label, type, childCount, previewModalSta
12
12
  const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
13
13
  const title = isDisabled ? "You can't select this item" : label;
14
14
  return (react_1.default.createElement("li", { className: `flex items-stretch p-1 bg-white border border-grey-200 min-h-[64px] ${className}` },
15
- react_1.default.createElement(ModalOpeningButton_1.default, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(item, overlayProps), className: `
16
- relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${childCount > 0 ? 'mr-2' : ''} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
15
+ react_1.default.createElement(ModalOpeningButton_1.default, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(item, overlayProps), "aria-label": childCount === undefined ? `Drill down to ${label} children` : '', className: `
16
+ relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${childCount !== undefined && childCount > 0 ? 'mr-2' : ''} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
17
17
  `, title: title },
18
18
  react_1.default.createElement(Icon_1.default, { icon: type, resourceSource: "matrix", "aria-label": type, className: `mr-4 shrink-0 ${isDisabled && 'opacity-40'}` }),
19
19
  react_1.default.createElement("span", { className: `relative flex items-center ${selected ? 'mr-8' : ''}` },
20
20
  react_1.default.createElement("span", { className: "line-clamp-2 text-left break-word" }, label),
21
- selected && react_1.default.createElement(Icon_1.default, { icon: 'selected', "aria-label": "selected", className: "absolute -right-8" }))),
22
- childCount > 0 && (react_1.default.createElement("button", { type: "button", "aria-label": `Drill down to ${label} children`, onClick: () => onDrillDown(item), className: `relative shrink-0 flex items-center p-4 rounded outline-0 before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-50 focus:bg-gray-50` },
21
+ selected && react_1.default.createElement(Icon_1.default, { icon: 'selected', "aria-label": "selected", className: "absolute -right-8" })),
22
+ childCount === undefined && react_1.default.createElement(Icon_1.default, { icon: 'arrow-right', className: "absolute right-5" })),
23
+ childCount !== undefined && childCount > 0 && onDrillDown && (react_1.default.createElement("button", { type: "button", "aria-label": `Drill down to ${label} children`, onClick: () => onDrillDown(item), className: `relative shrink-0 flex items-center p-4 rounded outline-0 before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-50 focus:bg-gray-50` },
23
24
  react_1.default.createElement("span", { className: "ml-auto flex items-center" },
24
25
  react_1.default.createElement("span", { className: "truncate w-14 text-right", title: String(childCount) }, childCount),
25
26
  react_1.default.createElement(Icon_1.default, { icon: 'arrow-right', className: "ml-1" }))))));
@@ -9,8 +9,9 @@ export interface ResourceListProps {
9
9
  onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
10
10
  onResourceDrillDown: (resource: Resource) => void;
11
11
  allowedTypes?: string[] | undefined;
12
- error: Error | null;
12
+ handleReturnToRoot: () => void;
13
13
  handleReload: () => void;
14
+ error: Error | null;
14
15
  }
15
- declare const ResourceList: ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, error, handleReload, }: ResourceListProps) => JSX.Element;
16
+ declare const ResourceList: ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, handleReturnToRoot, handleReload, error, }: ResourceListProps) => JSX.Element;
16
17
  export default ResourceList;
@@ -29,8 +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 ResourceError_1 = __importDefault(require("../ResourceError/ResourceError"));
33
- const ResourceList = function ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, error, handleReload, }) {
32
+ const ResourceState_1 = __importDefault(require("../ResourceState/ResourceState"));
33
+ const ResourceList = function ({ resources, selectedResource, previewModalState, isLoading, onResourceSelect, onResourceDrillDown, allowedTypes, handleReturnToRoot, handleReload, error, }) {
34
34
  const listRef = (0, react_1.useRef)(null);
35
35
  // When resources change, because we are on a new page, reset focus to the list
36
36
  (0, react_1.useEffect)(() => {
@@ -44,7 +44,8 @@ const ResourceList = function ({ resources, selectedResource, previewModalState,
44
44
  isLoading && (react_1.default.createElement(react_1.default.Fragment, null, [...Array(8)].map((_item, index) => {
45
45
  return react_1.default.createElement(SkeletonListItem_1.SkeletonListItem, { key: index });
46
46
  }))),
47
- !isLoading && error && react_1.default.createElement(ResourceError_1.default, { errorMessage: error.message, handleReload: handleReload }),
47
+ !isLoading && error && react_1.default.createElement(ResourceState_1.default, { state: "error", message: error.message, handleReload: handleReload }),
48
+ !isLoading && !error && resources.length === 0 && (react_1.default.createElement(ResourceState_1.default, { state: "empty", handleReload: handleReturnToRoot })),
48
49
  !isLoading &&
49
50
  !error &&
50
51
  resources.map((resource) => {
@@ -79,8 +79,8 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
79
79
  react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
80
80
  react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
81
81
  hierarchy.length > 0 && (react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: popUntil, onReturnToRoot: handleReturnToRoot })),
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 }))),
82
+ !source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, 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, handleReturnToRoot: handleReturnToRoot, handleReload: handleResourceReload, error: resourceError }))),
84
84
  react_1.default.createElement(PreviewPanel_1.default, { resource: selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose }))));
85
85
  }
86
86
  exports.default = ResourcePickerContainer;
@@ -0,0 +1,7 @@
1
+ interface ResourceState {
2
+ state: 'error' | 'empty';
3
+ message?: string;
4
+ handleReload: () => void;
5
+ }
6
+ declare const ResourceState: ({ state, message, handleReload }: ResourceState) => JSX.Element;
7
+ export default ResourceState;
@@ -5,12 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
7
  const Icon_1 = __importDefault(require("../Icons/Icon"));
8
- const ResourceError = function ({ errorMessage, handleReload }) {
8
+ const ResourceState = function ({ state, message, handleReload }) {
9
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")));
10
+ react_1.default.createElement(Icon_1.default, { icon: state, "aria-hidden": true }),
11
+ react_1.default.createElement("span", { className: "text-md text-gray-800 font-semibold leading-5" }, state === 'empty' ? 'There are no items to display' : message),
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 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3" },
13
+ react_1.default.createElement(Icon_1.default, { icon: state === 'empty' ? 'back' : 'retry', "aria-hidden": true }),
14
+ state === 'empty' ? 'Back to source list' : 'Try again')));
15
15
  };
16
- exports.default = ResourceError;
16
+ exports.default = ResourceState;
@@ -6,10 +6,8 @@ export interface SourceListProps {
6
6
  previewModalState: OverlayTriggerState;
7
7
  isLoading: boolean;
8
8
  onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
9
- onSourceDrillDown: (node: ScopedSource) => void;
10
- allowedTypes?: string[] | undefined;
11
9
  handleReload: () => void;
12
10
  error: Error | null;
13
11
  }
14
- declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, handleReload, error, }: SourceListProps) => JSX.Element;
12
+ declare const SourceList: ({ sources, previewModalState, isLoading, onSourceSelect, handleReload, error, }: SourceListProps) => JSX.Element;
15
13
  export default SourceList;
@@ -31,8 +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 ResourceError_1 = __importDefault(require("../ResourceError/ResourceError"));
35
- const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, onSourceDrillDown, allowedTypes, handleReload, error, }) {
34
+ const ResourceState_1 = __importDefault(require("../ResourceState/ResourceState"));
35
+ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect, handleReload, error, }) {
36
36
  const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
37
37
  const listRef = (0, react_1.useRef)(null);
38
38
  (0, react_1.useEffect)(() => {
@@ -48,7 +48,7 @@ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSe
48
48
  react_1.default.createElement(SkeletonList_1.SkeletonList, { itemCount: 3 })),
49
49
  react_1.default.createElement("li", null,
50
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 }),
51
+ !isLoading && error && react_1.default.createElement(ResourceState_1.default, { state: "error", message: error.message, handleReload: handleReload }),
52
52
  !isLoading &&
53
53
  !error &&
54
54
  categorisedSources.map(({ key, label, sources }, index) => {
@@ -56,7 +56,7 @@ const SourceList = function ({ sources, previewModalState, isLoading, onSourceSe
56
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" },
57
57
  react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
58
58
  sources.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col" }, sources.map(({ source, resource }) => {
59
- return (react_1.default.createElement(ResourceItem_1.default, { key: `${source.id}-${resource?.id}`, item: { source, resource }, label: resource?.name || source.name, type: resource?.type.code || 'folder', childCount: resource?.childCount || 0, previewModalState: previewModalState, onSelect: onSourceSelect, onDrillDown: onSourceDrillDown, className: "mt-3 rounded-lg", allowedTypes: allowedTypes }));
59
+ return (react_1.default.createElement(ResourceItem_1.default, { key: `${source.id}-${resource?.id}`, item: { source, resource }, label: resource?.name || source.name, type: resource?.type.code || 'folder', previewModalState: previewModalState, onSelect: onSourceSelect, className: "mt-3 rounded-lg" }));
60
60
  })))));
61
61
  })));
62
62
  };
package/lib/index.css CHANGED
@@ -393,6 +393,9 @@
393
393
  .squiz-rb-scope .right-4 {
394
394
  right: 1rem;
395
395
  }
396
+ .squiz-rb-scope .right-5 {
397
+ right: 1.25rem;
398
+ }
396
399
  .squiz-rb-scope .top-2 {
397
400
  top: 0.5rem;
398
401
  }
@@ -610,9 +613,6 @@
610
613
  .squiz-rb-scope .w-72 {
611
614
  width: 18rem;
612
615
  }
613
- .squiz-rb-scope .w-\[119px\] {
614
- width: 119px;
615
- }
616
616
  .squiz-rb-scope .w-\[20px\] {
617
617
  width: 20px;
618
618
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "1.32.1-alpha.32",
3
+ "version": "1.32.1-alpha.33",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@mui/icons-material": "5.11.16",
18
- "@squiz/dx-json-schema-lib": "1.32.1-alpha.32",
18
+ "@squiz/dx-json-schema-lib": "1.32.1-alpha.33",
19
19
  "pretty-bytes": "5.6.0",
20
20
  "react-aria": "3.23.1",
21
21
  "react-responsive": "9.0.2",
@@ -73,5 +73,5 @@
73
73
  "volta": {
74
74
  "node": "18.15.0"
75
75
  },
76
- "gitHead": "cddd834def3c2d6b84923f3be0c58c1073025abc"
76
+ "gitHead": "614c4ec6f82220758521a2887b45094c36474aef"
77
77
  }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export default function Back({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+ <path
7
+ d="M15.2912 7.00501H4.12124L9.00124 2.12501C9.39124 1.73501 9.39124 1.09501 9.00124 0.705006C8.61124 0.315006 7.98124 0.315006 7.59124 0.705006L1.00124 7.29501C0.61124 7.68501 0.61124 8.31501 1.00124 8.70501L7.59124 15.295C7.98124 15.685 8.61124 15.685 9.00124 15.295C9.39124 14.905 9.39124 14.275 9.00124 13.885L4.12124 9.00501H15.2912C15.8412 9.00501 16.2912 8.55501 16.2912 8.00501C16.2912 7.45501 15.8412 7.00501 15.2912 7.00501Z"
8
+ fill="#3D3D3D"
9
+ />
10
+ {!isDecorative && <title>back icon</title>}
11
+ </svg>
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export default function Empty({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+ <path
7
+ d="M1.3925 3.82749C0.612495 4.60749 0.612495 5.88749 1.3925 6.66749L4.5125 9.78749C2.0125 13.5475 0.752495 18.1875 1.3325 23.1675C2.37249 32.2475 9.75249 39.6275 18.8325 40.6675C23.8125 41.2475 28.4525 39.9875 32.2125 37.4875L35.3325 40.6075C36.1125 41.3875 37.3725 41.3875 38.1525 40.6075C38.9325 39.8275 38.9325 38.5675 38.1525 37.7875L4.21249 3.82749C3.4325 3.04749 2.17249 3.04749 1.3925 3.82749ZM21.1925 36.8075C12.3725 36.8075 5.19249 29.6275 5.19249 20.8075C5.19249 17.8475 6.01249 15.0875 7.43249 12.6875L29.3125 34.5675C26.9125 35.9875 24.1525 36.8075 21.1925 36.8075ZM13.0725 7.04749L10.1725 4.12749C13.3325 2.0275 17.1125 0.807495 21.1925 0.807495C32.2325 0.807495 41.1925 9.76749 41.1925 20.8075C41.1925 24.8875 39.9725 28.6675 37.8725 31.8275L34.9525 28.9075C36.3725 26.5275 37.1925 23.7675 37.1925 20.8075C37.1925 11.9875 30.0125 4.80749 21.1925 4.80749C18.2325 4.80749 15.4725 5.62749 13.0725 7.04749Z"
8
+ fill="#949494"
9
+ />
10
+ {!isDecorative && <title>empty icon</title>}
11
+ </svg>
12
+ );
13
+ }
@@ -1,4 +1,4 @@
1
- import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry } from '.';
1
+ import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry, Empty, Back } from '.';
2
2
 
3
3
  // Define our map of matrix types to icons
4
4
  const GenericIconMap = {
@@ -10,6 +10,8 @@ const GenericIconMap = {
10
10
  close: Close,
11
11
  error: Error,
12
12
  retry: Retry,
13
+ empty: Empty,
14
+ back: Back,
13
15
  };
14
16
 
15
17
  // Export our map
@@ -7,3 +7,5 @@ export { default as ResourceSelect } from './ResourceSelect';
7
7
  export { default as Close } from './Close';
8
8
  export { default as Error } from './Error';
9
9
  export { default as Retry } from './Retry';
10
+ export { default as Empty } from './Empty';
11
+ export { default as Back } from './Back';
@@ -11,10 +11,10 @@ interface ResourceItem<T> {
11
11
  selected?: boolean;
12
12
  label: string;
13
13
  type: string;
14
- childCount: number;
14
+ childCount?: number;
15
15
  previewModalState: OverlayTriggerState;
16
16
  onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
17
- onDrillDown: (node: T) => void;
17
+ onDrillDown?: (node: T) => void;
18
18
  className: string;
19
19
  allowedTypes?: string[] | undefined;
20
20
  }
@@ -42,9 +42,10 @@ const ResourceItem = <T,>({
42
42
  {...triggerProps}
43
43
  isDisabled={isDisabled}
44
44
  onPress={() => onSelect(item, overlayProps)}
45
+ aria-label={childCount === undefined ? `Drill down to ${label} children` : ''}
45
46
  className={`
46
47
  relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${
47
- childCount > 0 ? 'mr-2' : ''
48
+ childCount !== undefined && childCount > 0 ? 'mr-2' : ''
48
49
  } ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
49
50
  `}
50
51
  title={title}
@@ -59,8 +60,9 @@ const ResourceItem = <T,>({
59
60
  <span className="line-clamp-2 text-left break-word">{label}</span>
60
61
  {selected && <Icon icon={'selected' as IconOptions} aria-label="selected" className="absolute -right-8" />}
61
62
  </span>
63
+ {childCount === undefined && <Icon icon={'arrow-right' as IconOptions} className="absolute right-5" />}
62
64
  </ModalOpeningButton>
63
- {childCount > 0 && (
65
+ {childCount !== undefined && childCount > 0 && onDrillDown && (
64
66
  <button
65
67
  type="button"
66
68
  aria-label={`Drill down to ${label} children`}
@@ -39,6 +39,7 @@ describe('ResourceList', () => {
39
39
  onResourceDrillDown={() => {}}
40
40
  error={null}
41
41
  handleReload={reload}
42
+ handleReturnToRoot={reload}
42
43
  />
43
44
  );
44
45
  }}
@@ -65,6 +66,7 @@ describe('ResourceList', () => {
65
66
  onResourceDrillDown={() => {}}
66
67
  error={null}
67
68
  handleReload={reload}
69
+ handleReturnToRoot={reload}
68
70
  />
69
71
  );
70
72
  }}
@@ -91,6 +93,7 @@ describe('ResourceList', () => {
91
93
  onResourceDrillDown={() => {}}
92
94
  error={null}
93
95
  handleReload={reload}
96
+ handleReturnToRoot={reload}
94
97
  />
95
98
  );
96
99
  }}
@@ -122,6 +125,7 @@ describe('ResourceList', () => {
122
125
  onResourceDrillDown={() => {}}
123
126
  error={null}
124
127
  handleReload={reload}
128
+ handleReturnToRoot={reload}
125
129
  />
126
130
  );
127
131
  }}
@@ -153,6 +157,7 @@ describe('ResourceList', () => {
153
157
  onResourceDrillDown={onResourceDrillDown}
154
158
  error={null}
155
159
  handleReload={reload}
160
+ handleReturnToRoot={reload}
156
161
  />
157
162
  );
158
163
  }}
@@ -182,6 +187,7 @@ describe('ResourceList', () => {
182
187
  onResourceDrillDown={() => {}}
183
188
  error={new Error('This is a resource error!')}
184
189
  handleReload={reload}
190
+ handleReturnToRoot={reload}
185
191
  />
186
192
  );
187
193
  }}
@@ -5,7 +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
+ import ResourceState from '../ResourceState/ResourceState';
9
9
 
10
10
  export interface ResourceListProps {
11
11
  resources: Array<Resource>;
@@ -15,8 +15,9 @@ export interface ResourceListProps {
15
15
  onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
16
16
  onResourceDrillDown: (resource: Resource) => void;
17
17
  allowedTypes?: string[] | undefined;
18
- error: Error | null;
18
+ handleReturnToRoot: () => void;
19
19
  handleReload: () => void;
20
+ error: Error | null;
20
21
  }
21
22
 
22
23
  const ResourceList = function ({
@@ -27,8 +28,9 @@ const ResourceList = function ({
27
28
  onResourceSelect,
28
29
  onResourceDrillDown,
29
30
  allowedTypes,
30
- error,
31
+ handleReturnToRoot,
31
32
  handleReload,
33
+ error,
32
34
  }: ResourceListProps) {
33
35
  const listRef = useRef<HTMLUListElement>(null);
34
36
 
@@ -56,7 +58,13 @@ const ResourceList = function ({
56
58
  </>
57
59
  )}
58
60
 
59
- {!isLoading && error && <ResourceError errorMessage={error.message} handleReload={handleReload} />}
61
+ {/* Error State */}
62
+ {!isLoading && error && <ResourceState state="error" message={error.message} handleReload={handleReload} />}
63
+
64
+ {/* Empty State */}
65
+ {!isLoading && !error && resources.length === 0 && (
66
+ <ResourceState state="empty" handleReload={handleReturnToRoot} />
67
+ )}
60
68
 
61
69
  {!isLoading &&
62
70
  !error &&
@@ -324,7 +324,7 @@ describe('ResourcePickerContainer', () => {
324
324
  });
325
325
 
326
326
  const user = userEvent.setup();
327
- user.click(screen.getByRole('button', { name: 'site Test Website' }));
327
+ user.click(screen.getByRole('button', { name: 'Drill down to Test Website children' }));
328
328
 
329
329
  expect(mockModalState).toEqual(
330
330
  expect.objectContaining({
@@ -136,8 +136,6 @@ function ResourcePickerContainer({
136
136
  previewModalState={previewModalState}
137
137
  isLoading={isSourceLoading}
138
138
  onSourceSelect={handleSourceDrilldown}
139
- onSourceDrillDown={handleSourceDrilldown}
140
- allowedTypes={allowedTypes}
141
139
  handleReload={handleSourceReload}
142
140
  error={sourceError}
143
141
  />
@@ -151,6 +149,7 @@ function ResourcePickerContainer({
151
149
  onResourceSelect={handleResourceSelected}
152
150
  onResourceDrillDown={handleResourceDrillDown}
153
151
  allowedTypes={allowedTypes}
152
+ handleReturnToRoot={handleReturnToRoot}
154
153
  handleReload={handleResourceReload}
155
154
  error={resourceError}
156
155
  />
@@ -1,22 +1,23 @@
1
1
  import React from 'react';
2
2
  import { render, fireEvent } from '@testing-library/react';
3
- import ResourceError from './ResourceError';
3
+ import ResourceState from './ResourceState';
4
4
 
5
5
  const defaultProps: any = {
6
- errorMessage: 'This is a test error!',
6
+ state: 'error',
7
+ message: 'This is a test error!',
7
8
  handleReload: jest.fn(),
8
9
  };
9
10
 
10
11
  describe('ResourceError', () => {
11
12
  it('should render the component with the correct error message', () => {
12
- const { getByText } = render(<ResourceError {...defaultProps} />);
13
- const errorMessage = getByText(defaultProps.errorMessage);
13
+ const { getByText } = render(<ResourceState {...defaultProps} />);
14
+ const errorMessage = getByText(defaultProps.message);
14
15
 
15
16
  expect(errorMessage).toBeInTheDocument();
16
17
  });
17
18
 
18
19
  it('should call the reload function when the button is pressed', () => {
19
- const { getByRole } = render(<ResourceError {...defaultProps} />);
20
+ const { getByRole } = render(<ResourceState {...defaultProps} />);
20
21
  const buttonElement = getByRole('button');
21
22
  fireEvent.click(buttonElement);
22
23
 
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import ResourceState from './ResourceState';
5
+
6
+ export default {
7
+ title: 'Resource State',
8
+ component: ResourceState,
9
+ } as Meta<typeof ResourceState>;
10
+
11
+ const Template: StoryFn<typeof ResourceState> = ({ state, message }) => (
12
+ <ResourceState state={state} message={message} handleReload={() => alert('Resource browser reload')} />
13
+ );
14
+
15
+ export const Error = Template.bind({});
16
+ Error.args = {
17
+ state: 'error',
18
+ message: 'This is a resource browser error!',
19
+ };
20
+
21
+ export const Empty = Template.bind({});
22
+ Empty.args = {
23
+ state: 'empty',
24
+ };
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import Icon, { IconOptions } from '../Icons/Icon';
3
+
4
+ interface ResourceState {
5
+ state: 'error' | 'empty';
6
+ message?: string;
7
+ handleReload: () => void;
8
+ }
9
+
10
+ const ResourceState = function ({ state, message, handleReload }: ResourceState) {
11
+ return (
12
+ <div className="flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
13
+ <Icon icon={state as IconOptions} aria-hidden />
14
+ {/* Message */}
15
+ <span className="text-md text-gray-800 font-semibold leading-5">
16
+ {state === 'empty' ? 'There are no items to display' : message}
17
+ </span>
18
+ {/* Retry button */}
19
+ <button
20
+ type="button"
21
+ onClick={handleReload}
22
+ className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3"
23
+ >
24
+ <Icon icon={state === 'empty' ? 'back' : 'retry'} aria-hidden />
25
+ {state === 'empty' ? 'Back to source list' : 'Try again'}
26
+ </button>
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default ResourceState;
@@ -80,7 +80,6 @@ describe('SourceList', () => {
80
80
  previewModalState={previewModalState}
81
81
  isLoading={true}
82
82
  onSourceSelect={() => {}}
83
- onSourceDrillDown={() => {}}
84
83
  error={null}
85
84
  handleReload={reload}
86
85
  />
@@ -106,7 +105,6 @@ describe('SourceList', () => {
106
105
  previewModalState={previewModalState}
107
106
  isLoading={false}
108
107
  onSourceSelect={() => {}}
109
- onSourceDrillDown={() => {}}
110
108
  error={null}
111
109
  handleReload={reload}
112
110
  />
@@ -133,7 +131,6 @@ describe('SourceList', () => {
133
131
  previewModalState={previewModalState}
134
132
  isLoading={false}
135
133
  onSourceSelect={() => {}}
136
- onSourceDrillDown={() => {}}
137
134
  error={null}
138
135
  handleReload={reload}
139
136
  />
@@ -159,7 +156,6 @@ describe('SourceList', () => {
159
156
  previewModalState={previewModalState}
160
157
  isLoading={false}
161
158
  onSourceSelect={() => {}}
162
- onSourceDrillDown={() => {}}
163
159
  error={null}
164
160
  handleReload={reload}
165
161
  />
@@ -192,7 +188,6 @@ describe('SourceList', () => {
192
188
  previewModalState={previewModalState}
193
189
  isLoading={false}
194
190
  onSourceSelect={onSourceSelect}
195
- onSourceDrillDown={() => {}}
196
191
  error={null}
197
192
  handleReload={reload}
198
193
  />
@@ -202,7 +197,7 @@ describe('SourceList', () => {
202
197
  );
203
198
 
204
199
  const user = userEvent.setup();
205
- const itemButton = screen.getByRole('button', { name: 'site Node 1' });
200
+ const itemButton = screen.getByRole('button', { name: 'Drill down to Node 1 children' });
206
201
  user.click(itemButton);
207
202
 
208
203
  await waitFor(() => {
@@ -217,39 +212,6 @@ describe('SourceList', () => {
217
212
  });
218
213
  });
219
214
 
220
- it('Clicking node child count triggers correct onSourceDrillDown', async () => {
221
- const onSourceDrillDown = jest.fn();
222
- const reload = jest.fn();
223
-
224
- render(
225
- <SourceListTestWrapper
226
- constructFunction={(previewModalState) => {
227
- return (
228
- <SourceList
229
- sources={sources}
230
- previewModalState={previewModalState}
231
- isLoading={false}
232
- onSourceSelect={() => {}}
233
- onSourceDrillDown={onSourceDrillDown}
234
- error={null}
235
- handleReload={reload}
236
- />
237
- );
238
- }}
239
- />,
240
- );
241
-
242
- const user = userEvent.setup();
243
- user.click(screen.getByRole('button', { name: 'Drill down to Node 1 children' }));
244
-
245
- await waitFor(() => {
246
- expect(onSourceDrillDown).toHaveBeenCalledWith({
247
- source: sources[0],
248
- resource: sources[0].nodes[0],
249
- });
250
- });
251
- });
252
-
253
215
  it('Renders error state when an error occurs loading source list', async () => {
254
216
  const reload = jest.fn();
255
217
 
@@ -262,7 +224,6 @@ describe('SourceList', () => {
262
224
  previewModalState={previewModalState}
263
225
  isLoading={false}
264
226
  onSourceSelect={() => {}}
265
- onSourceDrillDown={() => {}}
266
227
  error={new Error('Source list error!')}
267
228
  handleReload={reload}
268
229
  />
@@ -7,15 +7,13 @@ 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
+ import ResourceState from '../ResourceState/ResourceState';
11
11
 
12
12
  export interface SourceListProps {
13
13
  sources: Source[];
14
14
  previewModalState: OverlayTriggerState;
15
15
  isLoading: boolean;
16
16
  onSourceSelect: (node: ScopedSource, overlayProps: DOMAttributes<FocusableElement>) => void;
17
- onSourceDrillDown: (node: ScopedSource) => void;
18
- allowedTypes?: string[] | undefined;
19
17
  handleReload: () => void;
20
18
  error: Error | null;
21
19
  }
@@ -25,8 +23,6 @@ const SourceList = function ({
25
23
  previewModalState,
26
24
  isLoading,
27
25
  onSourceSelect,
28
- onSourceDrillDown,
29
- allowedTypes,
30
26
  handleReload,
31
27
  error,
32
28
  }: SourceListProps) {
@@ -59,7 +55,7 @@ const SourceList = function ({
59
55
  </>
60
56
  )}
61
57
 
62
- {!isLoading && error && <ResourceError errorMessage={error.message} handleReload={handleReload} />}
58
+ {!isLoading && error && <ResourceState state="error" message={error.message} handleReload={handleReload} />}
63
59
 
64
60
  {!isLoading &&
65
61
  !error &&
@@ -78,12 +74,9 @@ const SourceList = function ({
78
74
  item={{ source, resource }}
79
75
  label={resource?.name || source.name}
80
76
  type={resource?.type.code || 'folder'}
81
- childCount={resource?.childCount || 0}
82
77
  previewModalState={previewModalState}
83
78
  onSelect={onSourceSelect}
84
- onDrillDown={onSourceDrillDown}
85
79
  className="mt-3 rounded-lg"
86
- allowedTypes={allowedTypes}
87
80
  />
88
81
  );
89
82
  })}
@@ -1,6 +0,0 @@
1
- interface ResourceError {
2
- errorMessage: string;
3
- handleReload: () => void;
4
- }
5
- declare const ResourceError: ({ errorMessage, handleReload }: ResourceError) => JSX.Element;
6
- export default ResourceError;
@@ -1,27 +0,0 @@
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;