@squiz/resource-browser 1.69.0 → 1.69.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @squiz/resource-browser
2
2
 
3
+ ## 1.69.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 5cd6ef6: Updated logic & handling of recent locations in resource browser
8
+
3
9
  ## 1.69.0
4
10
 
5
11
  ### Minor Changes
@@ -1,10 +1,5 @@
1
- import { Resource, Source } from '../types';
2
- export interface RecentLocation {
3
- rootNode: Resource | null;
4
- source: Source;
5
- path: Array<Resource>;
6
- }
1
+ import { ResourceReference } from '../types';
7
2
  export declare const useRecentLocations: (maxLocations?: number, storageKey?: string) => {
8
- recentLocations: RecentLocation[];
9
- addRecentLocation: (newLocation: RecentLocation) => void;
3
+ recentLocations: ResourceReference[];
4
+ addRecentLocation: (newLocation: ResourceReference) => void;
10
5
  };
@@ -14,10 +14,14 @@ const useRecentLocations = (maxLocations = 3, storageKey = 'rb_recent_locations'
14
14
  if (!Array.isArray(initialRecentLocations)) {
15
15
  initialRecentLocations = [];
16
16
  }
17
+ // Check if any item in the current recent locations is not the right format, if so, we reset it
18
+ if (initialRecentLocations.find((item) => !(item?.resource?.length && item?.source?.length))) {
19
+ initialRecentLocations = [];
20
+ }
17
21
  const [recentLocations, setRecentLocations] = (0, react_1.useState)(initialRecentLocations);
18
22
  const addRecentLocation = (newLocation) => {
19
23
  // Check if the new location to make sure we don't already have a recent location for this
20
- if (JSON.stringify(recentLocations).indexOf(JSON.stringify(newLocation)) > -1) {
24
+ if (recentLocations.find((item) => item.resource === newLocation.resource && item.source === newLocation.source)) {
21
25
  return;
22
26
  }
23
27
  const updatedLocations = [newLocation, ...recentLocations.slice(0, maxLocations - 1)];
@@ -0,0 +1,20 @@
1
+ import { Resource, OnRequestResource, OnRequestSources, Source } from '../types';
2
+ export type RecentResourcesPathsProps = {
3
+ sourceIds?: string[];
4
+ resources?: Resource | null | (Resource | null)[];
5
+ onRequestResource: OnRequestResource;
6
+ onRequestSources: OnRequestSources;
7
+ };
8
+ export type RecentResourcesPaths = {
9
+ source?: Source;
10
+ path?: Resource[];
11
+ };
12
+ export declare const useRecentResourcesPaths: ({ sourceIds, resources, onRequestResource, onRequestSources, }: RecentResourcesPathsProps) => {
13
+ data: RecentResourcesPaths[] | {
14
+ source: Source | undefined;
15
+ path: Resource[] | undefined;
16
+ } | null;
17
+ error: Error | null;
18
+ isLoading: boolean;
19
+ reload: () => void;
20
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useRecentResourcesPaths = void 0;
4
+ const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
5
+ const findBestMatchLineage_1 = require("../utils/findBestMatchLineage");
6
+ const useRecentResourcesPaths = ({ sourceIds, resources, onRequestResource, onRequestSources, }) => {
7
+ const callbackArray = sourceIds?.map((sourceId, index) => async () => {
8
+ let path;
9
+ const sources = await onRequestSources();
10
+ const source = sources.find((source) => source.id === sourceId);
11
+ const resource = Array.isArray(resources) ? resources[index] : null;
12
+ if (sourceId && source && resource) {
13
+ const bestMatchLineage = (0, findBestMatchLineage_1.findBestMatchLineage)(source, resource);
14
+ if (Array.isArray(bestMatchLineage) && bestMatchLineage.length > 0) {
15
+ path = await Promise.all(bestMatchLineage.map(async (resourceId) => {
16
+ return onRequestResource({ source: sourceId, resource: resourceId });
17
+ }));
18
+ }
19
+ else {
20
+ path = [resource];
21
+ }
22
+ }
23
+ return { source, path };
24
+ });
25
+ return (0, generic_browser_lib_1.useAsync)({
26
+ callback: callbackArray ? callbackArray : () => null,
27
+ defaultValue: [],
28
+ }, [JSON.stringify(sourceIds), resources]);
29
+ };
30
+ exports.useRecentResourcesPaths = useRecentResourcesPaths;
@@ -3,6 +3,10 @@ type UseResourceProps = {
3
3
  onRequestResource: (reference: ResourceReference) => Promise<Resource | null>;
4
4
  reference?: ResourceReference | null;
5
5
  };
6
+ type UseResourcesProps = {
7
+ onRequestResource: (reference: ResourceReference) => Promise<Resource | null>;
8
+ references?: ResourceReference[] | null;
9
+ };
6
10
  /**
7
11
  * Loads the resource indicated by the provided reference.
8
12
  */
@@ -12,4 +16,13 @@ export declare const useResource: ({ onRequestResource, reference }: UseResource
12
16
  isLoading: boolean;
13
17
  reload: () => void;
14
18
  };
19
+ /**
20
+ * Loads the resources indicated by the provided reference.
21
+ */
22
+ export declare const useResources: ({ onRequestResource, references }: UseResourcesProps) => {
23
+ data: Resource | null;
24
+ error: Error | null;
25
+ isLoading: boolean;
26
+ reload: () => void;
27
+ };
15
28
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useResource = void 0;
3
+ exports.useResources = exports.useResource = void 0;
4
4
  const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
5
5
  /**
6
6
  * Loads the resource indicated by the provided reference.
@@ -10,3 +10,14 @@ const useResource = ({ onRequestResource, reference }) => (0, generic_browser_li
10
10
  defaultValue: null,
11
11
  }, [reference?.source, reference?.resource]);
12
12
  exports.useResource = useResource;
13
+ /**
14
+ * Loads the resources indicated by the provided reference.
15
+ */
16
+ const useResources = ({ onRequestResource, references }) => {
17
+ const callbackArray = references?.map((item) => () => onRequestResource(item));
18
+ return (0, generic_browser_lib_1.useAsync)({
19
+ callback: callbackArray ? callbackArray : () => null,
20
+ defaultValue: null,
21
+ }, []);
22
+ };
23
+ exports.useResources = useResources;
@@ -38,13 +38,30 @@ const useChildResources_1 = require("../Hooks/useChildResources");
38
38
  const useSources_1 = require("../Hooks/useSources");
39
39
  const usePreselectedResourcePath_1 = require("../Hooks/usePreselectedResourcePath");
40
40
  const useRecentLocations_1 = require("../Hooks/useRecentLocations");
41
+ const useResource_1 = require("../Hooks/useResource");
42
+ const useRecentResourcesPaths_1 = require("../Hooks/useRecentResourcesPaths");
41
43
  function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onRequestSources, onRequestResource, onRequestChildren, onChange, onClose, preselectedSourceId, preselectedResource, }) {
42
44
  const previewModalState = (0, react_stately_1.useOverlayTriggerState)({});
43
45
  const [selectedSource, setSelectedSource] = (0, react_1.useState)(null);
44
46
  const [selectedResourceId, setSelectedResourceId] = (0, react_1.useState)(null);
45
47
  const [previewModalOverlayProps, setPreviewModalOverlayProps] = (0, react_1.useState)({});
46
48
  const { source, currentResource, hierarchy, setSource, push, popUntil } = (0, useResourcePath_1.useResourcePath)();
47
- const { addRecentLocation } = (0, useRecentLocations_1.useRecentLocations)();
49
+ // Recent locations relevant data
50
+ const { addRecentLocation, recentLocations } = (0, useRecentLocations_1.useRecentLocations)();
51
+ const { data: recentLocationsResources, isLoading: recentLocationsResourcesLoading } = (0, useResource_1.useResources)({
52
+ onRequestResource,
53
+ references: recentLocations,
54
+ });
55
+ const { data: recentLocationsSources, isLoading: recentLocationsLoading } = (0, useRecentResourcesPaths_1.useRecentResourcesPaths)({
56
+ sourceIds: recentLocations.map((item) => item.source),
57
+ resources: recentLocationsResources,
58
+ onRequestResource,
59
+ onRequestSources,
60
+ });
61
+ // Type check the returned values from recent locations requests
62
+ let recentSources = [];
63
+ if (Array.isArray(recentLocationsSources))
64
+ recentSources = recentLocationsSources;
48
65
  const { data: sources, isLoading: isSourceLoading, reload: handleSourceReload, error: sourceError, } = (0, useSources_1.useSources)({ onRequestSources });
49
66
  const { data: resources, isLoading: isResourcesLoading, reload: handleResourceReload, error: resourceError, } = (0, useChildResources_1.useChildResources)({ source, currentResource, onRequestChildren });
50
67
  const { data: { source: preselectedSource, path: preselectedPath }, isLoading: isPreselectedResourcePathLoading, } = (0, usePreselectedResourcePath_1.usePreselectedResourcePath)({
@@ -84,19 +101,15 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
84
101
  setSource(null);
85
102
  }, [setSource]);
86
103
  const handleDetailSelect = (0, react_1.useCallback)((resource) => {
87
- onChange({ resource, source: selectedSource ?? source?.source });
104
+ const detailSelectedSource = selectedSource ?? source?.source;
105
+ onChange({ resource, source: detailSelectedSource });
88
106
  // Find the path that got them to where they are
89
- const selectedPath = hierarchy.map((path) => {
90
- const pathNode = path.node;
91
- return 'resource' in pathNode ? pathNode.resource : pathNode;
92
- });
93
- const [rootNode, ...path] = selectedPath;
94
- if (rootNode) {
95
- // Update the recent locations in local storage
107
+ const lastPathItem = hierarchy[hierarchy.length - 1]?.node;
108
+ const lastPathResource = lastPathItem && 'resource' in lastPathItem ? lastPathItem?.resource : lastPathItem;
109
+ if (lastPathResource) {
96
110
  addRecentLocation({
97
- rootNode,
98
- path: path,
99
- source: selectedSource ?? source?.source,
111
+ resource: lastPathResource.id,
112
+ source: detailSelectedSource.id,
100
113
  });
101
114
  }
102
115
  onClose();
@@ -134,7 +147,7 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
134
147
  react_1.default.createElement("div", { className: "flex items-center p-4.5" },
135
148
  react_1.default.createElement("h2", { ...titleAriaProps, className: "text-xl leading-6 text-gray-800 font-semibold mr-6" }, title),
136
149
  react_1.default.createElement("div", { className: "px-3 border-l border-gray-300 w-300px" },
137
- react_1.default.createElement(SourceDropdown_1.default, { sources: sources, selectedSource: source, isLoading: isSourceLoading, onSourceSelect: handleSourceDrilldown, onRootSelect: handleReturnToRoot, setSource: setSource, currentResource: currentResource })),
150
+ react_1.default.createElement(SourceDropdown_1.default, { sources: sources, selectedSource: source, isLoading: isSourceLoading || recentLocationsLoading || recentLocationsResourcesLoading, onSourceSelect: handleSourceDrilldown, onRootSelect: handleReturnToRoot, setSource: setSource, currentResource: currentResource, recentSources: recentSources })),
138
151
  react_1.default.createElement("button", { type: "button", "aria-label": `Close ${title} dialog`, onClick: onClose, className: "absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100" },
139
152
  react_1.default.createElement("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg" },
140
153
  react_1.default.createElement("path", { d: "M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z", fill: "currentColor" })))),
@@ -142,7 +155,10 @@ function ResourcePickerContainer({ title, titleAriaProps, allowedTypes, onReques
142
155
  react_1.default.createElement("div", { className: "overflow-y-scroll flex-1 grow-[3] border-r border-gray-300" },
143
156
  react_1.default.createElement("h3", { className: "sr-only" }, "Resource List"),
144
157
  hierarchy.length > 0 && (react_1.default.createElement(ResourceBreadcrumb_1.default, { hierarchy: hierarchy, onBreadcrumbSelect: popUntil, onReturnToRoot: handleReturnToRoot })),
145
- !source && (react_1.default.createElement(SourceList_1.default, { sources: sources, selectedResource: selectedResource, previewModalState: previewModalState, isLoading: isSourceLoading || isPreselectedResourcePathLoading, onSourceSelect: handleSourceSelected, onSourceDrilldown: handleSourceDrilldown, handleReload: handleSourceReload, setSource: setSource, error: sourceError })),
158
+ !source && (react_1.default.createElement(SourceList_1.default, { sources: sources, selectedResource: selectedResource, previewModalState: previewModalState, isLoading: isSourceLoading ||
159
+ isPreselectedResourcePathLoading ||
160
+ recentLocationsLoading ||
161
+ recentLocationsResourcesLoading, onSourceSelect: handleSourceSelected, onSourceDrilldown: handleSourceDrilldown, handleReload: handleSourceReload, setSource: setSource, recentSources: recentSources, error: sourceError })),
146
162
  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 }))),
147
163
  react_1.default.createElement("div", { className: "sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white" },
148
164
  react_1.default.createElement(PreviewPanel_1.default, { resource: isResourcesLoading ? null : selectedResource, modalState: previewModalState, previewModalOverlayProps: previewModalOverlayProps, allowedTypes: allowedTypes, onSelect: handleDetailSelect, onClose: handleDetailClose })))));
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import type { Source, ScopedSource, Resource } from '../types';
3
- export default function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, setSource, currentResource, }: {
3
+ import { RecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
4
+ export default function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, setSource, currentResource, recentSources, }: {
4
5
  sources: Source[];
5
6
  selectedSource: ScopedSource | null;
6
7
  isLoading: boolean;
@@ -8,4 +9,5 @@ export default function SourceDropdown({ sources, selectedSource, isLoading, onR
8
9
  onSourceSelect: (source: ScopedSource) => void;
9
10
  setSource: (source: ScopedSource | null, path?: Resource[]) => void;
10
11
  currentResource: Resource | null;
12
+ recentSources: RecentResourcesPaths[];
11
13
  }): React.JSX.Element;
@@ -31,11 +31,10 @@ const interactions_1 = require("@react-aria/interactions");
31
31
  const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
32
32
  const uuid_1 = __importDefault(require("../utils/uuid"));
33
33
  const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
34
- const useRecentLocations_1 = require("../Hooks/useRecentLocations");
35
34
  const HistoryIcon_1 = require("../Icons/HistoryIcon");
36
- function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, setSource, currentResource, }) {
35
+ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSourceSelect, setSource, currentResource, recentSources, }) {
37
36
  const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
38
- const { recentLocations } = (0, useRecentLocations_1.useRecentLocations)();
37
+ const filteredRecentSources = recentSources.filter((item) => item.path?.length);
39
38
  const [recentLocationSelection, setRecentLocationSelection] = (0, react_1.useState)();
40
39
  const [uniqueId] = (0, react_1.useState)((0, uuid_1.default)());
41
40
  const buttonRef = (0, react_1.useRef)(null);
@@ -71,17 +70,22 @@ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSo
71
70
  setIsOpen(false);
72
71
  setRecentLocationSelection(location);
73
72
  buttonRef.current?.focus();
74
- setSource({
75
- source: location.source,
76
- resource: location.rootNode,
77
- }, location.path);
73
+ if (location.path) {
74
+ const [rootNode, ...path] = location.path;
75
+ setSource({
76
+ source: location.source,
77
+ resource: rootNode,
78
+ }, path);
79
+ }
78
80
  };
