@squiz/resource-browser 1.32.1-alpha.33 → 1.32.1-alpha.34

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 (62) hide show
  1. package/lib/Hooks/useAsync.d.ts +21 -0
  2. package/lib/Hooks/useAsync.js +53 -0
  3. package/lib/Hooks/useChildResources.d.ts +3 -7
  4. package/lib/Hooks/useChildResources.js +5 -29
  5. package/lib/Hooks/useResource.d.ts +15 -0
  6. package/lib/Hooks/useResource.js +12 -0
  7. package/lib/Hooks/useResourcePath.d.ts +1 -1
  8. package/lib/PreviewPanel/details/MatrixResource.js +2 -1
  9. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +8 -0
  10. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +18 -0
  11. package/lib/ResourceItem/ResourceItem.js +1 -1
  12. package/lib/ResourcePicker/ResetButton.d.ts +5 -0
  13. package/lib/ResourcePicker/ResetButton.js +11 -0
  14. package/lib/ResourcePicker/ResourcePicker.d.ts +14 -0
  15. package/lib/ResourcePicker/ResourcePicker.js +26 -0
  16. package/lib/ResourcePicker/States/Error.d.ts +6 -0
  17. package/lib/ResourcePicker/States/Error.js +14 -0
  18. package/lib/ResourcePicker/States/Loading.d.ts +1 -0
  19. package/lib/ResourcePicker/States/Loading.js +11 -0
  20. package/lib/ResourcePicker/States/Selected.d.ts +7 -0
  21. package/lib/ResourcePicker/States/Selected.js +43 -0
  22. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +3 -3
  23. package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
  24. package/lib/SourceDropdown/SourceDropdown.js +3 -3
  25. package/lib/StatusIndicator/StatusIndicator.d.ts +2 -1
  26. package/lib/StatusIndicator/StatusIndicator.js +3 -2
  27. package/lib/index.css +6 -0
  28. package/lib/index.d.ts +8 -7
  29. package/lib/index.js +35 -13
  30. package/lib/types.d.ts +67 -0
  31. package/lib/types.js +2 -0
  32. package/package.json +3 -3
  33. package/src/Hooks/useAsync.spec.ts +106 -0
  34. package/src/Hooks/useAsync.ts +62 -0
  35. package/src/Hooks/useChildResources.spec.ts +2 -23
  36. package/src/Hooks/useChildResources.ts +9 -34
  37. package/src/Hooks/useResource.spec.ts +32 -0
  38. package/src/Hooks/useResource.ts +19 -0
  39. package/src/Hooks/useSources.spec.ts +2 -14
  40. package/src/Hooks/useSources.ts +3 -26
  41. package/src/PreviewPanel/details/MatrixResource.tsx +1 -2
  42. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +32 -0
  43. package/src/ResourceBrowserContext/ResourceBrowserContext.ts +20 -0
  44. package/src/ResourceItem/ResourceItem.tsx +1 -1
  45. package/src/ResourcePicker/ResetButton.tsx +7 -1
  46. package/src/ResourcePicker/ResourcePicker.spec.tsx +8 -4
  47. package/src/ResourcePicker/ResourcePicker.stories.tsx +2 -2
  48. package/src/ResourcePicker/ResourcePicker.tsx +21 -12
  49. package/src/ResourcePicker/States/Error.tsx +9 -3
  50. package/src/ResourcePicker/States/Selected.tsx +9 -4
  51. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +5 -5
  52. package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
  53. package/src/SourceDropdown/SourceDropdown.tsx +3 -3
  54. package/src/StatusIndicator/StatusIndicator.tsx +5 -2
  55. package/src/__mocks__/StorybookHelpers.ts +18 -13
  56. package/src/index.spec.tsx +4 -4
  57. package/src/index.stories.tsx +15 -15
  58. package/src/index.tsx +39 -54
  59. package/src/{types.d.ts → types.ts} +1 -1
  60. package/tailwind.config.cjs +5 -0
  61. package/lib/Hooks/useSources.d.ts +0 -16
  62. package/lib/Hooks/useSources.js +0 -31
