@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.
- package/lib/Hooks/useAsync.d.ts +21 -0
- package/lib/Hooks/useAsync.js +53 -0
- package/lib/Hooks/useChildResources.d.ts +3 -7
- package/lib/Hooks/useChildResources.js +5 -29
- package/lib/Hooks/useResource.d.ts +15 -0
- package/lib/Hooks/useResource.js +12 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Icons/Generics/Back.d.ts +4 -0
- package/lib/Icons/Generics/Back.js +12 -0
- package/lib/Icons/Generics/Empty.d.ts +4 -0
- package/lib/Icons/Generics/Empty.js +12 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
- package/lib/Icons/Generics/GenericIconMap.js +2 -0
- package/lib/Icons/Generics/index.d.ts +2 -0
- package/lib/Icons/Generics/index.js +5 -1
- package/lib/Icons/Icon.d.ts +2 -0
- package/lib/PreviewPanel/details/MatrixResource.js +2 -1
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +8 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +18 -0
- package/lib/ResourceItem/ResourceItem.d.ts +2 -2
- package/lib/ResourceItem/ResourceItem.js +6 -5
- package/lib/ResourceList/ResourceList.d.ts +3 -2
- package/lib/ResourceList/ResourceList.js +4 -3
- package/lib/ResourcePicker/ResetButton.d.ts +5 -0
- package/lib/ResourcePicker/ResetButton.js +11 -0
- package/lib/ResourcePicker/ResourcePicker.d.ts +14 -0
- package/lib/ResourcePicker/ResourcePicker.js +26 -0
- package/lib/ResourcePicker/States/Error.d.ts +6 -0
- package/lib/ResourcePicker/States/Error.js +14 -0
- package/lib/ResourcePicker/States/Loading.d.ts +1 -0
- package/lib/ResourcePicker/States/Loading.js +11 -0
- package/lib/ResourcePicker/States/Selected.d.ts +7 -0
- package/lib/ResourcePicker/States/Selected.js +43 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +5 -5
- package/lib/ResourceState/ResourceState.d.ts +7 -0
- package/lib/{ResourceError/ResourceError.js → ResourceState/ResourceState.js} +7 -7
- package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
- package/lib/SourceDropdown/SourceDropdown.js +3 -3
- package/lib/SourceList/SourceList.d.ts +1 -3
- package/lib/SourceList/SourceList.js +4 -4
- package/lib/StatusIndicator/StatusIndicator.d.ts +2 -1
- package/lib/StatusIndicator/StatusIndicator.js +3 -2
- package/lib/index.css +9 -3
- package/lib/index.d.ts +8 -7
- package/lib/index.js +35 -13
- package/lib/types.d.ts +67 -0
- package/lib/types.js +2 -0
- package/package.json +3 -3
- package/src/Hooks/useAsync.spec.ts +106 -0
- package/src/Hooks/useAsync.ts +62 -0
- package/src/Hooks/useChildResources.spec.ts +2 -23
- package/src/Hooks/useChildResources.ts +9 -34
- package/src/Hooks/useResource.spec.ts +32 -0
- package/src/Hooks/useResource.ts +19 -0
- package/src/Hooks/useSources.spec.ts +2 -14
- package/src/Hooks/useSources.ts +3 -26
- package/src/Icons/Generics/Back.tsx +13 -0
- package/src/Icons/Generics/Empty.tsx +13 -0
- package/src/Icons/Generics/GenericIconMap.ts +3 -1
- package/src/Icons/Generics/index.tsx +2 -0
- package/src/PreviewPanel/details/MatrixResource.tsx +1 -2
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +32 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +20 -0
- package/src/ResourceItem/ResourceItem.tsx +7 -5
- package/src/ResourceList/ResourceList.spec.tsx +6 -0
- package/src/ResourceList/ResourceList.tsx +12 -4
- package/src/ResourcePicker/ResetButton.tsx +7 -1
- package/src/ResourcePicker/ResourcePicker.spec.tsx +8 -4
- package/src/ResourcePicker/ResourcePicker.stories.tsx +2 -2
- package/src/ResourcePicker/ResourcePicker.tsx +21 -12
- package/src/ResourcePicker/States/Error.tsx +9 -3
- package/src/ResourcePicker/States/Selected.tsx +9 -4
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +6 -7
- package/src/{ResourceError/ResourceError.spec.tsx → ResourceState/ResourceState.spec.tsx} +6 -5
- package/src/ResourceState/ResourceState.stories.tsx +24 -0
- package/src/ResourceState/ResourceState.tsx +31 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
- package/src/SourceDropdown/SourceDropdown.tsx +3 -3
- package/src/SourceList/SourceList.spec.tsx +1 -40
- package/src/SourceList/SourceList.tsx +2 -9
- package/src/StatusIndicator/StatusIndicator.tsx +5 -2
- package/src/__mocks__/StorybookHelpers.ts +18 -13
- package/src/index.spec.tsx +4 -4
- package/src/index.stories.tsx +15 -15
- package/src/index.tsx +39 -54
- package/src/{types.d.ts → types.ts} +1 -1
- package/tailwind.config.cjs +5 -0
- package/lib/Hooks/useSources.d.ts +0 -16
- package/lib/Hooks/useSources.js +0 -31
- package/lib/ResourceError/ResourceError.d.ts +0 -6
- 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
|
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
|
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,
|
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
|
35
|
-
const SourceList = function ({ sources, previewModalState, isLoading, onSourceSelect,
|
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(
|
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',
|
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:
|
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
|
4
|
+
export { ResourceBrowserContext };
|
5
|
+
type ResourceBrowserInputProps = {
|
4
6
|
modalTitle: string;
|
5
|
-
allowedTypes
|
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
|
-
|
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
|
-
|
7
|
-
const
|
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
|
10
|
-
|
11
|
-
const
|
12
|
-
const
|
13
|
-
|
14
|
-
const
|
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(
|
17
|
-
|
18
|
-
|
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
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/resource-browser",
|
3
|
-
"version": "1.32.1-alpha.
|
3
|
+
"version": "1.32.1-alpha.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.
|
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": "
|
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.
|
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.
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
+
);
|