79
81
  (0, react_1.useEffect)(() => {
80
- const lastResource = recentLocationSelection?.path[recentLocationSelection.path.length - 1];
81
- // If the current resource selected in the resource browser is no longer the item selected in the
82
- // recent locations section dropdown then we set the selection to null to prevent active statuses.
83
- if (currentResource?.id !== lastResource?.id) {
84
- setRecentLocationSelection(null);
82
+ if (recentLocationSelection?.path) {
83
+ const lastResource = recentLocationSelection.path[recentLocationSelection.path.length - 1];
84
+ // If the current resource selected in the resource browser is no longer the item selected in the
85
+ // recent locations section dropdown then we set the selection to null to prevent active statuses.
86
+ if (currentResource && currentResource.id !== lastResource?.id) {
87
+ setRecentLocationSelection(null);
88
+ }
85
89
  }
86
90
  }, [recentLocationSelection, currentResource]);
87
91
  return (react_1.default.createElement("div", { ...focusWithinProps, ...keyboardProps, className: "relative w-72 border-2 rounded border-gray-300" },
@@ -112,26 +116,24 @@ function SourceDropdown({ sources, selectedSource, isLoading, onRootSelect, onSo
112
116
  "All available sources")),
113
117
  isLoading && (react_1.default.createElement("li", { className: "mt-2" },
114
118
  react_1.default.createElement(generic_browser_lib_1.Spinner, { size: "sm", label: "Loading sources", className: "m-3" }))),
115
- !isLoading && recentLocations.length > 0 && (react_1.default.createElement("li", { className: `flex flex-col text-sm font-semibold text-grey-800` },
119
+ !isLoading && filteredRecentSources.length > 0 && (react_1.default.createElement("li", { className: `flex flex-col text-sm font-semibold text-grey-800` },
116
120
  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" },
117
121
  react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5 flex gap-1 items-center" },
118
122
  react_1.default.createElement(HistoryIcon_1.HistoryIcon, null),
119
123
  "Recent locations")),
120
- react_1.default.createElement("ul", { "aria-label": "recent location nodes", className: "flex flex-col mt-2" }, recentLocations.map((item, index) => {
121
- const lastResource = item.path[item.path.length - 1];
124
+ react_1.default.createElement("ul", { "aria-label": "recent location nodes", className: "flex flex-col mt-2" }, filteredRecentSources.map((item, index) => {
125
+ const lastResource = item.path && item.path[item.path.length - 1];
122
126
  const isSelectedSource = item.source?.id === selectedSource?.source.id &&
123
- item.rootNode?.id === selectedSource?.resource?.id &&
124
- lastResource?.id === currentResource?.id &&
125
- recentLocationSelection;
126
- return (react_1.default.createElement("li", { key: `${index}-${item.source?.id}-${item.rootNode?.id}`, className: "flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b" },
127
+ lastResource?.id === recentLocationSelection?.path?.[recentLocationSelection.path?.length - 1]?.id;
128
+ return (react_1.default.createElement("li", { key: `${index}-${item.source?.id}-${lastResource?.id}`, className: "flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b" },
127
129
  react_1.default.createElement("button", { type: "button", onClick: () => handleRecentLocationClick(item), className: `relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100` },
128
- react_1.default.createElement(generic_browser_lib_1.Icon, { icon: (lastResource?.type.code || item.rootNode?.type.code || 'folder'), resourceSource: "matrix", "aria-label": lastResource?.name || item.rootNode?.name || item.source?.name, className: "shrink-0 mr-2.5" }),
129
- react_1.default.createElement("span", { className: "text-left mr-7" }, lastResource?.name || item.rootNode?.name || item.source?.name),
130
+ react_1.default.createElement(generic_browser_lib_1.Icon, { icon: (lastResource?.type.code || 'folder'), resourceSource: "matrix", "aria-label": lastResource?.name || item.source?.name, className: "shrink-0 mr-2.5" }),
131
+ react_1.default.createElement("span", { className: "text-left mr-7" }, lastResource?.name || item.source?.name),
130
132
  isSelectedSource && (react_1.default.createElement(generic_browser_lib_1.Icon, { icon: 'selected', "aria-label": "selected", className: "absolute right-4" })))));
131
133
  })))),
132
134
  !isLoading &&
133
135
  categorisedSources.map(({ key, label, sources }, index) => {
134
- return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 || recentLocations.length > 0 ? 'mt-3' : ''}` },
136
+ return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 || filteredRecentSources.length > 0 ? 'mt-3' : ''}` },
135
137
  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" },
136
138
  react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
137
139
  sources?.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col mt-2" }, sources.map(({ source, resource }) => {
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { OverlayTriggerState } from 'react-stately';
3
3
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
4
4
  import { Source, ScopedSource, Resource } from '../types';
5
+ import { RecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
5
6
  export interface SourceListProps {
6
7
  sources: Source[];
7
8
  selectedResource?: Resource | null;
@@ -11,7 +12,8 @@ export interface SourceListProps {
11
12
  onSourceDrilldown: (source: ScopedSource) => void;
12
13
  handleReload: () => void;
13
14
  setSource: (source: ScopedSource | null, path?: Resource[]) => void;
15
+ recentSources: RecentResourcesPaths[];
14
16
  error: Error | null;
15
17
  }
16
- declare const SourceList: ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, error, }: SourceListProps) => React.JSX.Element;
18
+ declare const SourceList: ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, recentSources, error, }: SourceListProps) => React.JSX.Element;
17
19
  export default SourceList;
@@ -30,12 +30,11 @@ const react_1 = __importStar(require("react"));
30
30
  const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
31
31
  const clsx_1 = __importDefault(require("clsx"));
32
32
  const useCategorisedSources_1 = require("../Hooks/useCategorisedSources");
33
- const useRecentLocations_1 = require("../Hooks/useRecentLocations");
34
33
  const HistoryIcon_1 = require("../Icons/HistoryIcon");
35
- const SourceList = function ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, error, }) {
34
+ const SourceList = function ({ sources, selectedResource, previewModalState, isLoading, onSourceSelect, onSourceDrilldown, handleReload, setSource, recentSources, error, }) {
36
35
  const categorisedSources = (0, useCategorisedSources_1.useCategorisedSources)(sources);
37
36
  const listRef = (0, react_1.useRef)(null);
38
- const { recentLocations } = (0, useRecentLocations_1.useRecentLocations)();
37
+ const filteredRecentSources = recentSources.filter((item) => item.path?.length);
39
38
  (0, react_1.useEffect)(() => {
40
39
  if (listRef.current) {
41
40
  listRef.current?.focus({
@@ -50,23 +49,26 @@ const SourceList = function ({ sources, selectedResource, previewModalState, isL
50
49
  }
51
50
  return (react_1.default.createElement("ul", { ref: listRef, tabIndex: -1, "aria-label": `Source list`, className: (0, clsx_1.default)('flex flex-col bg-gray-100 min-h-full focus-visible:outline-0 px-7 py-4') },
52
51
  error && react_1.default.createElement(generic_browser_lib_1.ResourceState, { state: "error", message: error.message, handleReload: handleReload }),
53
- !error && recentLocations.length > 0 && (react_1.default.createElement("li", { className: `flex flex-col text-sm font-semibold text-grey-800` },
52
+ !error && filteredRecentSources.length > 0 && (react_1.default.createElement("li", { className: `flex flex-col text-sm font-semibold text-grey-800` },
54
53
  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" },
55
54
  react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5 flex gap-1 items-center" },
56
55
  react_1.default.createElement(HistoryIcon_1.HistoryIcon, null),
57
56
  "Recent locations")),
58
- react_1.default.createElement("ul", { "aria-label": `recent location nodes`, className: "flex flex-col" }, recentLocations.map((item, index) => {
59
- const lastResource = item.path[item.path.length - 1];
60
- return (react_1.default.createElement(generic_browser_lib_1.ResourceItem, { key: `${index}-${item.source?.id}-${item.rootNode?.id}`, item: { source: item.source, resource: item.rootNode }, label: lastResource?.name || item.rootNode?.name || item?.source.name, type: lastResource?.type?.code || item.rootNode?.type?.code || 'folder', previewModalState: previewModalState, onSelect: () => {
61
- setSource({
62
- source: item.source,
63
- resource: item.rootNode,
64
- }, item.path);
65
- }, className: (0, clsx_1.default)(index === 0 && 'rounded-t-lg mt-3', index === recentLocations.length - 1 && 'rounded-b-lg'), showChevron: true }));
57
+ react_1.default.createElement("ul", { "aria-label": `recent location nodes`, className: "flex flex-col" }, filteredRecentSources.map((item, index) => {
58
+ if (item.path) {
59
+ const lastResource = item.path[item.path.length - 1];
60
+ const [rootNode, ...path] = item.path;
61
+ return (react_1.default.createElement(generic_browser_lib_1.ResourceItem, { key: `${index}-${item.source?.id}-${lastResource?.id}`, item: { source: item.source, resource: lastResource }, label: lastResource?.name || item.source?.name || '', type: lastResource?.type?.code || 'folder', previewModalState: previewModalState, onSelect: () => {
62
+ setSource({
63
+ source: item.source,
64
+ resource: rootNode,
65
+ }, path);
66
+ }, className: (0, clsx_1.default)(index === 0 && 'rounded-t-lg mt-3', index === filteredRecentSources.length - 1 && 'rounded-b-lg'), showChevron: true }));
67
+ }
66
68
  })))),
67
69
  !error &&
68
70
  categorisedSources.map(({ key, label, sources }, index) => {
69
- return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 || recentLocations.length > 0 ? 'mt-3' : ''}` },
71
+ return (react_1.default.createElement("li", { key: key, className: `flex flex-col text-sm font-semibold text-grey-800 ${index > 0 || filteredRecentSources.length > 0 ? 'mt-3' : ''}` },
70
72
  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" },
71
73
  react_1.default.createElement("span", { className: "z-10 bg-gray-100 px-2.5" }, label)),
72
74
  sources.length > 0 && (react_1.default.createElement("ul", { "aria-label": `${label} nodes`, className: "flex flex-col" }, sources.map(({ source, resource }) => {
package/lib/index.css CHANGED
@@ -983,6 +983,9 @@
983
983
  --tw-blur: blur(8px);
984
984
  filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
985
985
  }
986
+ .squiz-rb-scope .filter {
987
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
988
+ }
986
989
  .squiz-rb-scope .resource-breadcrumb--collapsed .resource-breadcrumb__label {
987
990
  max-width: 250px;
988
991
  cursor: pointer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "1.69.0",
3
+ "version": "1.69.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -1,35 +1,8 @@
1
1
  import { act, renderHook } from '@testing-library/react';
2
2
  import { useRecentLocations } from './useRecentLocations';
3
3
 
4
- import { mockResource, mockSource } from '../__mocks__/MockModels';
5
-
6
4
  describe('useRecentLocations', () => {
7
- const mockLocalStorageData = [
8
- {
9
- path: [],
10
- source: {
11
- id: '1',
12
- name: 'Test source',
13
- nodes: [],
14
- },
15
- rootNode: {
16
- childCount: 0,
17
- id: '1',
18
- lineages: [],
19
- name: 'Test resource',
20
- status: {
21
- code: 'live',
22
- name: 'Live',
23
- },
24
- type: {
25
- code: 'folder',
26
- name: 'Folder',
27
- },
28
- url: 'https://no-where.com',
29
- urls: [],
30
- },
31
- },
32
- ];
5
+ const mockLocalStorageData = [{ resource: '20', source: '1' }];
33
6
 
34
7
  beforeEach(() => {
35
8
  localStorage.clear();
@@ -45,13 +18,17 @@ describe('useRecentLocations', () => {
45
18
 
46
19
  act(() => {
47
20
  result.current.addRecentLocation({
48
- path: [],
49
- source: mockSource(),
50
- rootNode: mockResource(),
21
+ source: '1',
22
+ resource: '32',
51
23
  });
52
24
  });
53
25
 
54
- expect(result.current.recentLocations).toEqual(mockLocalStorageData);
26
+ expect(result.current.recentLocations).toEqual([
27
+ {
28
+ source: '1',
29
+ resource: '32',
30
+ },
31
+ ]);
55
32
  });
56
33
 
57
34
  it('should not add duplicate recent locations', () => {
@@ -59,20 +36,31 @@ describe('useRecentLocations', () => {
59
36
 
60
37
  act(() => {
61
38
  result.current.addRecentLocation({
62
- path: [],
63
- source: mockSource(),
64
- rootNode: mockResource(),
39
+ source: '1',
40
+ resource: '55',
65
41
  });
42
+ });
43
+
44
+ expect(result.current.recentLocations).toEqual([
45
+ {
46
+ source: '1',
47
+ resource: '55',
48
+ },
49
+ ]);
66
50
 
67
- // Add duplicate
51
+ act(() => {
68
52
  result.current.addRecentLocation({
69
- path: [],
70
- source: mockSource(),
71
- rootNode: mockResource(),
53
+ source: '1',
54
+ resource: '55',
72
55
  });
73
56
  });
74
57
 
75
- expect(result.current.recentLocations).toEqual(mockLocalStorageData);
58
+ expect(result.current.recentLocations).toEqual([
59
+ {
60
+ source: '1',
61
+ resource: '55',
62
+ },
63
+ ]);
76
64
  });
77
65
 
78
66
  it('should load recent locations from local storage on mount', () => {
@@ -82,4 +70,12 @@ describe('useRecentLocations', () => {
82
70
 
83
71
  expect(result.current.recentLocations).toEqual(mockLocalStorageData);
84
72
  });
73
+
74
+ it('should handle local storage recent locations not being in the correct format', () => {
75
+ localStorage.setItem('rb_recent_locations', JSON.stringify({}));
76
+
77
+ const { result } = renderHook(() => useRecentLocations());
78
+
79
+ expect(result.current.recentLocations).toEqual([]);
80
+ });
85
81
  });
@@ -1,14 +1,8 @@
1
1
  import { useState } from 'react';
2
- import { Resource, Source } from '../types';
3
-
4
- export interface RecentLocation {
5
- rootNode: Resource | null;
6
- source: Source;
7
- path: Array<Resource>;
8
- }
2
+ import { ResourceReference } from '../types';
9
3
 
10
4
  export const useRecentLocations = (maxLocations = 3, storageKey = 'rb_recent_locations') => {
11
- let initialRecentLocations = [];
5
+ let initialRecentLocations: Array<ResourceReference> = [];
12
6
 
13
7
  try {
14
8
  initialRecentLocations = JSON.parse(localStorage.getItem(storageKey) ?? '[]');
@@ -21,11 +15,16 @@ export const useRecentLocations = (maxLocations = 3, storageKey = 'rb_recent_loc
21
15
  initialRecentLocations = [];
22
16
  }
23
17
 
24
- const [recentLocations, setRecentLocations] = useState<Array<RecentLocation>>(initialRecentLocations);
18
+ // Check if any item in the current recent locations is not the right format, if so, we reset it
19
+ if (initialRecentLocations.find((item) => !(item?.resource?.length && item?.source?.length))) {
20
+ initialRecentLocations = [];
21
+ }
22
+
23
+ const [recentLocations, setRecentLocations] = useState<Array<ResourceReference>>(initialRecentLocations);
25
24
 
26
- const addRecentLocation = (newLocation: RecentLocation) => {
25
+ const addRecentLocation = (newLocation: ResourceReference) => {
27
26
  // Check if the new location to make sure we don't already have a recent location for this
28
- if (JSON.stringify(recentLocations).indexOf(JSON.stringify(newLocation)) > -1) {
27
+ if (recentLocations.find((item) => item.resource === newLocation.resource && item.source === newLocation.source)) {
29
28
  return;
30
29
  }
31
30
 
@@ -0,0 +1,54 @@
1
+ import { Resource, OnRequestResource, OnRequestSources, Source } from '../types';
2
+ import { useAsync } from '@squiz/generic-browser-lib';
3
+ import { findBestMatchLineage } from '../utils/findBestMatchLineage';
4
+
5
+ export type RecentResourcesPathsProps = {
6
+ sourceIds?: string[];
7
+ resources?: Resource | null | (Resource | null)[];
8
+ onRequestResource: OnRequestResource;
9
+ onRequestSources: OnRequestSources;
10
+ };
11
+
12
+ export type RecentResourcesPaths = {
13
+ source?: Source;
14
+ path?: Resource[];
15
+ };
16
+
17
+ export const useRecentResourcesPaths = ({
18
+ sourceIds,
19
+ resources,
20
+ onRequestResource,
21
+ onRequestSources,
22
+ }: RecentResourcesPathsProps) => {
23
+ const callbackArray = sourceIds?.map((sourceId, index) => async () => {
24
+ let path: Resource[] | undefined;
25
+
26
+ const sources = await onRequestSources();
27
+ const source = sources.find((source) => source.id === sourceId);
28
+ const resource = Array.isArray(resources) ? resources[index] : null;
29
+
30
+ if (sourceId && source && resource) {
31
+ const bestMatchLineage = findBestMatchLineage(source, resource);
32
+
33
+ if (Array.isArray(bestMatchLineage) && bestMatchLineage.length > 0) {
34
+ path = await Promise.all(
35
+ bestMatchLineage.map(async (resourceId) => {
36
+ return onRequestResource({ source: sourceId, resource: resourceId });
37
+ }),
38
+ );
39
+ } else {
40
+ path = [resource];
41
+ }
42
+ }
43
+
44
+ return { source, path };
45
+ });
46
+
47
+ return useAsync(
48
+ {
49
+ callback: callbackArray ? callbackArray : () => null,
50
+ defaultValue: [] as RecentResourcesPaths[],
51
+ },
52
+ [JSON.stringify(sourceIds), resources],
53
+ );
54
+ };