package/lib/types.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { SquizImageType } from '@squiz/dx-json-schema-lib';
2
+ export type Status = {
3
+ code: string;
4
+ name: string;
5
+ };
6
+ /**
7
+ * Represents a resource that has been picked from a source.
8
+ */
9
+ export type Resource = {
10
+ id: string;
11
+ name: string;
12
+ type: {
13
+ code: string;
14
+ name: string;
15
+ };
16
+ status: Status;
17
+ url: string;
18
+ urls: string[];
19
+ childCount: number;
20
+ squizImage?: SquizImageType['__shape__'];
21
+ };
22
+ /**
23
+ * Represents a system that resources can be picked from.
24
+ * Optionally, may indicate a list of "nodes" that can be used to scope the source to a subset of its resources.
25
+ */
26
+ export type Source = {
27
+ id: string;
28
+ name: string;
29
+ nodes: Resource[];
30
+ };
31
+ /**
32
+ * Represents a source that has been optionally scoped to one of its "nodes".
33
+ */
34
+ export type ScopedSource = {
35
+ source: Source;
36
+ resource: Resource | null;
37
+ };
38
+ /**
39
+ * A non-hydrated resource reference.
40
+ */
41
+ export type ResourceReference = {
42
+ source: string;
43
+ resource: string;
44
+ };
45
+ /**
46
+ * A hydrated resource reference.
47
+ */
48
+ export type HydratedResourceReference = {
49
+ source: Source;
50
+ resource: Resource;
51
+ };
52
+ /**
53
+ * Represents the hierarchy within the asset picker.
54
+ * Within the picker T will be ScopedSource|Resource.
55
+ * ScopedSource will be the first item in the array, Resource will be every other item.
56
+ */
57
+ export type Hierarchy<T> = Array<{
58
+ key: string;
59
+ label: string;
60
+ node: T;
61
+ }>;
62
+ /**
63
+ * Augments a type so that all properties are optional.
64
+ */
65
+ export type DeepPartial<T> = {
66
+ [P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : T[P] extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : DeepPartial<T[P]>;
67
+ };
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "1.32.1-alpha.33",
3
+ "version": "1.32.1-alpha.34",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@mui/icons-material": "5.11.16",
18
- "@squiz/dx-json-schema-lib": "1.32.1-alpha.33",
18
+ "@squiz/dx-json-schema-lib": "1.32.1-alpha.34",
19
19
  "pretty-bytes": "5.6.0",
20
20
  "react-aria": "3.23.1",
21
21
  "react-responsive": "9.0.2",
@@ -73,5 +73,5 @@
73
73
  "volta": {
74
74
  "node": "18.15.0"
75
75
  },
76
- "gitHead": "614c4ec6f82220758521a2887b45094c36474aef"
76
+ "gitHead": "34965675fd331238d984f5c4d86cc96d85b5f165"
77
77
  }
