@squiz/resource-browser 1.69.0 → 1.69.2
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 +12 -0
- package/lib/Hooks/useRecentLocations.d.ts +3 -8
- package/lib/Hooks/useRecentLocations.js +5 -1
- package/lib/Hooks/useRecentResourcesPaths.d.ts +20 -0
- package/lib/Hooks/useRecentResourcesPaths.js +30 -0
- package/lib/Hooks/useResource.d.ts +13 -0
- package/lib/Hooks/useResource.js +14 -1
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +30 -14
- package/lib/SourceDropdown/SourceDropdown.d.ts +3 -1
- package/lib/SourceDropdown/SourceDropdown.js +24 -22
- package/lib/SourceList/SourceList.d.ts +3 -1
- package/lib/SourceList/SourceList.js +15 -13
- package/lib/index.css +3 -0
- package/package.json +1 -1
- package/src/Hooks/useRecentLocations.spec.ts +36 -40
- package/src/Hooks/useRecentLocations.ts +10 -11
- package/src/Hooks/useRecentResourcesPaths.ts +54 -0
- package/src/Hooks/useResource.spec.ts +30 -1
- package/src/Hooks/useResource.ts +21 -0
- package/src/ResourcePicker/ResourcePicker.spec.tsx +18 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +17 -2
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +40 -15
- package/src/SourceDropdown/SourceDropdown.spec.tsx +92 -27
- package/src/SourceDropdown/SourceDropdown.tsx +33 -29
- package/src/SourceList/SourceList.spec.tsx +89 -72
- package/src/SourceList/SourceList.tsx +34 -29
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# @squiz/resource-browser
|
2
2
|
|
3
|
+
## 1.69.2
|
4
|
+
|
5
|
+
### Patch Changes
|
6
|
+
|
7
|
+
- d167ab9: Fixed bug with resource browser inputs
|
8
|
+
|
9
|
+
## 1.69.1
|
10
|
+
|
11
|
+
### Patch Changes
|
12
|
+
|
13
|
+
- 5cd6ef6: Updated logic & handling of recent locations in resource browser
|
14
|
+
|
3
15
|
## 1.69.0
|
4
16
|
|
5
17
|
### Minor Changes
|
@@ -1,10 +1,5 @@
|
|
1
|
-
import {
|
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:
|
9
|
-
addRecentLocation: (newLocation:
|
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 (
|
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 {};
|
package/lib/Hooks/useResource.js
CHANGED
@@ -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.
|
@@ -8,5 +8,18 @@ const generic_browser_lib_1 = require("@squiz/generic-browser-lib");
|
|
8
8
|
const useResource = ({ onRequestResource, reference }) => (0, generic_browser_lib_1.useAsync)({
|
9
9
|
callback: () => (reference ? onRequestResource(reference) : null),
|
10
10
|
defaultValue: null,
|
11
|
+
// Avoid the race condition bug found in FEAAS-891
|
12
|
+
ignorePrevious: true,
|
11
13
|
}, [reference?.source, reference?.resource]);
|
12
14
|
exports.useResource = useResource;
|
15
|
+
/**
|
16
|
+
* Loads the resources indicated by the provided reference.
|
17
|
+
*/
|
18
|
+
const useResources = ({ onRequestResource, references }) => {
|
19
|
+
const callbackArray = references?.map((item) => () => onRequestResource(item));
|
20
|
+
return (0, generic_browser_lib_1.useAsync)({
|
21
|
+
callback: callbackArray ? callbackArray : () => null,
|
22
|
+
defaultValue: null,
|
23
|
+
}, []);
|
24
|
+
};
|
25
|
+
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
|
-
|
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
|
-
|
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
|
90
|
-
|
91
|
-
|
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
|
-
|
98
|
-
|
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 ||
|
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
|
-
|
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
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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 &&
|
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" },
|
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
|
-
|
124
|
-
|
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 ||
|
129
|
-
react_1.default.createElement("span", { className: "text-left mr-7" }, lastResource?.name || item.
|
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 ||
|
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
|
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 &&
|
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" },
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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 ||
|
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,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
|
-
|
49
|
-
|
50
|
-
rootNode: mockResource(),
|
21
|
+
source: '1',
|
22
|
+
resource: '32',
|
51
23
|
});
|
52
24
|
});
|
53
25
|
|
54
|
-
expect(result.current.recentLocations).toEqual(
|
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
|
-
|
63
|
-
|
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
|
-
|
51
|
+
act(() => {
|
68
52
|
result.current.addRecentLocation({
|
69
|
-
|
70
|
-
|
71
|
-
rootNode: mockResource(),
|
53
|
+
source: '1',
|
54
|
+
resource: '55',
|
72
55
|
});
|
73
56
|
});
|
74
57
|
|
75
|
-
expect(result.current.recentLocations).toEqual(
|
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 {
|
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
|
-
|
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:
|
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 (
|
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
|
+
};
|