@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
@@ -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 { ResourceBrowserContext, ResourceBrowserContextProps } from './ResourceBrowserContext';
4
- import { mockSource } from '../__mocks__/MockModels';
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 | null = null;
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
+ };