@@ -0,0 +1,106 @@
1
+ import { DependencyList } from 'react';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import { useAsync } from './useAsync';
4
+
5
+ describe('useAsync', () => {
6
+ const renderAsyncHook = (callback: () => any, deps: DependencyList) => {
7
+ return renderHook(
8
+ ({ deps }: { deps: DependencyList }) => useAsync({ callback, defaultValue: 'Initial state' }, deps),
9
+ { initialProps: { deps } },
10
+ );
11
+ };
12
+
13
+ it('Should invoke callback when hook is initially rendered', async () => {
14
+ const callback = jest.fn().mockResolvedValue('Resolved data');
15
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
16
+
17
+ expect(result.current.isLoading).toBe(true);
18
+ expect(result.current.error).toBe(null);
19
+ expect(result.current.data).toBe('Initial state');
20
+
21
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
22
+
23
+ expect(result.current.isLoading).toBe(false);
24
+ expect(result.current.error).toBe(null);
25
+ expect(result.current.data).toBe('Resolved data');
26
+ });
27
+
28
+ it('Should not invoke callback when hook is re-rendered with the same dependencies', async () => {
29
+ const callback = jest.fn().mockResolvedValue('Resolved data');
30
+ const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']);
31
+
32
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
33
+
34
+ // If we re-render with the same "deps" we should not attempt to re-load data.
35
+ rerender({ deps: ['initial_dependency_value'] });
36
+
37
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
38
+
39
+ expect(result.current.isLoading).toBe(false);
40
+ expect(result.current.error).toBe(null);
41
+ expect(result.current.data).toBe('Resolved data');
42
+ expect(callback).toBeCalledTimes(1);
43
+ });
44
+
45
+ it('Should invoke callback when hook is re-rendered with different dependencies', async () => {
46
+ const callback = jest.fn().mockResolvedValueOnce('Resolved data').mockResolvedValueOnce('Updated resolved data');
47
+ const { result, rerender } = renderAsyncHook(callback, ['initial_dependency_value']);
48
+
49
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
50
+
51
+ // If we re-render with the same "deps" we should not attempt to re-load data.
52
+ rerender({ deps: ['updated_dependency_value'] });
53
+
54
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
55
+
56
+ expect(result.current.isLoading).toBe(false);
57
+ expect(result.current.error).toBe(null);
58
+ expect(result.current.data).toBe('Updated resolved data');
59
+ expect(callback).toBeCalledTimes(2);
60
+ });
61
+
62
+ it.each([
63
+ ['Error object thrown', new Error('Callback failed'), 'Callback failed'],
64
+ ['Non-error object thrown', 'Callback failed', 'Callback failed'],
65
+ ])(
66
+ 'Should retain the error if the callback rejects - %s',
67
+ async (description: string, error: any, expectedErrorMessage: string) => {
68
+ const callback = jest.fn().mockRejectedValue(error);
69
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
70
+
71
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
72
+
73
+ expect(result.current.isLoading).toBe(false);
74
+ expect(result.current.error).toBeInstanceOf(Error);
75
+ expect(result.current.error?.message).toBe(expectedErrorMessage);
76
+ expect(result.current.data).toBe('Initial state');
77
+ },
78
+ );
79
+
80
+ it('Should handle non-async return data returned from callback', async () => {
81
+ const callback = jest.fn(() => 'Returned data');
82
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
83
+
84
+ expect(result.current.isLoading).toBe(false);
85
+ expect(result.current.error).toBe(null);
86
+ expect(result.current.data).toBe('Returned data');
87
+ });
88
+
89
+ it.each([
90
+ ['Error object thrown', new Error('Callback failed'), 'Callback failed'],
91
+ ['Non-error object thrown', 'Callback failed', 'Callback failed'],
92
+ ])(
93
+ 'Should handle non-async thrown errors from callback - %s',
94
+ async (description: string, error: any, expectedErrorMessage: string) => {
95
+ const callback = jest.fn(() => {
96
+ throw error;
97
+ });
98
+ const { result } = renderAsyncHook(callback, ['initial_dependency_value']);
99
+
100
+ expect(result.current.isLoading).toBe(false);
101
+ expect(result.current.error).toBeInstanceOf(Error);
102
+ expect(result.current.error?.message).toBe(expectedErrorMessage);
103
+ expect(result.current.data).toBe('Initial state');
104
+ },
105
+ );
106
+ });
@@ -0,0 +1,62 @@
1
+ import { DependencyList, useState, useCallback, useEffect } from 'react';
2
+
3
+ export type UseAsyncProps<TReturnType, TDefaultValueType> = {
4
+ /** The async callback to call for fetching data. */
5
+ callback: () => TReturnType | Promise<TReturnType>;
6
+ /** The default value to populate the data as when initially mounted or reloading data. */
7
+ defaultValue: TReturnType | TDefaultValueType;
8
+ };
9
+
10
+ /**
11
+ * Hook for invoking a piece of async code and keeping track of its state.
12
+ *
13
+ * Data is loaded in 3 different ways:
14
+ * 1. On initial mount.
15
+ * 2. When any of the `deps` change.
16
+ * 3. When the `relaod` function is called.
17
+ */
18
+ export const useAsync = <TReturnType, TDefaultValueType>(
19
+ { callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>,
20
+ deps: DependencyList,
21
+ ) => {
22
+ const [data, setData] = useState(defaultValue);
23
+ const [isLoading, setIsLoading] = useState(false);
24
+ const [error, setError] = useState<Error | null>(null);
25
+ const reload = useCallback(() => {
26
+ setIsLoading(true);
27
+ setError(null);
28
+ setData(defaultValue);
29
+
30
+ try {
31
+ const result = callback();
32
+
33
+ if (result instanceof Promise) {
34
+ // if the callback returned a promise wait for it to either resolve or reject.
35
+ result
36
+ .then((resolved: TReturnType) => {
37
+ setData(resolved);
38
+ setIsLoading(false);
39
+ })
40
+ .catch((e: unknown) => {
41
+ setError(e instanceof Error ? e : new Error(String(e)));
42
+ setIsLoading(false);
43
+ });
44
+ } else {
45
+ // if the callback returned something other than a promise assume it is the data we want.
46
+ setData(result);
47
+ setIsLoading(false);
48
+ }
49
+ } catch (e: unknown) {
50
+ // callback threw outside of the scope of the promise.
51
+ setError(e instanceof Error ? e : new Error(String(e)));
52
+ setIsLoading(false);
53
+ }
54
+ }, deps);
55
+
56
+ // reload data on dependency change (and initial mount)
57
+ useEffect(() => {
58
+ reload();
59
+ }, deps);
60
+
61
+ return { data, error, isLoading, reload };
62
+ };
@@ -19,32 +19,11 @@ describe('useChildResources', () => {
19
19
 
20
20
  expect(result.current.isLoading).toBe(true);
21
21
  expect(result.current.error).toBe(null);
22
- expect(result.current.resources).toEqual([]);
22
+ expect(result.current.data).toEqual([]);
23
23
 
24
24
  await waitFor(() => expect(result.current.isLoading).toBe(false));
25
25
 
26
26
  expect(result.current.isLoading).toBe(false);
27
- expect(result.current.resources).toBe(children);
28
- });
29
-
30
- it('Should return the error if loading resources fails', async () => {
31
- const source = mockScopedSource();
32
- const currentResource = mockResource({ name: 'Current resource' });
33
- const error = new Error('Loading the resources failed.');
34
- const onRequestChildren = jest.fn().mockRejectedValue(error);
35
-
36
- const { result } = renderHook(() =>
37
- useChildResources({
38
- source,
39
- currentResource,
40
- onRequestChildren,
41
- }),
42
- );
43
-
44
- await waitFor(() => expect(result.current.isLoading).toBe(false));
45
-
46
- expect(result.current.isLoading).toBe(false);
47
- expect(result.current.error).toBe(error);
48
- expect(result.current.resources).toEqual([]);
27
+ expect(result.current.data).toBe(children);
49
28
  });
50
29
  });
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
1
  import { Resource, ScopedSource, Source } from '../types';
