@squiz/resource-browser 1.66.3 → 1.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/lib/Hooks/usePreselectedResourcePath.d.ts +20 -0
- package/lib/Hooks/usePreselectedResourcePath.js +26 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Hooks/useResourcePath.js +2 -2
- package/lib/Hooks/useSources.d.ts +14 -0
- package/lib/Hooks/useSources.js +9 -0
- package/lib/Icons/CircledLoopIcon.d.ts +4 -0
- package/lib/Icons/CircledLoopIcon.js +12 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +12 -5
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +52 -2
- package/lib/ResourcePicker/ResourcePicker.js +1 -1
- package/lib/ResourcePicker/States/Selected.d.ts +2 -1
- package/lib/ResourcePicker/States/Selected.js +6 -2
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +7 -4
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +35 -11
- package/lib/SourceDropdown/SourceDropdown.js +1 -1
- package/lib/index.css +19 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -2
- package/lib/types.d.ts +7 -0
- package/lib/utils/findBestMatchLineage.d.ts +2 -0
- package/lib/utils/findBestMatchLineage.js +28 -0
- package/package.json +6 -4
- package/src/Hooks/usePreselectedResourcePath.ts +50 -0
- package/src/Hooks/useResourcePath.ts +2 -2
- package/src/Hooks/useSources.ts +1 -1
- package/src/Icons/CircledLoopIcon.tsx +14 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +93 -3
- package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +56 -0
- package/src/ResourceList/sample-resources.json +684 -439
- package/src/ResourcePicker/ResourcePicker.tsx +8 -1
- package/src/ResourcePicker/States/Selected.tsx +23 -3
- package/src/ResourcePicker/resource-picker.scss +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +146 -32
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +64 -18
- package/src/SourceDropdown/SourceDropdown.tsx +1 -1
- package/src/SourceList/sample-sources.json +4 -4
- package/src/__mocks__/MockModels.ts +1 -0
- package/src/__mocks__/StorybookHelpers.ts +33 -4
- package/src/__mocks__/renderWithContext.tsx +23 -0
- package/src/index.spec.tsx +81 -21
- package/src/index.stories.tsx +4 -4
- package/src/index.tsx +10 -2
- package/src/types.ts +9 -0
- package/src/utils/findBestMatchLineage.spec.ts +81 -0
- package/src/utils/findBestMatchLineage.ts +30 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +0 -20
- /package/lib/{uuid.d.ts → utils/uuid.d.ts} +0 -0
- /package/lib/{uuid.js → utils/uuid.js} +0 -0
- /package/src/{uuid.ts → utils/uuid.ts} +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
import React, { SVGAttributes } from 'react';
|
2
|
+
|
3
|
+
type CircledLoopIconProps = SVGAttributes<SVGElement>;
|
4
|
+
|
5
|
+
export const CircledLoopIcon = (props: CircledLoopIconProps) => {
|
6
|
+
return (
|
7
|
+
<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" {...props}>
|
8
|
+
<path
|
9
|
+
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"
|
10
|
+
fill="#949494"
|
11
|
+
/>
|
12
|
+
</svg>
|
13
|
+
);
|
14
|
+
};
|
@@ -1,11 +1,15 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { render } from '@testing-library/react';
|
3
|
-
import {
|
4
|
-
|
3
|
+
import {
|
4
|
+
ResourceBrowserContext,
|
5
|
+
ResourceBrowserContextProps,
|
6
|
+
ResourceBrowserContextProvider,
|
7
|
+
} from './ResourceBrowserContext';
|
8
|
+
import { mockResource, mockSource } from '../__mocks__/MockModels';
|
5
9
|
|
6
10
|
describe('ResourceBrowserContext', () => {
|
7
11
|
it('Should render with expected default value', () => {
|
8
|
-
let defaultContext: ResourceBrowserContextProps |
|
12
|
+
let defaultContext: ResourceBrowserContextProps | undefined;
|
9
13
|
|
10
14
|
render(
|
11
15
|
<ResourceBrowserContext.Consumer>
|
@@ -29,4 +33,90 @@ describe('ResourceBrowserContext', () => {
|
|
29
33
|
);
|
30
34
|
expect(() => defaultContext?.onRequestSources()).toThrow('onRequestSources has not been configured.');
|
31
35
|
});
|
36
|
+
|
37
|
+
it('Should add memoization to data fetching functions', async () => {
|
38
|
+
let context: ResourceBrowserContextProps | undefined;
|
39
|
+
const sources = [mockSource({ id: '10' }), mockSource({ id: '20' })];
|
40
|
+
const resources = [mockResource({ id: '100' }), mockResource({ id: '200' })];
|
41
|
+
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
42
|
+
const onRequestResource = jest.fn().mockResolvedValue(resources[0]);
|
43
|
+
const onRequestChildren = jest.fn().mockResolvedValue(resources);
|
44
|
+
|
45
|
+
render(
|
46
|
+
<ResourceBrowserContextProvider
|
47
|
+
value={{
|
48
|
+
onRequestSources,
|
49
|
+
onRequestResource,
|
50
|
+
onRequestChildren,
|
51
|
+
}}
|
52
|
+
>
|
53
|
+
<ResourceBrowserContext.Consumer>
|
54
|
+
{(value) => {
|
55
|
+
context = value;
|
56
|
+
return null;
|
57
|
+
}}
|
58
|
+
</ResourceBrowserContext.Consumer>
|
59
|
+
</ResourceBrowserContextProvider>,
|
60
|
+
);
|
61
|
+
|
62
|
+
const result = await Promise.all([
|
63
|
+
context?.onRequestSources?.(),
|
64
|
+
context?.onRequestSources?.(),
|
65
|
+
context?.onRequestResource?.({ resource: '100', source: '10' }),
|
66
|
+
context?.onRequestResource?.({ resource: '100', source: '10' }),
|
67
|
+
context?.onRequestResource?.({ resource: '200', source: '20' }),
|
68
|
+
context?.onRequestChildren?.(sources[0], null),
|
69
|
+
context?.onRequestChildren?.(sources[0], null),
|
70
|
+
context?.onRequestChildren?.(sources[1], null),
|
71
|
+
]);
|
72
|
+
|
73
|
+
// mocked data should be returned for all invocations of memoized function
|
74
|
+
expect(result).toEqual([
|
75
|
+
sources,
|
76
|
+
sources,
|
77
|
+
resources[0],
|
78
|
+
resources[0],
|
79
|
+
resources[0],
|
80
|
+
resources,
|
81
|
+
resources,
|
82
|
+
resources,
|
83
|
+
]);
|
84
|
+
|
85
|
+
// memoized function should only be called when different arguments are provided
|
86
|
+
expect(onRequestSources).toBeCalledTimes(1);
|
87
|
+
expect(onRequestResource).toBeCalledTimes(2);
|
88
|
+
expect(onRequestChildren).toBeCalledTimes(2);
|
89
|
+
});
|
90
|
+
|
91
|
+
it('Should not cache failures from data fetching functions', async () => {
|
92
|
+
let context: ResourceBrowserContextProps | undefined;
|
93
|
+
const sources = [mockSource({ id: '10' })];
|
94
|
+
const onRequestSources = jest
|
95
|
+
.fn()
|
96
|
+
.mockRejectedValueOnce(new Error('Cannot fetch sources.'))
|
97
|
+
.mockResolvedValueOnce(sources);
|
98
|
+
const onRequestResource = jest.fn();
|
99
|
+
const onRequestChildren = jest.fn();
|
100
|
+
|
101
|
+
render(
|
102
|
+
<ResourceBrowserContextProvider
|
103
|
+
value={{
|
104
|
+
onRequestSources,
|
105
|
+
onRequestResource,
|
106
|
+
onRequestChildren,
|
107
|
+
}}
|
108
|
+
>
|
109
|
+
<ResourceBrowserContext.Consumer>
|
110
|
+
{(value) => {
|
111
|
+
context = value;
|
112
|
+
return null;
|
113
|
+
}}
|
114
|
+
</ResourceBrowserContext.Consumer>
|
115
|
+
</ResourceBrowserContextProvider>,
|
116
|
+
);
|
117
|
+
|
118
|
+
await expect(context?.onRequestSources?.()).rejects.toThrow(new Error('Cannot fetch sources.'));
|
119
|
+
await expect(context?.onRequestSources?.()).resolves.toEqual(sources);
|
120
|
+
expect(onRequestSources).toBeCalledTimes(2);
|
121
|
+
});
|
32
122
|
});
|
@@ -0,0 +1,56 @@
|
|
1
|
+
import React, { PropsWithChildren, useMemo } from 'react';
|
2
|
+
import { OnRequestResource, OnRequestSources, OnRequestChildren } from '../types';
|
3
|
+
import pMemoize from 'p-memoize';
|
4
|
+
import ExpiryMap from 'expiry-map';
|
5
|
+
|
6
|
+
export type ResourceBrowserContextProps = {
|
7
|
+
onRequestSources: OnRequestSources;
|
8
|
+
onRequestChildren: OnRequestChildren;
|
9
|
+
onRequestResource: OnRequestResource;
|
10
|
+
};
|
11
|
+
|
12
|
+
/**
|
13
|
+
* @internal Direct usage of this object is discouraged. It will be privated in a future major version.
|
14
|
+
* Please use ResourceBrowserContextProvider instead.
|
15
|
+
*/
|
16
|
+
export const ResourceBrowserContext = React.createContext<ResourceBrowserContextProps>({
|
17
|
+
onRequestSources: () => {
|
18
|
+
throw new Error('onRequestSources has not been configured.');
|
19
|
+
},
|
20
|
+
onRequestChildren: () => {
|
21
|
+
throw new Error('onRequestChildren has not been configured.');
|
22
|
+
},
|
23
|
+
onRequestResource: () => {
|
24
|
+
throw new Error('onRequestResource has not been configured.');
|
25
|
+
},
|
26
|
+
});
|
27
|
+
|
28
|
+
export const ResourceBrowserContextProvider = (props: PropsWithChildren<{ value: ResourceBrowserContextProps }>) => {
|
29
|
+
const CACHE_DURATION = 30000; // 30 seconds
|
30
|
+
const {
|
31
|
+
value: { onRequestSources, onRequestChildren, onRequestResource, ...other },
|
32
|
+
children,
|
33
|
+
} = props;
|
34
|
+
const cache = new ExpiryMap(CACHE_DURATION);
|
35
|
+
const memoized = useMemo(
|
36
|
+
() => ({
|
37
|
+
onRequestSources: pMemoize(onRequestSources, {
|
38
|
+
cache,
|
39
|
+
cacheKey: () => 'onRequestSources',
|
40
|
+
}),
|
41
|
+
onRequestChildren: pMemoize(onRequestChildren, {
|
42
|
+
cache,
|
43
|
+
cacheKey: ([source, resource]) => `onRequestChildren.${source.id}.${resource?.id}`,
|
44
|
+
}),
|
45
|
+
onRequestResource: pMemoize(onRequestResource, {
|
46
|
+
cache,
|
47
|
+
cacheKey: ([reference]) => `onRequestResource.${reference.source}.${reference.resource}`,
|
48
|
+
}),
|
49
|
+
}),
|
50
|
+
[onRequestSources, onRequestChildren, onRequestResource],
|
51
|
+
);
|
52
|
+
|
53
|
+
return (
|
54
|
+
<ResourceBrowserContext.Provider value={{ ...memoized, ...other }}>{children}</ResourceBrowserContext.Provider>
|
55
|
+
);
|
56
|
+
};
|