@squiz/resource-browser 1.66.3 → 1.67.1
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/CHANGELOG.md +19 -0
- package/lib/Hooks/usePreselectedResourcePath.d.ts +20 -0
- package/lib/Hooks/usePreselectedResourcePath.js +26 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Hooks/useResourcePath.js +2 -2
- package/lib/Hooks/useSources.d.ts +14 -0
- package/lib/Hooks/useSources.js +9 -0
- package/lib/Icons/CircledLoopIcon.d.ts +4 -0
- package/lib/Icons/CircledLoopIcon.js +12 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +12 -5
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +52 -2
- package/lib/ResourcePicker/ResourcePicker.js +1 -1
- package/lib/ResourcePicker/States/Selected.d.ts +2 -1
- package/lib/ResourcePicker/States/Selected.js +6 -2
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +7 -4
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +35 -11
- package/lib/SourceDropdown/SourceDropdown.js +1 -1
- package/lib/index.css +19 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -2
- package/lib/types.d.ts +7 -0
- package/lib/utils/findBestMatchLineage.d.ts +2 -0
- package/lib/utils/findBestMatchLineage.js +28 -0
- package/package.json +6 -4
- package/src/Hooks/usePreselectedResourcePath.ts +50 -0
- package/src/Hooks/useResourcePath.ts +2 -2
- package/src/Hooks/useSources.ts +1 -1
- package/src/Icons/CircledLoopIcon.tsx +14 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +93 -3
- package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +56 -0
- package/src/ResourceList/sample-resources.json +684 -439
- package/src/ResourcePicker/ResourcePicker.tsx +8 -1
- package/src/ResourcePicker/States/Selected.tsx +23 -3
- package/src/ResourcePicker/resource-picker.scss +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +146 -32
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +64 -18
- package/src/SourceDropdown/SourceDropdown.tsx +1 -1
- package/src/SourceList/sample-sources.json +4 -4
- package/src/__mocks__/MockModels.ts +1 -0
- package/src/__mocks__/StorybookHelpers.ts +33 -4
- package/src/__mocks__/renderWithContext.tsx +23 -0
- package/src/index.spec.tsx +81 -21
- package/src/index.stories.tsx +4 -4
- package/src/index.tsx +10 -2
- package/src/types.ts +9 -0
- package/src/utils/findBestMatchLineage.spec.ts +81 -0
- package/src/utils/findBestMatchLineage.ts +30 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +0 -20
- /package/lib/{uuid.d.ts → utils/uuid.d.ts} +0 -0
- /package/lib/{uuid.js → utils/uuid.js} +0 -0
- /package/src/{uuid.ts → utils/uuid.ts} +0 -0
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# @squiz/resource-browser
|
2
2
|
|
3
|
+
## 1.67.1
|
4
|
+
|
5
|
+
### Patch Changes
|
6
|
+
|
7
|
+
- 9e4cad8: Fixed minor issue with finding best matched lineage in resource browser
|
8
|
+
|
9
|
+
## 1.67.0
|
10
|
+
|
11
|
+
### Minor Changes
|
12
|
+
|
13
|
+
- 5fa9b39: Updated resource browser to allow replacing the previously selected resource. Replacing will open the resource browser to the location where the resource resides.
|
14
|
+
|
15
|
+
### Patch Changes
|
16
|
+
|
17
|
+
- Updated dependencies [5fa9b39]
|
18
|
+
- Updated dependencies [5fa9b39]
|
19
|
+
- @squiz/dx-json-schema-lib@1.67.0
|
20
|
+
- @squiz/generic-browser-lib@1.66.0
|
21
|
+
|
3
22
|
## 1.66.3
|
4
23
|
|
5
24
|
### Patch Changes
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { Source, Resource, OnRequestResource, OnRequestSources } from '../types';
|
2
|
+
export type PreselectedResourceProps = {
|
3
|
+
sourceId?: string;
|
4
|
+
resource?: Resource | null;
|
5
|
+
onRequestResource: OnRequestResource;
|
6
|
+
onRequestSources: OnRequestSources;
|
7
|
+
};
|
8
|
+
export type PreselectedResourcePath = {
|
9
|
+
source?: Source;
|
10
|
+
path?: Resource[];
|
11
|
+
};
|
12
|
+
export declare const usePreselectedResourcePath: ({ sourceId, resource, onRequestResource, onRequestSources, }: PreselectedResourceProps) => {
|
13
|
+
data: PreselectedResourcePath | {
|
14
|
+
source: Source | undefined;
|
15
|
+
path: Resource[] | undefined;
|
16
|
+
};
|
17
|
+
error: Error | null;
|
18
|
+
isLoading: boolean;
|
19
|
+
reload: () => void;
|
20
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.usePreselectedResourcePath = void 0;
|
4
|
+
const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
5
|
+
const findBestMatchLineage_1 = require("../utils/findBestMatchLineage");
|
6
|
+
const usePreselectedResourcePath = ({ sourceId, resource, onRequestResource, onRequestSources, }) => {
|
7
|
+
return (0, generic_browser_lib_1.useAsync)({
|
8
|
+
callback: async () => {
|
9
|
+
let source;
|
10
|
+
let path;
|
11
|
+
if (sourceId) {
|
12
|
+
const sources = await onRequestSources();
|
13
|
+
source = sources.find((source) => source.id === sourceId);
|
14
|
+
}
|
15
|
+
if (sourceId && source && resource) {
|
16
|
+
const bestMatchLineage = (0, findBestMatchLineage_1.findBestMatchLineage)(source, resource);
|
17
|
+
path = await Promise.all(bestMatchLineage.map(async (resourceId) => {
|
18
|
+
return onRequestResource({ source: sourceId, resource: resourceId });
|
19
|
+
}));
|
20
|
+
}
|
21
|
+
return { source, path };
|
22
|
+
},
|
23
|
+
defaultValue: {},
|
24
|
+
}, [sourceId, resource]);
|
25
|
+
};
|
26
|
+
exports.usePreselectedResourcePath = usePreselectedResourcePath;
|
@@ -10,7 +10,7 @@ export declare const useResourcePath: () => {
|
|
10
10
|
source: ScopedSource | null;
|
11
11
|
currentResource: Resource | null;
|
12
12
|
hierarchy: Hierarchy<Resource | ScopedSource>;
|
13
|
-
setSource: (source: ScopedSource | null) => void;
|
13
|
+
setSource: (source: ScopedSource | null, path?: Resource[]) => void;
|
14
14
|
push: (resource: Resource) => void;
|
15
15
|
popUntil: (node: ScopedSource | Resource) => void;
|
16
16
|
};
|
@@ -12,9 +12,9 @@ const react_1 = require("react");
|
|
12
12
|
const useResourcePath = () => {
|
13
13
|
const [source, setSourceInternal] = (0, react_1.useState)(null);
|
14
14
|
const [resourceStack, setResourceStack] = (0, react_1.useState)([]);
|
15
|
-
const setSource = (0, react_1.useCallback)((source) => {
|
15
|
+
const setSource = (0, react_1.useCallback)((source, path = []) => {
|
16
16
|
setSourceInternal(source);
|
17
|
-
setResourceStack(
|
17
|
+
setResourceStack(path);
|
18
18
|
}, []);
|
19
19
|
const push = (0, react_1.useCallback)((resource) => {
|
20
20
|
setResourceStack([...resourceStack, resource]);
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import { Source } from '../types';
|
2
|
+
type UseSourcesProps = {
|
3
|
+
onRequestSources: () => Promise<Source[]>;
|
4
|
+
};
|
5
|
+
/**
|
6
|
+
* Loads and caches the source list when a component using the hook is mounted.
|
7
|
+
*/
|
8
|
+
export declare const useSources: ({ onRequestSources }: UseSourcesProps) => {
|
9
|
+
data: Source[];
|
10
|
+
error: Error | null;
|
11
|
+
isLoading: boolean;
|
12
|
+
reload: () => void;
|
13
|
+
};
|
14
|
+
export {};
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.useSources = void 0;
|
4
|
+
const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
5
|
+
/**
|
6
|
+
* Loads and caches the source list when a component using the hook is mounted.
|
7
|
+
*/
|
8
|
+
const useSources = ({ onRequestSources }) => (0, generic_browser_lib_1.useAsync)({ callback: onRequestSources, defaultValue: [] }, []);
|
9
|
+
exports.useSources = useSources;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.CircledLoopIcon = void 0;
|
7
|
+
const react_1 = __importDefault(require("react"));
|
8
|
+
const CircledLoopIcon = (props) => {
|
9
|
+
return (react_1.default.createElement("svg", { fill: "none", height: "16", viewBox: "0 0 16 16", width: "16", xmlns: "http://www.w3.org/2000/svg", ...props },
|
10
|
+
react_1.default.createElement("path", { d: "M8 0.5C3.86 0.5 0.5 3.86 0.5 8C0.5 12.14 3.86 15.5 8 15.5C12.14 15.5 15.5 12.14 15.5 8C15.5 3.86 12.14 0.5 8 0.5ZM8 14C4.6925 14 2 11.3075 2 8C2 4.6925 4.6925 2 8 2C11.3075 2 14 4.6925 14 8C14 11.3075 11.3075 14 8 14ZM11.1275 10.07L10.3025 9.245C10.835 8.2475 10.7 6.9875 9.86 6.1475C9.3425 5.63 8.675 5.375 8 5.375C7.9775 5.375 7.955 5.3825 7.9325 5.3825L8.75 6.2L7.955 6.995L5.8325 4.8725L7.955 2.75L8.75 3.545L8.03 4.265C8.9825 4.2725 9.9275 4.625 10.655 5.345C11.93 6.6275 12.0875 8.615 11.1275 10.07ZM10.1675 11.1275L8.045 13.25L7.25 12.455L7.9625 11.7425C7.0175 11.735 6.0725 11.3675 5.3525 10.6475C4.07 9.365 3.9125 7.385 4.8725 5.93L5.6975 6.755C5.165 7.7525 5.3 9.0125 6.14 9.8525C6.665 10.3775 7.3625 10.6325 8.06 10.61L7.25 9.8L8.045 9.005L10.1675 11.1275Z", fill: "#949494" })));
|
11
|
+
};
|
12
|
+
exports.CircledLoopIcon = CircledLoopIcon;
|
@@ -1,8 +1,15 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import {
|
1
|
+
import React, { PropsWithChildren } from 'react';
|
2
|
+
import { OnRequestResource, OnRequestSources, OnRequestChildren } from '../types';
|
3
3
|
export type ResourceBrowserContextProps = {
|
4
|
-
onRequestSources:
|
5
|
-
onRequestChildren
|
6
|
-
onRequestResource
|
4
|
+
onRequestSources: OnRequestSources;
|
5
|
+
onRequestChildren: OnRequestChildren;
|
6
|
+
onRequestResource: OnRequestResource;
|
7
7
|
};
|
8
|
+
/**
|
9
|
+
* @internal Direct usage of this object is discouraged. It will be privated in a future major version.
|
10
|
+
* Please use ResourceBrowserContextProvider instead.
|
11
|
+
*/
|
8
12
|
export declare const ResourceBrowserContext: React.Context<ResourceBrowserContextProps>;
|
13
|
+
export declare const ResourceBrowserContextProvider: (props: PropsWithChildren<{
|
14
|
+
value: ResourceBrowserContextProps;
|
15
|
+
}>) => React.JSX.Element;
|
@@ -1,10 +1,39 @@
|
|
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
|
-
exports.ResourceBrowserContext = void 0;
|
7
|
-
const react_1 =
|
29
|
+
exports.ResourceBrowserContextProvider = exports.ResourceBrowserContext = void 0;
|
30
|
+
const react_1 = __importStar(require("react"));
|
31
|
+
const p_memoize_1 = __importDefault(require("p-memoize"));
|
32
|
+
const expiry_map_1 = __importDefault(require("expiry-map"));
|
33
|
+
/**
|
34
|
+
* @internal Direct usage of this object is discouraged. It will be privated in a future major version.
|
35
|
+
* Please use ResourceBrowserContextProvider instead.
|
36
|
+
*/
|
8
37
|
exports.ResourceBrowserContext = react_1.default.createContext({
|
9
38
|
onRequestSources: () => {
|
10
39
|
throw new Error('onRequestSources has not been configured.');
|
@@ -16,3 +45,24 @@ exports.ResourceBrowserContext = react_1.default.createContext({
|
|
16
45
|
throw new Error('onRequestResource has not been configured.');
|
17
46
|
},
|
18
47
|
});
|
48
|
+
const ResourceBrowserContextProvider = (props) => {
|
49
|
+
const CACHE_DURATION = 30000; // 30 seconds
|
50
|
+
const { value: { onRequestSources, onRequestChildren, onRequestResource, ...other }, children, } = props;
|
51
|
+
const cache = new expiry_map_1.default(CACHE_DURATION);
|
52
|
+
const memoized = (0, react_1.useMemo)(() => ({
|
53
|
+
onRequestSources: (0, p_memoize_1.default)(onRequestSources, {
|
54
|
+
cache,
|
55
|
+
cacheKey: () => 'onRequestSources',
|
56
|
+
}),
|
57
|
+
onRequestChildren: (0, p_memoize_1.default)(onRequestChildren, {
|
58
|
+
cache,
|
59
|
+
cacheKey: ([source, resource]) => `onRequestChildren.${source.id}.${resource?.id}`,
|
60
|
+
}),
|
61
|
+
onRequestResource: (0, p_memoize_1.default)(onRequestResource, {
|
62
|
+
cache,
|
63
|
+
cacheKey: ([reference]) => `onRequestResource.${reference.source}.${reference.resource}`,
|
64
|
+
}),
|
65
|
+
}), [onRequestSources, onRequestChildren, onRequestResource]);
|
66
|
+
return (react_1.default.createElement(exports.ResourceBrowserContext.Provider, { value: { ...memoized, ...other } }, children));
|
67
|
+
};
|
68
|
+
exports.ResourceBrowserContextProvider = ResourceBrowserContextProvider;
|
@@ -21,6 +21,6 @@ const ResourcePicker = ({ resource, allowedTypes, error, isLoading, isDisabled,
|
|
21
21
|
react_1.default.createElement("div", { className: "resource-picker-info__layout" },
|
22
22
|
isLoading && react_1.default.createElement(Loading_1.LoadingState, null),
|
23
23
|
error && react_1.default.createElement(Error_1.ErrorState, { error: error, isDisabled: isDisabled, onClear: onClear }),
|
24
|
-
resource && react_1.default.createElement(Selected_1.SelectedState, { resource: resource, isDisabled: isDisabled, onClear: onClear }))))));
|
24
|
+
resource && (react_1.default.createElement(Selected_1.SelectedState, { resource: resource, isDisabled: isDisabled, onClear: onClear, resourcePickerContainer: children })))))));
|
25
25
|
};
|
26
26
|
exports.default = ResourcePicker;
|
@@ -4,5 +4,6 @@ export type SelectedStateProps = {
|
|
4
4
|
resource: Resource;
|
5
5
|
isDisabled?: boolean;
|
6
6
|
onClear: () => void;
|
7
|
+
resourcePickerContainer: any;
|
7
8
|
};
|
8
|
-
export declare const SelectedState: ({ resource: { id, type, name, status, squizImage, url }, isDisabled, onClear, }: SelectedStateProps) => React.JSX.Element;
|
9
|
+
export declare const SelectedState: ({ resource: { id, type, name, status, squizImage, url }, isDisabled, onClear, resourcePickerContainer, }: SelectedStateProps) => React.JSX.Element;
|
@@ -8,10 +8,12 @@ const react_1 = __importDefault(require("react"));
|
|
8
8
|
const pretty_bytes_1 = __importDefault(require("pretty-bytes"));
|
9
9
|
const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
10
10
|
const StatusIndicator_1 = __importDefault(require("../../StatusIndicator/StatusIndicator"));
|
11
|
-
const
|
11
|
+
const CircledLoopIcon_1 = require("../../Icons/CircledLoopIcon");
|
12
|
+
const SelectedState = ({ resource: { id, type, name, status, squizImage, url }, isDisabled, onClear, resourcePickerContainer, }) => {
|
12
13
|
const fileSize = squizImage?.imageVariations?.original?.byteSize;
|
13
14
|
const fileWidth = squizImage?.imageVariations?.original?.width;
|
14
15
|
const fileHeight = squizImage?.imageVariations?.original?.height;
|
16
|
+
const replaceAsset = (react_1.default.createElement(generic_browser_lib_1.ModalTrigger, { showLabel: false, label: "Replace selection", containerClasses: "text-gray-500 hover:text-gray-800 focus:text-gray-800 disabled:text-gray-500 disabled:cursor-not-allowed", icon: react_1.default.createElement(CircledLoopIcon_1.CircledLoopIcon, { "aria-hidden": true, className: "m-1" }), isDisabled: isDisabled, scope: "squiz-rb-scope" }, resourcePickerContainer));
|
15
17
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
16
18
|
type.code === 'image' && url ? (react_1.default.createElement("div", { className: "checkered-bg w-[56px] h-[56px] overflow-hidden flex justify-center items-center rounded" },
|
17
19
|
react_1.default.createElement("img", { src: url, className: "w-full h-full object-cover object-center", alt: name }))) : (react_1.default.createElement(generic_browser_lib_1.Icon, { icon: type.code, resourceSource: "matrix", className: "w-4 h-4 mt-1 flex self-start" })),
|
@@ -39,6 +41,8 @@ const SelectedState = ({ resource: { id, type, name, status, squizImage, url },
|
|
39
41
|
" x ",
|
40
42
|
fileHeight,
|
41
43
|
"px"))))),
|
42
|
-
react_1.default.createElement(
|
44
|
+
react_1.default.createElement("div", { className: "flex" },
|
45
|
+
replaceAsset,
|
46
|
+
react_1.default.createElement(generic_browser_lib_1.ResetButton, { isDisabled: isDisabled, onClick: onClear }))));
|
43
47
|
};
|
44
48
|
exports.SelectedState = SelectedState;
|
@@ -1,14 +1,17 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
|
-
import {
|
3
|
+
import { Resource, HydratedResourceReference, OnRequestSources, OnRequestChildren, OnRequestResource } from '../types';
|
4
4
|
interface ResourcePickerContainerProps {
|
5
5
|
title: string;
|
6
6
|
titleAriaProps: DOMAttributes<FocusableElement>;
|
7
7
|
allowedTypes: string[] | undefined;
|
8
|
-
onRequestSources:
|
9
|
-
|
8
|
+
onRequestSources: OnRequestSources;
|
9
|
+
onRequestResource: OnRequestResource;
|
10
|
+
onRequestChildren: OnRequestChildren;
|
10
11
|
onChange(resource: HydratedResourceReference | null): void;
|
11
12
|
onClose: () => void;
|
13
|
+
preselectedSourceId?: string;
|
14
|
+
preselectedResource?: Resource | null;
|
12
15
|
}
|
13
|
-
declare function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onChange, onClose, }: ResourcePickerContainerProps): React.JSX.Element;
|
16
|
+
declare function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestResource, onRequestChildren, onChange, onClose, preselectedSourceId, preselectedResource, }: ResourcePickerContainerProps): React.JSX.Element;
|
14
17
|
export default ResourcePickerContainer;
|
@@ -28,7 +28,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
29
29
|
const react_1 = __importStar(require("react"));
|
30
30
|
const react_stately_1 = require("react-stately");
|
31
|
-
const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
32
31
|
const SourceList_1 = __importDefault(require("../SourceList/SourceList"));
|
33
32
|
const ResourceList_1 = __importDefault(require("../ResourceList/ResourceList"));
|
34
33
|
const ResourceBreadcrumb_1 = __importDefault(require("../ResourceBreadcrumb/ResourceBreadcrumb"));
|
@@ -36,19 +35,28 @@ const PreviewPanel_1 = __importDefault(require("../PreviewPanel/PreviewPanel"));
|
|
36
35
|
const SourceDropdown_1 = __importDefault(require("../SourceDropdown/SourceDropdown"));
|
37
36
|
const useResourcePath_1 = require("../Hooks/useResourcePath");
|
38
37
|
const useChildResources_1 = require("../Hooks/useChildResources");
|
39
|
-
|
38
|
+
const useSources_1 = require("../Hooks/useSources");
|
39
|
+
const usePreselectedResourcePath_1 = require("../Hooks/usePreselectedResourcePath");
|
40
|
+
function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestResource, onRequestChildren, onChange, onClose, preselectedSourceId, preselectedResource, }) {
|
40
41
|
const previewModalState = (0, react_stately_1.useOverlayTriggerState)({});
|
41
|
-
const [
|
42
|
+
const [selectedResourceId, setSelectedResourceId] = (0, react_1.useState)(null);
|
42
43
|
const [previewModalOverlayProps, setPreviewModalOverlayProps] = (0, react_1.useState)({});
|
43
44
|
const { source, currentResource, hierarchy, setSource, push, popUntil } = (0, useResourcePath_1.useResourcePath)();
|
44
|
-
const { data: sources, isLoading: isSourceLoading, reload: handleSourceReload, error: sourceError, } = (0,
|
45
|
+
const { data: sources, isLoading: isSourceLoading, reload: handleSourceReload, error: sourceError, } = (0, useSources_1.useSources)({ onRequestSources });
|
45
46
|
const { data: resources, isLoading: isResourcesLoading, reload: handleResourceReload, error: resourceError, } = (0, useChildResources_1.useChildResources)({ source, currentResource, onRequestChildren });
|
47
|
+
const { data: { source: preselectedSource, path: preselectedPath }, isLoading: isPreselectedResourcePathLoading, } = (0, usePreselectedResourcePath_1.usePreselectedResourcePath)({
|
48
|
+
sourceId: preselectedSourceId,
|
49
|
+
resource: preselectedResource,
|
50
|
+
onRequestResource,
|
51
|
+
onRequestSources,
|
52
|
+
});
|
53
|
+
const selectedResource = (0, react_1.useMemo)(() => resources.find((resource) => resource.id === selectedResourceId) || null, [selectedResourceId, resources]);
|
46
54
|
const handleResourceDrillDown = (0, react_1.useCallback)((resource) => {
|
47
55
|
push(resource);
|
48
56
|
}, [push]);
|
49
57
|
const handleResourceSelected = (0, react_1.useCallback)((resource, overlayProps) => {
|
50
58
|
setPreviewModalOverlayProps(overlayProps);
|
51
|
-
|
59
|
+
setSelectedResourceId(resource.id);
|
52
60
|
}, []);
|
53
61
|
const handleSourceDrilldown = (0, react_1.useCallback)((source) => {
|
54
62
|
setSource(source);
|
@@ -61,12 +69,28 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
61
69
|
onClose();
|
62
70
|
}, [source]);
|
63
71
|
const handleDetailClose = (0, react_1.useCallback)(() => {
|
64
|
-
|
72
|
+
setSelectedResourceId(null);
|
65
73
|
}, []);
|
66
|
-
//
|
74
|
+
// Clear the selected resource if it no longer exists in the list of resources
|
75
|
+
// (eg. due to navigating up/down the tree).
|
67
76
|
(0, react_1.useEffect)(() => {
|
68
|
-
|
69
|
-
|
77
|
+
if (resources.length > 0 && selectedResourceId && !selectedResource) {
|
78
|
+
setSelectedResourceId(null);
|
79
|
+
}
|
80
|
+
}, [resources, selectedResourceId, selectedResource]);
|
81
|
+
(0, react_1.useEffect)(() => {
|
82
|
+
if (preselectedSource && preselectedPath?.length) {
|
83
|
+
const [rootNode, ...path] = preselectedPath;
|
84
|
+
const leaf = path.pop();
|
85
|
+
setSource({
|
86
|
+
source: preselectedSource,
|
87
|
+
resource: rootNode,
|
88
|
+
}, path);
|
89
|
+
if (leaf) {
|
90
|
+
setSelectedResourceId(leaf.id);
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}, [preselectedSource, preselectedSource]);
|
70
94
|
return (react_1.default.createElement("div", { className: "relative flex flex-col h-full text-gray-800" },
|
71
95
|
react_1.default.createElement("div", { className: "flex items-center p-4.5" },
|
72
96
|
react_1.default.createElement("h2", { ...titleAriaProps, className: "text-xl leading-6 text-gray-800 font-semibold mr-6" }, title),
|
@@ -79,9 +103,9 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
|
|
79
103
|
react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
|
80
104
|
react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
|
81
105
|
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, handleReload: handleSourceReload, error: sourceError })),
|
106
|
+
!source && (react_1.default.createElement(SourceList_1.default, { sources: sources, previewModalState: previewModalState, isLoading: isSourceLoading || isPreselectedResourcePathLoading, onSourceSelect: handleSourceDrilldown, handleReload: handleSourceReload, error: sourceError })),
|
83
107
|
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
108
|
react_1.default.createElement("div", { className: "sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white" },
|
85
|
-
react_1.default.createElement(PreviewPanel_1.default, { resource: selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose })))));
|
109
|
+
react_1.default.createElement(PreviewPanel_1.default, { resource: isResourcesLoading ? null : selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose })))));
|
86
110
|
}
|
87
111
|
exports.default = ResourcePickerContainer;
|
@@ -29,7 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
const react_1 = __importStar(require("react"));
|
30
30
|
const interactions_1 = require("@react-aria/interactions");
|
31
31
|
const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
32
|
-
const uuid_1 = __importDefault(require("../uuid"));
|
32
|
+
const uuid_1 = __importDefault(require("../utils/uuid"));
|
33
33
|
const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
|
34
34
|
function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, }) {
|
35
35
|
const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
|
package/lib/index.css
CHANGED
@@ -423,6 +423,9 @@
|
|
423
423
|
.squiz-rb-scope .col-end-2 {
|
424
424
|
grid-column-end: 2;
|
425
425
|
}
|
426
|
+
.squiz-rb-scope .m-1 {
|
427
|
+
margin: 0.25rem;
|
428
|
+
}
|
426
429
|
.squiz-rb-scope .m-2 {
|
427
430
|
margin: 0.5rem;
|
428
431
|
}
|
@@ -1037,7 +1040,7 @@
|
|
1037
1040
|
}
|
1038
1041
|
.squiz-rb-scope .resource-picker-info__layout {
|
1039
1042
|
display: grid;
|
1040
|
-
grid-template-columns: auto 1fr
|
1043
|
+
grid-template-columns: auto 1fr auto;
|
1041
1044
|
gap: 0.5rem;
|
1042
1045
|
justify-items: center;
|
1043
1046
|
}
|
@@ -1052,7 +1055,21 @@
|
|
1052
1055
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
1053
1056
|
background-size: 24px 24px;
|
1054
1057
|
background-position: 0 0, 12px 12px;
|
1055
|
-
background-image:
|
1058
|
+
background-image:
|
1059
|
+
linear-gradient(
|
1060
|
+
45deg,
|
1061
|
+
#e0e0e0 25%,
|
1062
|
+
transparent 25%,
|
1063
|
+
transparent 75%,
|
1064
|
+
#e0e0e0 75%,
|
1065
|
+
#e0e0e0),
|
1066
|
+
linear-gradient(
|
1067
|
+
45deg,
|
1068
|
+
#e0e0e0 25%,
|
1069
|
+
transparent 25%,
|
1070
|
+
transparent 75%,
|
1071
|
+
#e0e0e0 75%,
|
1072
|
+
#e0e0e0);
|
1056
1073
|
}
|
1057
1074
|
.squiz-rb-scope .spinner {
|
1058
1075
|
display: inline-block;
|
package/lib/index.d.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { HydratedResourceReference, Resource, ResourceReference, Source } from './types';
|
3
|
-
import { ResourceBrowserContext, ResourceBrowserContextProps } from './ResourceBrowserContext/ResourceBrowserContext';
|
3
|
+
import { ResourceBrowserContext, ResourceBrowserContextProvider, ResourceBrowserContextProps } from './ResourceBrowserContext/ResourceBrowserContext';
|
4
4
|
export type { HydratedResourceReference, Resource, ResourceReference, Source, ResourceBrowserContextProps };
|
5
|
-
export { ResourceBrowserContext };
|
5
|
+
export { ResourceBrowserContext, ResourceBrowserContextProvider };
|
6
6
|
export type ResourceBrowserInputProps = {
|
7
7
|
modalTitle: string;
|
8
8
|
allowedTypes?: string[];
|
package/lib/index.js
CHANGED
@@ -26,11 +26,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
27
27
|
};
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
29
|
-
exports.ResourceBrowserInput = exports.ResourceBrowserContext = void 0;
|
29
|
+
exports.ResourceBrowserInput = exports.ResourceBrowserContextProvider = exports.ResourceBrowserContext = void 0;
|
30
30
|
const react_1 = __importStar(require("react"));
|
31
31
|
const ResourcePickerContainer_1 = __importDefault(require("./ResourcePickerContainer/ResourcePickerContainer"));
|
32
32
|
const ResourceBrowserContext_1 = require("./ResourceBrowserContext/ResourceBrowserContext");
|
33
33
|
Object.defineProperty(exports, "ResourceBrowserContext", { enumerable: true, get: function () { return ResourceBrowserContext_1.ResourceBrowserContext; } });
|
34
|
+
Object.defineProperty(exports, "ResourceBrowserContextProvider", { enumerable: true, get: function () { return ResourceBrowserContext_1.ResourceBrowserContextProvider; } });
|
34
35
|
const ResourcePicker_1 = __importDefault(require("./ResourcePicker/ResourcePicker"));
|
35
36
|
const useResource_1 = require("./Hooks/useResource");
|
36
37
|
const ResourceBrowserInput = ({ modalTitle, allowedTypes, onChange, value, isDisabled, onClear, }) => {
|
@@ -39,6 +40,6 @@ const ResourceBrowserInput = ({ modalTitle, allowedTypes, onChange, value, isDis
|
|
39
40
|
const defaultOnClear = () => onChange(null);
|
40
41
|
const onClearFunction = onClear ?? defaultOnClear;
|
41
42
|
return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
|
42
|
-
react_1.default.createElement(ResourcePicker_1.default, { resource: resource, allowedTypes: allowedTypes, error: error, isLoading: isLoading, isDisabled: isDisabled, onClear: () => onClearFunction() }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestChildren: onRequestChildren, onChange: onChange })))));
|
43
|
+
react_1.default.createElement(ResourcePicker_1.default, { resource: resource, allowedTypes: allowedTypes, error: error, isLoading: isLoading, isDisabled: isDisabled, onClear: () => onClearFunction() }, (onClose, titleProps) => (react_1.default.createElement(ResourcePickerContainer_1.default, { preselectedSourceId: value?.source, preselectedResource: resource, title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onClose: onClose, onRequestSources: onRequestSources, onRequestResource: onRequestResource, onRequestChildren: onRequestChildren, onChange: onChange })))));
|
43
44
|
};
|
44
45
|
exports.ResourceBrowserInput = ResourceBrowserInput;
|
package/lib/types.d.ts
CHANGED
@@ -18,6 +18,10 @@ export type Resource = {
|
|
18
18
|
urls: string[];
|
19
19
|
childCount: number;
|
20
20
|
squizImage?: SquizImageType['__shape__'];
|
21
|
+
lineages?: ResourceLineage[];
|
22
|
+
};
|
23
|
+
export type ResourceLineage = {
|
24
|
+
resourceIds: string[];
|
21
25
|
};
|
22
26
|
/**
|
23
27
|
* Represents a system that resources can be picked from.
|
@@ -65,3 +69,6 @@ export type Hierarchy<T> = Array<{
|
|
65
69
|
export type DeepPartial<T> = {
|
66
70
|
[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
71
|
};
|
72
|
+
export type OnRequestSources = () => Promise<Source[]>;
|
73
|
+
export type OnRequestResource = (reference: ResourceReference) => Promise<Resource>;
|
74
|
+
export type OnRequestChildren = (source: Source, resource: Resource | null) => Promise<Resource[]>;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.findBestMatchLineage = void 0;
|
4
|
+
const findBestMatchLineage = (source, resource) => {
|
5
|
+
if (resource.lineages) {
|
6
|
+
for (const lineage of resource.lineages) {
|
7
|
+
// Lineage must:
|
8
|
+
// 1. Appear beneath the root node.
|
9
|
+
// 2. Not be the root node itself (root nodes can't be selected, to be changed in FEAAS-760).
|
10
|
+
// TODO: FEAAS-760 update as necessary so the lineage will be returned even if it ends at the root node, eg:
|
11
|
+
// const rootNode = source.nodes.find(node => lineage.resourceIds.includes(node.id));
|
12
|
+
const rootNode = source.nodes.find((node) => {
|
13
|
+
const index = lineage.resourceIds.indexOf(node.id);
|
14
|
+
return index >= 0 && index < lineage.resourceIds.length - 1;
|
15
|
+
});
|
16
|
+
if (rootNode) {
|
17
|
+
const rootNodeIndex = lineage.resourceIds.indexOf(rootNode.id);
|
18
|
+
// Return the lineage starting from the root node. eg.
|
19
|
+
// * Full lineage is: 1 > 10 > 100 > 1000 > 10000
|
20
|
+
// * The source has a node with an ID of: 100
|
21
|
+
// * The returned lineage will be: 100 > 1000 > 10000
|
22
|
+
return lineage.resourceIds.slice(rootNodeIndex);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
return [];
|
27
|
+
};
|
28
|
+
exports.findBestMatchLineage = findBestMatchLineage;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@squiz/resource-browser",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.67.1",
|
4
4
|
"main": "lib/index.js",
|
5
5
|
"types": "lib/index.d.ts",
|
6
6
|
"private": false,
|
@@ -19,8 +19,10 @@
|
|
19
19
|
},
|
20
20
|
"dependencies": {
|
21
21
|
"@mui/icons-material": "5.11.16",
|
22
|
-
"@squiz/dx-json-schema-lib": "^1.
|
23
|
-
"@squiz/generic-browser-lib": "^1.
|
22
|
+
"@squiz/dx-json-schema-lib": "^1.67.0",
|
23
|
+
"@squiz/generic-browser-lib": "^1.66.0",
|
24
|
+
"expiry-map": "^2.0.0",
|
25
|
+
"p-memoize": "^4.0.4",
|
24
26
|
"pretty-bytes": "5.6.0",
|
25
27
|
"react-aria": "3.23.1",
|
26
28
|
"react-responsive": "9.0.2",
|
@@ -43,7 +45,7 @@
|
|
43
45
|
"@types/react-dom": "^18.2.18",
|
44
46
|
"@vitejs/plugin-react-swc": "3.0.0",
|
45
47
|
"autoprefixer": "10.4.14",
|
46
|
-
"esbuild": "0.
|
48
|
+
"esbuild": "^0.20.2",
|
47
49
|
"esbuild-sass-plugin": "2.8.0",
|
48
50
|
"jest": "29.4.1",
|
49
51
|
"jest-environment-jsdom": "29.4.1",
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { Source, Resource, OnRequestResource, OnRequestSources } from '../types';
|
2
|
+
import { useAsync } from '@squiz/generic-browser-lib';
|
3
|
+
import { findBestMatchLineage } from '../utils/findBestMatchLineage';
|
4
|
+
|
5
|
+
export type PreselectedResourceProps = {
|
6
|
+
sourceId?: string;
|
7
|
+
resource?: Resource | null;
|
8
|
+
onRequestResource: OnRequestResource;
|
9
|
+
onRequestSources: OnRequestSources;
|
10
|
+
};
|
11
|
+
|
12
|
+
export type PreselectedResourcePath = {
|
13
|
+
source?: Source;
|
14
|
+
path?: Resource[];
|
15
|
+
};
|
16
|
+
|
17
|
+
export const usePreselectedResourcePath = ({
|
18
|
+
sourceId,
|
19
|
+
resource,
|
20
|
+
onRequestResource,
|
21
|
+
onRequestSources,
|
22
|
+
}: PreselectedResourceProps) => {
|
23
|
+
return useAsync(
|
24
|
+
{
|
25
|
+
callback: async () => {
|
26
|
+
let source: Source | undefined;
|
27
|
+
let path: Resource[] | undefined;
|
28
|
+
|
29
|
+
if (sourceId) {
|
30
|
+
const sources = await onRequestSources();
|
31
|
+
source = sources.find((source) => source.id === sourceId);
|
32
|
+
}
|
33
|
+
|
34
|
+
if (sourceId && source && resource) {
|
35
|
+
const bestMatchLineage = findBestMatchLineage(source, resource);
|
36
|
+
|
37
|
+
path = await Promise.all(
|
38
|
+
bestMatchLineage.map(async (resourceId) => {
|
39
|
+
return onRequestResource({ source: sourceId, resource: resourceId });
|
40
|
+
}),
|
41
|
+
);
|
42
|
+
}
|
43
|
+
|
44
|
+
return { source, path };
|
45
|
+
},
|
46
|
+
defaultValue: {} as PreselectedResourcePath,
|
47
|
+
},
|
48
|
+
[sourceId, resource],
|
49
|
+
);
|
50
|
+
};
|
@@ -12,9 +12,9 @@ export const useResourcePath = () => {
|
|
12
12
|
const [source, setSourceInternal] = useState<ScopedSource | null>(null);
|
13
13
|
const [resourceStack, setResourceStack] = useState<Array<Resource>>([]);
|
14
14
|
|
15
|
-
const setSource = useCallback((source: ScopedSource | null) => {
|
15
|
+
const setSource = useCallback((source: ScopedSource | null, path: Resource[] = []) => {
|
16
16
|
setSourceInternal(source);
|
17
|
-
setResourceStack(
|
17
|
+
setResourceStack(path);
|
18
18
|
}, []);
|
19
19
|
|
20
20
|
const push = useCallback(
|
package/src/Hooks/useSources.ts
CHANGED
@@ -9,4 +9,4 @@ type UseSourcesProps = {
|
|
9
9
|
* Loads and caches the source list when a component using the hook is mounted.
|
10
10
|
*/
|
11
11
|
export const useSources = ({ onRequestSources }: UseSourcesProps) =>
|
12
|
-
useAsync({ callback: onRequestSources, defaultValue: [] }, []);
|
12
|
+
useAsync({ callback: onRequestSources, defaultValue: [] as Source[] }, []);
|