2
+ import { useAsync } from './useAsync';
3
3
 
4
4
  type UseChildResourcesProps = {
5
5
  source: ScopedSource | null;
@@ -9,37 +9,12 @@ type UseChildResourcesProps = {
9
9
 
10
10
  /**
11
11
  * Triggers a reload of the child resources when the source or current resource change.
12
- *
13
- * @param {ScopedSource|null} source
14
- * @param {Resource|null} currentResource
15
- * @param {Function} onRequestChildren
16
12
  */
17
- export const useChildResources = ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) => {
18
- const [error, setError] = useState<Error | null>(null);
19
- const [isLoading, setIsLoading] = useState(false);
20
- const [resources, setResources] = useState<Resource[]>([]);
21
-
22
- const loadResource = useCallback(() => {
23
- setError(null);
24
- setResources([]);
25
-
26
- if (source) {
27
- setIsLoading(true);
28
-
29
- onRequestChildren(source.source, currentResource)
30
- .then((resources: Array<Resource>) => {
31
- setResources(resources);
32
- setIsLoading(false);
33
- })
34
- .catch((e) => {
35
- setError(e);
36
- setIsLoading(false);
37
- });
38
- }
39
- }, [source, currentResource]);
40
-
41
- // trigger a reload of the resources when the source or the current resource changes.
42
- useEffect(loadResource, [source, currentResource]);
43
-
44
- return { isLoading, resources, reloadResources: loadResource, error };
45
- };
13
+ export const useChildResources = ({ source, currentResource, onRequestChildren }: UseChildResourcesProps) =>
14
+ useAsync(
15
+ {
16
+ callback: () => (source ? onRequestChildren(source.source, currentResource || source.resource) : []),
17
+ defaultValue: [],
18
+ },
19
+ [source, currentResource],
20
+ );
@@ -0,0 +1,32 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { mockResource } from '../__mocks__/MockModels';
3
+ import { useResource } from './useResource';
4
+
5
+ describe('useResource', () => {
6
+ it('Should load the resource', async () => {
7
+ const resource = mockResource();
8
+ const reference = { source: 'source-id', resource: 'resource-id' };
9
+ const onRequestResource = jest.fn().mockResolvedValue(resource);
10
+ const { result } = renderHook(() => useResource({ onRequestResource, reference }));
11
+
12
+ expect(result.current.isLoading).toBe(true);
13
+ expect(result.current.error).toBe(null);
14
+ expect(result.current.data).toEqual(null);
15
+
16
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
17
+
18
+ expect(result.current.isLoading).toBe(false);
19
+ expect(result.current.data).toBe(resource);
20
+ });
21
+
22
+ it('Should not load the resource if no reference provided', async () => {
23
+ const reference = null;
24
+ const onRequestResource = jest.fn();
25
+ const { result } = renderHook(() => useResource({ onRequestResource, reference }));
26
+
27
+ expect(result.current.isLoading).toBe(false);
28
+ expect(result.current.error).toBe(null);
29
+ expect(result.current.data).toEqual(null);
30
+ expect(onRequestResource).not.toBeCalled();
31
+ });
32
+ });
@@ -0,0 +1,19 @@
1
+ import { useAsync } from './useAsync';
2
+ import { Resource, ResourceReference } from '../types';
3
+
4
+ type UseResourceProps = {
5
+ onRequestResource: (reference: ResourceReference) => Promise<Resource | null>;
6
+ reference?: ResourceReference | null;
7
+ };
8
+
9
+ /**
10
+ * Loads the resource indicated by the provided reference.
11
+ */
12
+ export const useResource = ({ onRequestResource, reference }: UseResourceProps) =>
13
+ useAsync(
14
+ {
15
+ callback: () => (reference ? onRequestResource(reference) : null),
16
+ defaultValue: null,
17
+ },
18
+ [reference?.source, reference?.resource],
19
+ );
@@ -10,23 +10,11 @@ describe('useSources', () => {
10
10
 
11
11
  expect(result.current.isLoading).toBe(true);
12
12
  expect(result.current.error).toBe(null);
13
- expect(result.current.sources).toEqual([]);
13
+ expect(result.current.data).toEqual([]);
14
14
 
15
15
  await waitFor(() => expect(result.current.isLoading).toBe(false));
16
16
 
17
17
  expect(result.current.isLoading).toBe(false);
18
- expect(result.current.sources).toBe(sources);
19
- });
20
-
21
- it('Should return the error if loading resources fails', async () => {
22
- const error = new Error('Loading the sources failed.');
23
- const onRequestSources = jest.fn().mockRejectedValue(error);
24
- const { result } = renderHook(() => useSources({ onRequestSources }));
25
-
26
- await waitFor(() => expect(result.current.isLoading).toBe(false));
27
-
28
- expect(result.current.isLoading).toBe(false);
29
- expect(result.current.error).toBe(error);
30
- expect(result.current.sources).toEqual([]);
18
+ expect(result.current.data).toBe(sources);
31
19
  });
32
20
  });
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
1
  import { Source } from '../types';
