@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/Hooks/usePreselectedResourcePath.d.ts +20 -0
  3. package/lib/Hooks/usePreselectedResourcePath.js +26 -0
  4. package/lib/Hooks/useResourcePath.d.ts +1 -1
  5. package/lib/Hooks/useResourcePath.js +2 -2
  6. package/lib/Hooks/useSources.d.ts +14 -0
  7. package/lib/Hooks/useSources.js +9 -0
  8. package/lib/Icons/CircledLoopIcon.d.ts +4 -0
  9. package/lib/Icons/CircledLoopIcon.js +12 -0
  10. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +12 -5
  11. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +52 -2
  12. package/lib/ResourcePicker/ResourcePicker.js +1 -1
  13. package/lib/ResourcePicker/States/Selected.d.ts +2 -1
  14. package/lib/ResourcePicker/States/Selected.js +6 -2
  15. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +7 -4
  16. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +35 -11
  17. package/lib/SourceDropdown/SourceDropdown.js +1 -1
  18. package/lib/index.css +19 -2
  19. package/lib/index.d.ts +2 -2
  20. package/lib/index.js +3 -2
  21. package/lib/types.d.ts +7 -0
  22. package/lib/utils/findBestMatchLineage.d.ts +2 -0
  23. package/lib/utils/findBestMatchLineage.js +28 -0
  24. package/package.json +6 -4
  25. package/src/Hooks/usePreselectedResourcePath.ts +50 -0
  26. package/src/Hooks/useResourcePath.ts +2 -2
  27. package/src/Hooks/useSources.ts +1 -1
  28. package/src/Icons/CircledLoopIcon.tsx +14 -0
  29. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +93 -3
  30. package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +56 -0
  31. package/src/ResourceList/sample-resources.json +684 -439
  32. package/src/ResourcePicker/ResourcePicker.tsx +8 -1
  33. package/src/ResourcePicker/States/Selected.tsx +23 -3
  34. package/src/ResourcePicker/resource-picker.scss +1 -1
  35. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +146 -32
  36. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +64 -18
  37. package/src/SourceDropdown/SourceDropdown.tsx +1 -1
  38. package/src/SourceList/sample-sources.json +4 -4
  39. package/src/__mocks__/MockModels.ts +1 -0
  40. package/src/__mocks__/StorybookHelpers.ts +33 -4
  41. package/src/__mocks__/renderWithContext.tsx +23 -0
  42. package/src/index.spec.tsx +81 -21
  43. package/src/index.stories.tsx +4 -4
  44. package/src/index.tsx +10 -2
  45. package/src/types.ts +9 -0
  46. package/src/utils/findBestMatchLineage.spec.ts +81 -0
  47. package/src/utils/findBestMatchLineage.ts +30 -0
  48. package/src/ResourceBrowserContext/ResourceBrowserContext.ts +0 -20
  49. /package/lib/{uuid.d.ts → utils/uuid.d.ts} +0 -0
  50. /package/lib/{uuid.js → utils/uuid.js} +0 -0
  51. /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,4 @@
1
+ import React, { SVGAttributes } from 'react';
2
+ type CircledLoopIconProps = SVGAttributes<SVGElement>;
3
+ export declare const CircledLoopIcon: (props: CircledLoopIconProps) => React.JSX.Element;
4
+ export {};
@@ -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 { Resource, ResourceReference, Source } from '../types';
1
+ import React, { PropsWithChildren } from 'react';
2
+ import { OnRequestResource, OnRequestSources, OnRequestChildren } from '../types';
3
3
  export type ResourceBrowserContextProps = {
4
- onRequestSources: () => Promise<Source[]>;
5
- onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
6
- onRequestResource(reference: ResourceReference): Promise<Resource | null>;
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 = __importDefault(require("react"));
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 SelectedState = ({ resource: { id, type, name, status, squizImage, url }, isDisabled, onClear, }) => {
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(generic_browser_lib_1.ResetButton, { isDisabled: isDisabled, onClick: onClear })));
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 { Source, Resource, HydratedResourceReference } from '../types';
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: () => Promise<Source[]>;
9
- onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
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
- function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestChildren, onChange, onClose, }) {
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 [selectedResource, setSelectedResource] = (0, react_1.useState)(null);
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, generic_browser_lib_1.useAsync)({ callback: onRequestSources, defaultValue: [] }, []);
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
- setSelectedResource(resource);
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
- setSelectedResource(null);
72
+ setSelectedResourceId(null);
65
73
  }, []);
66
- // When the active node changes clear the selected resource
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
- setSelectedResource(null);
69
- }, [hierarchy]);
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 24px;
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: linear-gradient(45deg, #e0e0e0 25%, transparent 25%, transparent 75%, #e0e0e0 75%, #e0e0e0), linear-gradient(45deg, #e0e0e0 25%, transparent 25%, transparent 75%, #e0e0e0 75%, #e0e0e0);
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,2 @@
1
+ import { Source, Resource } from '../types';
2
+ export declare const findBestMatchLineage: (source: Source, resource: Resource) => string[];
@@ -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.66.3",
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.65.0",
23
- "@squiz/generic-browser-lib": "^1.65.0",
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.17.17",
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(
@@ -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[] }, []);