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

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 (92) hide show
  1. package/lib/Hooks/useAsync.d.ts +21 -0
  2. package/lib/Hooks/useAsync.js +53 -0
  3. package/lib/Hooks/useChildResources.d.ts +3 -7
  4. package/lib/Hooks/useChildResources.js +5 -29
  5. package/lib/Hooks/useResource.d.ts +15 -0
  6. package/lib/Hooks/useResource.js +12 -0
  7. package/lib/Hooks/useResourcePath.d.ts +1 -1
  8. package/lib/Icons/Generics/Back.d.ts +4 -0
  9. package/lib/Icons/Generics/Back.js +12 -0
  10. package/lib/Icons/Generics/Empty.d.ts +4 -0
  11. package/lib/Icons/Generics/Empty.js +12 -0
  12. package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
  13. package/lib/Icons/Generics/GenericIconMap.js +2 -0
  14. package/lib/Icons/Generics/index.d.ts +2 -0
  15. package/lib/Icons/Generics/index.js +5 -1
  16. package/lib/Icons/Icon.d.ts +2 -0
  17. package/lib/PreviewPanel/details/MatrixResource.js +2 -1
  18. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +8 -0
  19. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +18 -0
  20. package/lib/ResourceItem/ResourceItem.d.ts +2 -2
  21. package/lib/ResourceItem/ResourceItem.js +6 -5
  22. package/lib/ResourceList/ResourceList.d.ts +3 -2
  23. package/lib/ResourceList/ResourceList.js +4 -3
  24. package/lib/ResourcePicker/ResetButton.d.ts +5 -0
  25. package/lib/ResourcePicker/ResetButton.js +11 -0
  26. package/lib/ResourcePicker/ResourcePicker.d.ts +14 -0
  27. package/lib/ResourcePicker/ResourcePicker.js +26 -0
  28. package/lib/ResourcePicker/States/Error.d.ts +6 -0
  29. package/lib/ResourcePicker/States/Error.js +14 -0
  30. package/lib/ResourcePicker/States/Loading.d.ts +1 -0
  31. package/lib/ResourcePicker/States/Loading.js +11 -0
  32. package/lib/ResourcePicker/States/Selected.d.ts +7 -0
  33. package/lib/ResourcePicker/States/Selected.js +43 -0
  34. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +5 -5
  35. package/lib/ResourceState/ResourceState.d.ts +7 -0
  36. package/lib/{ResourceError/ResourceError.js → ResourceState/ResourceState.js} +7 -7
  37. package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
  38. package/lib/SourceDropdown/SourceDropdown.js +3 -3
  39. package/lib/SourceList/SourceList.d.ts +1 -3
  40. package/lib/SourceList/SourceList.js +4 -4
  41. package/lib/StatusIndicator/StatusIndicator.d.ts +2 -1
  42. package/lib/StatusIndicator/StatusIndicator.js +3 -2
  43. package/lib/index.css +9 -3
  44. package/lib/index.d.ts +8 -7
  45. package/lib/index.js +35 -13
  46. package/lib/types.d.ts +67 -0
  47. package/lib/types.js +2 -0
  48. package/package.json +3 -3
  49. package/src/Hooks/useAsync.spec.ts +106 -0
  50. package/src/Hooks/useAsync.ts +62 -0
  51. package/src/Hooks/useChildResources.spec.ts +2 -23
  52. package/src/Hooks/useChildResources.ts +9 -34
  53. package/src/Hooks/useResource.spec.ts +32 -0
  54. package/src/Hooks/useResource.ts +19 -0
  55. package/src/Hooks/useSources.spec.ts +2 -14
  56. package/src/Hooks/useSources.ts +3 -26
  57. package/src/Icons/Generics/Back.tsx +13 -0
  58. package/src/Icons/Generics/Empty.tsx +13 -0
  59. package/src/Icons/Generics/GenericIconMap.ts +3 -1
  60. package/src/Icons/Generics/index.tsx +2 -0
  61. package/src/PreviewPanel/details/MatrixResource.tsx +1 -2
  62. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +32 -0
  63. package/src/ResourceBrowserContext/ResourceBrowserContext.ts +20 -0
  64. package/src/ResourceItem/ResourceItem.tsx +7 -5
  65. package/src/ResourceList/ResourceList.spec.tsx +6 -0
  66. package/src/ResourceList/ResourceList.tsx +12 -4
  67. package/src/ResourcePicker/ResetButton.tsx +7 -1
  68. package/src/ResourcePicker/ResourcePicker.spec.tsx +8 -4
  69. package/src/ResourcePicker/ResourcePicker.stories.tsx +2 -2
  70. package/src/ResourcePicker/ResourcePicker.tsx +21 -12
  71. package/src/ResourcePicker/States/Error.tsx +9 -3
  72. package/src/ResourcePicker/States/Selected.tsx +9 -4
  73. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +1 -1
  74. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +6 -7
  75. package/src/{ResourceError/ResourceError.spec.tsx → ResourceState/ResourceState.spec.tsx} +6 -5
  76. package/src/ResourceState/ResourceState.stories.tsx +24 -0
  77. package/src/ResourceState/ResourceState.tsx +31 -0
  78. package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
  79. package/src/SourceDropdown/SourceDropdown.tsx +3 -3
  80. package/src/SourceList/SourceList.spec.tsx +1 -40
  81. package/src/SourceList/SourceList.tsx +2 -9
  82. package/src/StatusIndicator/StatusIndicator.tsx +5 -2
  83. package/src/__mocks__/StorybookHelpers.ts +18 -13
  84. package/src/index.spec.tsx +4 -4
  85. package/src/index.stories.tsx +15 -15
  86. package/src/index.tsx +39 -54
  87. package/src/{types.d.ts → types.ts} +1 -1
  88. package/tailwind.config.cjs +5 -0
  89. package/lib/Hooks/useSources.d.ts +0 -16
  90. package/lib/Hooks/useSources.js +0 -31
  91. package/lib/ResourceError/ResourceError.d.ts +0 -6
  92. package/src/ResourceError/ResourceError.tsx +0 -27
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SkeletonListItem = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
- const SkeletonListItem = () => (react_1.default.createElement("li", { className: "flex items-center p-1 first:mt-0 bg-white border border-b-0 border-grey-200 first:rounded-t-lg last:rounded-b-lg last:border-b" },
8
+ const SkeletonListItem = () => (react_1.default.createElement("li", { className: "flex items-center p-1 first:mt-0 bg-white border-1 border-b-0 border-grey-200 first:rounded-t-lg last:rounded-b-lg last:border-b" },
9
9
  react_1.default.createElement("div", { className: "animate-skeleton-pulse grid grid-cols-[24px_1fr_45px] w-full flex items-center p-4 rounded" },
10
10
  react_1.default.createElement("span", { className: "w-6 h-6 bg-gray-200 rounded-full" }),
11
11
  react_1.default.createElement("div", { className: "w-full d-flex flex-col mx-4" },
@@ -62,7 +62,7 @@ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSo
62
62
  buttonRef.current?.focus();
63
63
  onRootSelect();
64
64
  };
65
- return (react_1.default.createElement("div", { ...focusWithinProps, ...keyboardProps, className: "relative w-72 border border-2 rounded border-gray-300" },
65
+ return (react_1.default.createElement("div", { ...focusWithinProps, ...keyboardProps, className: "relative w-72 border-2 rounded border-gray-300" },
66
66
  react_1.default.createElement("button", { ref: buttonRef, type: "button", "aria-label": "Source quick select", "aria-expanded": isOpen, "aria-controls": `${uniqueId}-button-menu`, onClick: () => setIsOpen(!isOpen), className: "relative flex items-center text-sm font-semibold p-1.5 w-full" },
67
67
  selectedSource && (react_1.default.createElement(react_1.default.Fragment, null,
68
68
  react_1.default.createElement("span", { className: "sr-only" }, "current source "),
@@ -73,8 +73,8 @@ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSo
73
73
  react_1.default.createElement(Icon_1.default, { icon: 'root', "aria-hidden": true, className: "mr-2.5 h-[20px] w-[20px]" }),
74
74
  "All available sources")),
75
75
  react_1.default.createElement(Icon_1.default, { icon: 'arrow-down', "aria-hidden": true, className: "absolute right-3" })),
76
- react_1.default.createElement("ul", { id: `${uniqueId}-button-menu`, "aria-hidden": !isOpen, className: `absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border border-2 rounded border-gray-300 p-2 ${!isOpen ? 'hidden' : ''}` },
77
- react_1.default.createElement("li", { key: "return-root", className: "flex items-center text-sm font-semibold mb-2 bg-white border rounded border-grey-200" },
76
+ react_1.default.createElement("ul", { id: `${uniqueId}-button-menu`, "aria-hidden": !isOpen, className: `absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border-2 rounded border-gray-300 p-2 ${!isOpen ? 'hidden' : ''}` },
77
+ react_1.default.createElement("li", { key: "return-root", className: "flex items-center text-sm font-semibold mb-2 bg-white border-1 rounded border-grey-200" },
78
78
  react_1.default.createElement("button", { type: "button", onClick: handleRootSelect, className: `relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100` },
79
79
  react_1.default.createElement(Icon_1.default, { icon: 'root', "aria-hidden": true, className: "mr-2.5" }),
80
80
  "All available sources")),
@@ -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
  };
@@ -1,6 +1,7 @@
1
1
  import { Status } from '../types';
2
2
  export type StatusIndicatorProps = {
3
+ className?: string;
3
4
  status: Status;
4
5
  };
5
- declare const StatusIndicator: ({ status }: StatusIndicatorProps) => JSX.Element;
6
+ declare const StatusIndicator: ({ className, status }: StatusIndicatorProps) => JSX.Element;
6
7
  export default StatusIndicator;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const react_1 = __importDefault(require("react"));
7
+ const clsx_1 = __importDefault(require("clsx"));
7
8
  const statusColour = {
8
9
  // Duplicated from the Matrix repository.
9
10
  // src/Api/AssetManagementApi/Constants/AssetStatuses.php - contains a list of possible statuses.
@@ -19,8 +20,8 @@ const statusColour = {
19
20
  safe_editing_pending_approval: '#d688db',
20
21
  safe_edit_approved_to_go_live: '#ffb34a',
21
22
  };
22
- const StatusIndicator = ({ status }) => {
23
+ const StatusIndicator = ({ className, status }) => {
23
24
  const color = statusColour[status.code] || statusColour.unknown;
24
- return (react_1.default.createElement("span", { style: { backgroundColor: color }, className: "block rounded-full w-3 h-3 border border-solid border-black border-opacity-20" }));
25
+ return (react_1.default.createElement("span", { style: { backgroundColor: color }, className: (0, clsx_1.default)('block rounded-full w-3 h-3 border-1 border-solid border-black border-opacity-20', className), title: status.name }));
25
26
  };
26
27
  exports.default = StatusIndicator;
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
  }
@@ -472,6 +475,9 @@
472
475
  .squiz-rb-scope .ml-auto {
473
476
  margin-left: auto;
474
477
  }
478
+ .squiz-rb-scope .mr-1 {
479
+ margin-right: 0.25rem;
480
+ }
475
481
  .squiz-rb-scope .mr-2 {
476
482
  margin-right: 0.5rem;
477
483
  }
@@ -610,9 +616,6 @@
610
616
  .squiz-rb-scope .w-72 {
611
617
  width: 18rem;
612
618
  }
613
- .squiz-rb-scope .w-\[119px\] {
614
- width: 119px;
615
- }
616
619
  .squiz-rb-scope .w-\[20px\] {
617
620
  width: 20px;
618
621
  }
@@ -711,6 +714,9 @@
711
714
  .squiz-rb-scope .border {
712
715
  border-width: 1px;
713
716
  }
717
+ .squiz-rb-scope .border-1 {
718
+ border-width: 1px;
719
+ }
714
720
  .squiz-rb-scope .border-2 {
715
721
  border-width: 2px;
716
722
  }
package/lib/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { HydratedResourceReference, Resource, ResourceReference, Source } from './types';
2
+ import { ResourceBrowserContext } from './ResourceBrowserContext/ResourceBrowserContext';
2
3
  export type { HydratedResourceReference, Resource, ResourceReference, Source };
3
- export default function ComponentEditorContentBrowser({ modalTitle, allowedTypes, onRequestSources, onRequestChildren, onChange, isDisabled, }: {
4
+ export { ResourceBrowserContext };
5
+ type ResourceBrowserInputProps = {
4
6
  modalTitle: string;
5
- allowedTypes: string[] | undefined;
6
- onRequestSources: () => Promise<Source[]>;
7
- onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
8
- onRequestResource(reference: ResourceReference): Promise<Resource | null>;
9
- onChange(resource: HydratedResourceReference | null): void;
7
+ allowedTypes?: string[];
10
8
  isDisabled?: boolean;
11
- }): JSX.Element;
9
+ value: ResourceReference | null;
10
+ onChange(resource: HydratedResourceReference | null): void;
11
+ };
12
+ export declare const ResourceBrowserInput: ({ modalTitle, allowedTypes, onChange, value, isDisabled, }: ResourceBrowserInputProps) => JSX.Element;
package/lib/index.js CHANGED
@@ -1,20 +1,42 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
5
28
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const react_1 = __importDefault(require("react"));
7
- const ModalTrigger_1 = __importDefault(require("./Modal/ModalTrigger"));
29
+ exports.ResourceBrowserInput = exports.ResourceBrowserContext = void 0;
30
+ const react_1 = __importStar(require("react"));
8
31
  const ResourcePickerContainer_1 = __importDefault(require("./ResourcePickerContainer/ResourcePickerContainer"));
9
- const AdsClickRounded_1 = __importDefault(require("@mui/icons-material/AdsClickRounded"));
10
- const AddCircleOutlineRounded_1 = __importDefault(require("@mui/icons-material/AddCircleOutlineRounded"));
11
- const PhotoLibraryRounded_1 = __importDefault(require("@mui/icons-material/PhotoLibraryRounded"));
12
- const clsx_1 = __importDefault(require("clsx"));
13
- function ComponentEditorContentBrowser({ modalTitle, allowedTypes, onRequestSources, onRequestChildren, onChange, isDisabled, }) {
14
- const isImagePicker = allowedTypes && allowedTypes.length === 1 && allowedTypes.includes('image');
32
+ const ResourceBrowserContext_1 = require("./ResourceBrowserContext/ResourceBrowserContext");
33
+ Object.defineProperty(exports, "ResourceBrowserContext", { enumerable: true, get: function () { return ResourceBrowserContext_1.ResourceBrowserContext; } });
34
+ const ResourcePicker_1 = __importDefault(require("./ResourcePicker/ResourcePicker"));
35
+ const useResource_1 = require("./Hooks/useResource");
36
+ const ResourceBrowserInput = ({ modalTitle, allowedTypes, onChange, value, isDisabled, }) => {
37
+ const { onRequestSources, onRequestChildren, onRequestResource } = (0, react_1.useContext)(ResourceBrowserContext_1.ResourceBrowserContext);
38
+ const { data: resource, error, isLoading } = (0, useResource_1.useResource)({ onRequestResource, reference: value });
15
39
  return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
16
- react_1.default.createElement("div", { className: (0, clsx_1.default)('resource-picker', isDisabled && 'bg-gray-300') },
17
- isImagePicker ? (react_1.default.createElement(PhotoLibraryRounded_1.default, { "aria-hidden": true, className: "w-6 h-6" })) : (react_1.default.createElement(AdsClickRounded_1.default, { "aria-hidden": true, className: "w-6 h-6" })),
18
- react_1.default.createElement(ModalTrigger_1.default, { showLabel: true, label: isImagePicker ? `Choose image` : `Choose asset`, icon: react_1.default.createElement(AddCircleOutlineRounded_1.default, { "aria-hidden": true, className: "!w-4 !h-4" }), isDisabled: isDisabled }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestChildren: onRequestChildren, onChange: onChange }))))));
19
- }
20
- exports.default = ComponentEditorContentBrowser;
40
+ react_1.default.createElement(ResourcePicker_1.default, { resource: resource, allowedTypes: allowedTypes, error: error, isLoading: isLoading, isDisabled: isDisabled, onClear: () => onChange(null) }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestChildren: onRequestChildren, onChange: onChange })))));
41
+ };
42
+ exports.ResourceBrowserInput = ResourceBrowserInput;
package/lib/types.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { SquizImageType } from '@squiz/dx-json-schema-lib';
2
+ export type Status = {
3
+ code: string;
4
+ name: string;
5
+ };
6
+ /**
7
+ * Represents a resource that has been picked from a source.
8
+ */
9
+ export type Resource = {
10
+ id: string;
11
+ name: string;
12
+ type: {
13
+ code: string;
14
+ name: string;
15
+ };
16
+ status: Status;
17
+ url: string;
18
+ urls: string[];
19
+ childCount: number;
20
+ squizImage?: SquizImageType['__shape__'];
21
+ };
22
+ /**
23
+ * Represents a system that resources can be picked from.
24
+ * Optionally, may indicate a list of "nodes" that can be used to scope the source to a subset of its resources.
25
+ */
26
+ export type Source = {
27
+ id: string;
28
+ name: string;
29
+ nodes: Resource[];
30
+ };
31
+ /**
32
+ * Represents a source that has been optionally scoped to one of its "nodes".
33
+ */
34
+ export type ScopedSource = {
35
+ source: Source;
36
+ resource: Resource | null;
37
+ };
38
+ /**
39
+ * A non-hydrated resource reference.
40
+ */
41
+ export type ResourceReference = {
42
+ source: string;
43
+ resource: string;
44
+ };
45
+ /**
46
+ * A hydrated resource reference.
47
+ */
48
+ export type HydratedResourceReference = {
49
+ source: Source;
50
+ resource: Resource;
51
+ };
52
+ /**
53
+ * Represents the hierarchy within the asset picker.
54
+ * Within the picker T will be ScopedSource|Resource.
55
+ * ScopedSource will be the first item in the array, Resource will be every other item.
56
+ */
57
+ export type Hierarchy<T> = Array<{
58
+ key: string;
59
+ label: string;
60
+ node: T;
61
+ }>;
62
+ /**
63
+ * Augments a type so that all properties are optional.
64
+ */
65
+ export type DeepPartial<T> = {
66
+ [P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : DeepPartial<T[P]>;
67
+ };
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
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.34",
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.34",
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": "34965675fd331238d984f5c4d86cc96d85b5f165"
77
77
  }
@@ -0,0 +1,106 @@
1
+ import { DependencyList } from 'react';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import { useAsync } from './useAsync';
4
+
5
+ describe('useAsync', () => {
6
+ const renderAsyncHook = (callback: () => any, deps: DependencyList) => {
7
+ return renderHook(
8
+ ({ deps }: { deps: DependencyList }) => useAsync({ callback, defaultValue: 'Initial state' }, deps),
9
+ { initialProps: { deps } },
10
+ );
11
+ };
12
+
13
+ it('Should invoke callback when hook is initially rendered', async () => {
14
+ const callback = jest.fn().mockResolvedValue('Resolved data');
15
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
16
+
17
+ expect(result.current.isLoading).toBe(true);
18
+ expect(result.current.error).toBe(null);
19
+ expect(result.current.data).toBe('Initial state');
20
+
21
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
22
+
23
+ expect(result.current.isLoading).toBe(false);
24
+ expect(result.current.error).toBe(null);
25
+ expect(result.current.data).toBe('Resolved data');
26
+ });
27
+
28
+ it('Should not invoke callback when hook is re-rendered with the same dependencies', async () => {
29
+ const callback = jest.fn().mockResolvedValue('Resolved data');
30
+ const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']);
31
+
32
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
33
+
34
+ // If we re-render with the same "deps" we should not attempt to re-load data.
35
+ rerender({ deps: ['initial_dependency_value'] });
36
+
37
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
38
+
39
+ expect(result.current.isLoading).toBe(false);
40
+ expect(result.current.error).toBe(null);
41
+ expect(result.current.data).toBe('Resolved data');
42
+ expect(callback).toBeCalledTimes(1);
43
+ });
44
+
45
+ it('Should invoke callback when hook is re-rendered with different dependencies', async () => {
46
+ const callback = jest.fn().mockResolvedValueOnce('Resolved data').mockResolvedValueOnce('Updated resolved data');
47
+ const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']);
48
+
49
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
50
+
51
+ // If we re-render with the same "deps" we should not attempt to re-load data.
52
+ rerender({ deps: ['updated_dependency_value'] });
53
+
54
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
55
+
56
+ expect(result.current.isLoading).toBe(false);
57
+ expect(result.current.error).toBe(null);
58
+ expect(result.current.data).toBe('Updated resolved data');
59
+ expect(callback).toBeCalledTimes(2);
60
+ });
61
+
62
+ it.each([
63
+ ['Error object thrown', new Error('Callback failed'), 'Callback failed'],
64
+ ['Non-error object thrown', 'Callback failed', 'Callback failed'],
65
+ ])(
66
+ 'Should retain the error if the callback rejects - %s',
67
+ async (description: string, error: any, expectedErrorMessage: string) => {
68
+ const callback = jest.fn().mockRejectedValue(error);
69
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
70
+
71
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
72
+
73
+ expect(result.current.isLoading).toBe(false);
74
+ expect(result.current.error).toBeInstanceOf(Error);
75
+ expect(result.current.error?.message).toBe(expectedErrorMessage);
76
+ expect(result.current.data).toBe('Initial state');
77
+ },
78
+ );
79
+
80
+ it('Should handle non-async return data returned from callback', async () => {
81
+ const callback = jest.fn(() => 'Returned data');
82
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
83
+
84
+ expect(result.current.isLoading).toBe(false);
85
+ expect(result.current.error).toBe(null);
86
+ expect(result.current.data).toBe('Returned data');
87
+ });
88
+
89
+ it.each([
90
+ ['Error object thrown', new Error('Callback failed'), 'Callback failed'],
91
+ ['Non-error object thrown', 'Callback failed', 'Callback failed'],
92
+ ])(
93
+ 'Should handle non-async thrown errors from callback - %s',
94
+ async (description: string, error: any, expectedErrorMessage: string) => {
95
+ const callback = jest.fn(() => {
96
+ throw error;
97
+ });
98
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
99
+
100
+ expect(result.current.isLoading).toBe(false);
101
+ expect(result.current.error).toBeInstanceOf(Error);
102
+ expect(result.current.error?.message).toBe(expectedErrorMessage);
103
+ expect(result.current.data).toBe('Initial state');
104
+ },
105
+ );
106
+ });
@@ -0,0 +1,62 @@
1
+ import { DependencyList, useState, useCallback, useEffect } from 'react';
2
+
3
+ export type UseAsyncProps<TReturnType, TDefaultValueType> = {
4
+ /** The async callback to call for fetching data. */
5
+ callback: () => TReturnType | Promise<TReturnType>;
6
+ /** The default value to populate the data as when initially mounted or reloading data. */
7
+ defaultValue: TReturnType | TDefaultValueType;
8
+ };
9
+
10
+ /**
11
+ * Hook for invoking a piece of async code and keeping track of its state.
12
+ *
13
+ * Data is loaded in 3 different ways:
14
+ * 1. On initial mount.
15
+ * 2. When any of the `deps` change.
16
+ * 3. When the `relaod` function is called.
17
+ */
18
+ export const useAsync = <TReturnType, TDefaultValueType>(
19
+ { callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>,
20
+ deps: DependencyList,
21
+ ) => {
22
+ const [data, setData] = useState(defaultValue);
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [error, setError] = useState<Error | null>(null);
25
+ const reload = useCallback(() => {
26
+ setIsLoading(true);
27
+ setError(null);
28
+ setData(defaultValue);
29
+
30
+ try {
31
+ const result = callback();
32
+
33
+ if (result instanceof Promise) {
34
+ // if the callback returned a promise wait for it to either resolve or reject.
35
+ result
36
+ .then((resolved: TReturnType) => {
37
+ setData(resolved);
38
+ setIsLoading(false);
39
+ })
40
+ .catch((e: unknown) => {
41
+ setError(e instanceof Error ? e : new Error(String(e)));
42
+ setIsLoading(false);
43
+ });
44
+ } else {
45
+ // if the callback returned something other than a promise assume it is the data we want.
46
+ setData(result);
47
+ setIsLoading(false);
48
+ }
49
+ } catch (e: unknown) {
50
+ // callback threw outside of the scope of the promise.
51
+ setError(e instanceof Error ? e : new Error(String(e)));
52
+ setIsLoading(false);
53
+ }
54
+ }, deps);
55
+
56
+ // reload data on dependency change (and initial mount)
57
+ useEffect(() => {
58
+ reload();
59
+ }, deps);
60
+
61
+ return { data, error, isLoading, reload };
62
+ };
@@ -19,32 +19,11 @@ describe('useChildResources', () => {
19
19
 
20
20
  expect(result.current.isLoading).toBe(true);
21
21
  expect(result.current.error).toBe(null);
22
- expect(result.current.resources).toEqual([]);
22
+ expect(result.current.data).toEqual([]);
23
23
 
24
24
  await waitFor(() => expect(result.current.isLoading).toBe(false));
25
25
 
26
26
  expect(result.current.isLoading).toBe(false);
27
- expect(result.current.resources).toBe(children);
28
- });
29
-
30
- it('Should return the error if loading resources fails', async () => {
31
- const source = mockScopedSource();
32
- const currentResource = mockResource({ name: 'Current resource' });
33
- const error = new Error('Loading the resources failed.');
34
- const onRequestChildren = jest.fn().mockRejectedValue(error);
35
-
36
- const { result } = renderHook(() =>
37
- useChildResources({
38
- source,
39
- currentResource,
40
- onRequestChildren,
41
- }),
42
- );
43
-
44
- await waitFor(() => expect(result.current.isLoading).toBe(false));
45
-
46
- expect(result.current.isLoading).toBe(false);
47
- expect(result.current.error).toBe(error);
48
- expect(result.current.resources).toEqual([]);
27
+ expect(result.current.data).toBe(children);
49
28
  });
50
29
  });
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
1
  import { Resource, ScopedSource, Source } from '../types';
2
+ import { useAsync } from './useAsync';
3
3
 
4
4
  type UseChildResourcesProps = {
5
5
  source: ScopedSource | null;
@@ -9,37 +9,12 @@ type UseChildResourcesProps = {
9
9
 
10
10
  /**
11
11
  * Triggers a reload of the child resources when the source or current resource change.
12
- *
13
- * @param {ScopedSource|null} source
14
- * @param {Resource|null} currentResource
15
- * @param {Function} onRequestChildren
16
12
  */
17
- export const useChildResources = ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) => {
18
- const [error, setError] = useState<Error | null>(null);
19
- const [isLoading, setIsLoading] = useState(false);
20
- const [resources, setResources] = useState<Resource[]>([]);
21
-
22
- const loadResource = useCallback(() => {
23
- setError(null);
24
- setResources([]);
25
-
26
- if (source) {
27
- setIsLoading(true);
28
-
29
- onRequestChildren(source.source, currentResource)
30
- .then((resources: Array<Resource>) => {
31
- setResources(resources);
32
- setIsLoading(false);
33
- })
34
- .catch((e) => {
35
- setError(e);
36
- setIsLoading(false);
37
- });
38
- }
39
- }, [source, currentResource]);
40
-
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 };
45
- };
13
+ export const useChildResources = ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) =>
14
+ useAsync(
15
+ {
16
+ callback: () => (source ? onRequestChildren(source.source, currentResource || source.resource) : []),
17
+ defaultValue: [],
18
+ },
19
+ [source, currentResource],
20
+ );
@@ -0,0 +1,32 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { mockResource } from '../__mocks__/MockModels';
3
+ import { useResource } from './useResource';
4
+
5
+ describe('useResource', () => {
6
+ it('Should load the resource', async () => {
7
+ const resource = mockResource();
8
+ const reference = { source: 'source-id', resource: 'resource-id' };
9
+ const onRequestResource = jest.fn().mockResolvedValue(resource);
10
+ const { result } = renderHook(() => useResource({ onRequestResource, reference }));
11
+
12
+ expect(result.current.isLoading).toBe(true);
13
+ expect(result.current.error).toBe(null);
14
+ expect(result.current.data).toEqual(null);
15
+
16
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
17
+
18
+ expect(result.current.isLoading).toBe(false);
19
+ expect(result.current.data).toBe(resource);
20
+ });
21
+
22
+ it('Should not load the resource if no reference provided', async () => {
23
+ const reference = null;
24
+ const onRequestResource = jest.fn();
25
+ const { result } = renderHook(() => useResource({ onRequestResource, reference }));
26
+
27
+ expect(result.current.isLoading).toBe(false);
28
+ expect(result.current.error).toBe(null);
29
+ expect(result.current.data).toEqual(null);
30
+ expect(onRequestResource).not.toBeCalled();
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import { useAsync } from './useAsync';
2
+ import { Resource, ResourceReference } from '../types';
3
+
4
+ type UseResourceProps = {
5
+ onRequestResource: (reference: ResourceReference) => Promise<Resource | null>;
6
+ reference?: ResourceReference | null;
7
+ };
8
+
9
+ /**
10
+ * Loads the resource indicated by the provided reference.
11
+ */
12
+ export const useResource = ({ onRequestResource, reference }: UseResourceProps) =>
13
+ useAsync(
14
+ {
15
+ callback: () => (reference ? onRequestResource(reference) : null),
16
+ defaultValue: null,
17
+ },
18
+ [reference?.source, reference?.resource],
19
+ );