2
+ import { useAsync } from './useAsync';
3
3
 
4
4
  type UseSourcesProps = {
5
5
  onRequestSources: () => Promise<Source[]>;
@@ -7,29 +7,6 @@ type UseSourcesProps = {
7
7
 
8
8
  /**
9
9
  * Loads and caches the source list when a component using the hook is mounted.
10
- *
11
- * @param {Function} onRequestSources
12
10
  */
13
- export const useSources = ({ onRequestSources }: UseSourcesProps) => {
14
- const [error, setError] = useState<Error | null>(null);
15
- const [isLoading, setIsLoading] = useState(true);
16
- const [sources, setSources] = useState<Source[]>([]);
17
- const loadSources = useCallback(() => {
18
- setIsLoading(true);
19
- onRequestSources()
20
- .then((sources) => {
21
- setIsLoading(false);
22
- setSources(sources);
23
- setError(null);
24
- })
25
- .catch((error) => {
26
- setIsLoading(false);
27
- setError(error);
28
- });
29
- }, []);
30
-
31
- // trigger a load of the sources when the component using the hook is initially rendered.
32
- useEffect(loadSources, []);
33
-
34
- return { isLoading, sources, reload: loadSources, error };
35
- };
11
+ export const useSources = ({ onRequestSources }: UseSourcesProps) =>
12
+ useAsync({ callback: onRequestSources, defaultValue: [] }, []);
@@ -30,8 +30,7 @@ const MatrixResource = ({ resource: { id, type, name, status } }: MatrixResource
30
30
  <div className="flex mb-2">
31
31
  <dt className="w-[60px] mr-4 text-gray-600">Status</dt>
32
32
  <dd className="flex items-center font-semibold">
33
- <StatusIndicator status={status} />
34
- {status.name}
33
+ <StatusIndicator className="mr-1" status={status} /> {status.name}
35
34
  </dd>
36
35
  </div>
37
36
  </dl>
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { ResourceBrowserContext, ResourceBrowserContextProps } from './ResourceBrowserContext';
4
+ import { mockSource } from '../__mocks__/MockModels';
5
+
6
+ describe('ResourceBrowserContext', () => {
7
+ it('Should render with expected default value', () => {
8
+ let defaultContext: ResourceBrowserContextProps | null = null;
9
+
10
+ render(
11
+ <ResourceBrowserContext.Consumer>
12
+ {(value) => {
13
+ defaultContext = value;
14
+ return null;
15
+ }}
16
+ </ResourceBrowserContext.Consumer>,
17
+ );
18
+
19
+ expect(defaultContext).toEqual({
20
+ onRequestChildren: expect.any(Function),
21
+ onRequestResource: expect.any(Function),
22
+ onRequestSources: expect.any(Function),
23
+ });
24
+ expect(() => defaultContext?.onRequestChildren(mockSource(), null)).toThrow(
25
+ 'onRequestChildren has not been configured.',
26
+ );
27
+ expect(() => defaultContext?.onRequestResource({ source: 'source-id', resource: 'resource-id' })).toThrow(
28
+ 'onRequestResource has not been configured.',
29
+ );
30
+ expect(() => defaultContext?.onRequestSources()).toThrow('onRequestSources has not been configured.');
31
+ });
32
+ });
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Resource, ResourceReference, Source } from '../types';
3
+
4
+ export type ResourceBrowserContextProps = {
5
+ onRequestSources: () => Promise<Source[]>;
6
+ onRequestChildren(source: Source, resource: Resource | null): Promise<Resource[]>;
7
+ onRequestResource(reference: ResourceReference): Promise<Resource | null>;
8
+ };
9
+
10
+ export const ResourceBrowserContext = React.createContext<ResourceBrowserContextProps>({
11
+ onRequestSources: () => {
12
+ throw new Error('onRequestSources has not been configured.');
13
+ },
14
+ onRequestChildren: () => {
15
+ throw new Error('onRequestChildren has not been configured.');
16
+ },
17
+ onRequestResource: () => {
18
+ throw new Error('onRequestResource has not been configured.');
19
+ },
20
+ });
@@ -36,7 +36,7 @@ const ResourceItem = <T,>({
36
36
  const title = isDisabled ? "You can't select this item" : label;
37
37
 
38
38
  return (
39
- <li className={`flex items-stretch p-1 bg-white border border-grey-200 min-h-[64px] ${className}`}>
39
+ <li className={`flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`}>
40
40
  <ModalOpeningButton
41
41
  type="button"
42
42
  {...triggerProps}
@@ -1,13 +1,19 @@
1
1
  import React from 'react';
2
2
  import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
3
3
 
4
- export const ResetButton = ({ isDisabled }: { isDisabled: boolean }) => (
4
+ export type ResetButtonProps = {
5
+ onClick: () => void;
6
+ isDisabled?: boolean;
7
+ };
8
+
9
+ export const ResetButton = ({ onClick, isDisabled }: ResetButtonProps) => (
5
10
  <button
6
11
  type="button"
7
12
  aria-label={`Remove selection`}
8
13
  title={`Remove selection`}
9
14
  className="text-gray-500 hover:text-gray-800 focus:text-gray-800 w-6 h-6 disabled:text-gray-500 disabled:cursor-not-allowed"
10
15
  disabled={isDisabled}
16
+ onClick={onClick}
11
17
  >
12
18
  <CloseRoundedIcon />
13
19
  </button>
@@ -35,9 +35,11 @@ describe('Resource picker', () => {
35
35
  });
36
36
 
37
37
  it('should render the error state if set to true', () => {
38
- render(<ResourcePicker {...defaultProps} isError={true} />);
38
+ const errorMessage = 'Failed to retrieve asset info due to a Component Service API key problem.';
39
+
40
+ render(<ResourcePicker {...defaultProps} error={new Error(errorMessage)} />);
39
41
  const pickerLabel = screen.queryByText('Choose image');
40
- const errorLabel = screen.queryByText('Failed to retrieve asset info due to a Component Service API key problem.');
42
+ const errorLabel = screen.queryByText(errorMessage);
41
43
 
42
44
  expect(pickerLabel).not.toBeInTheDocument();
43
45
  expect(errorLabel).toBeInTheDocument();
@@ -62,8 +64,10 @@ describe('Resource picker', () => {
62
64
  });
63
65
 
64
66
  it('should display the reset button in error state', () => {
65
- render(<ResourcePicker {...defaultProps} isError={true} />);
66
- const errorLabel = screen.queryByText('Failed to retrieve asset info due to a Component Service API key problem.');
67
+ const errorMessage = 'Failed to fetch resource.';
68
+
69
+ render(<ResourcePicker {...defaultProps} error={new Error(errorMessage)} />);
70
+ const errorLabel = screen.queryByText(errorMessage);
67
71
  const removeButton = screen.queryByLabelText('Remove selection');
68
72
 
69
73
  expect(errorLabel).toBeInTheDocument();
@@ -12,7 +12,7 @@ export default {
12
12
 
13
13
  const Template: StoryFn<ResourcePickerProps> = (args: ResourcePickerProps) => (
14
14
  <div className="w-[400px] m-3">
15
- <ResourcePicker {...args} />
15
+ <ResourcePicker {...args}>{() => <>Resource browser here</>}</ResourcePicker>
16
16
  </div>
17
17
  );
18
18
 
@@ -40,7 +40,7 @@ Loading.args = {
40
40
  export const Error = Template.bind({});
41
41
  Error.args = {
42
42
  ...Empty.args,
43
- isError: true,
43
+ error: new window.Error('Failed to retrieve asset info due to a Component Service API key problem.'),
44
44
  };
45
45
 
46
46
  export const Selected = Template.bind({});