@squiz/resource-browser 1.32.1-alpha.32 → 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 (92) 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/Icons/Generics/Back.d.ts +4 -0
  9. package/lib/Icons/Generics/Back.js +12 -0
  10. package/lib/Icons/Generics/Empty.d.ts +4 -0
  11. package/lib/Icons/Generics/Empty.js +12 -0
  12. package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
  13. package/lib/Icons/Generics/GenericIconMap.js +2 -0
  14. package/lib/Icons/Generics/index.d.ts +2 -0
  15. package/lib/Icons/Generics/index.js +5 -1
  16. package/lib/Icons/Icon.d.ts +2 -0
  17. package/lib/PreviewPanel/details/MatrixResource.js +2 -1
  18. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +8 -0
  19. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +18 -0
  20. package/lib/ResourceItem/ResourceItem.d.ts +2 -2
  21. package/lib/ResourceItem/ResourceItem.js +6 -5
  22. package/lib/ResourceList/ResourceList.d.ts +3 -2
  23. package/lib/ResourceList/ResourceList.js +4 -3
  24. package/lib/ResourcePicker/ResetButton.d.ts +5 -0
  25. package/lib/ResourcePicker/ResetButton.js +11 -0
  26. package/lib/ResourcePicker/ResourcePicker.d.ts +14 -0
  27. package/lib/ResourcePicker/ResourcePicker.js +26 -0
  28. package/lib/ResourcePicker/States/Error.d.ts +6 -0
  29. package/lib/ResourcePicker/States/Error.js +14 -0
  30. package/lib/ResourcePicker/States/Loading.d.ts +1 -0
  31. package/lib/ResourcePicker/States/Loading.js +11 -0
  32. package/lib/ResourcePicker/States/Selected.d.ts +7 -0
  33. package/lib/ResourcePicker/States/Selected.js +43 -0
  34. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +5 -5
  35. package/lib/ResourceState/ResourceState.d.ts +7 -0
  36. package/lib/{ResourceError/ResourceError.js → ResourceState/ResourceState.js} +7 -7
  37. package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
  38. package/lib/SourceDropdown/SourceDropdown.js +3 -3
  39. package/lib/SourceList/SourceList.d.ts +1 -3
  40. package/lib/SourceList/SourceList.js +4 -4
  41. package/lib/StatusIndicator/StatusIndicator.d.ts +2 -1
  42. package/lib/StatusIndicator/StatusIndicator.js +3 -2
  43. package/lib/index.css +9 -3
  44. package/lib/index.d.ts +8 -7
  45. package/lib/index.js +35 -13
  46. package/lib/types.d.ts +67 -0
  47. package/lib/types.js +2 -0
  48. package/package.json +3 -3
  49. package/src/Hooks/useAsync.spec.ts +106 -0
  50. package/src/Hooks/useAsync.ts +62 -0
  51. package/src/Hooks/useChildResources.spec.ts +2 -23
  52. package/src/Hooks/useChildResources.ts +9 -34
  53. package/src/Hooks/useResource.spec.ts +32 -0
  54. package/src/Hooks/useResource.ts +19 -0
  55. package/src/Hooks/useSources.spec.ts +2 -14
  56. package/src/Hooks/useSources.ts +3 -26
  57. package/src/Icons/Generics/Back.tsx +13 -0
  58. package/src/Icons/Generics/Empty.tsx +13 -0
  59. package/src/Icons/Generics/GenericIconMap.ts +3 -1
  60. package/src/Icons/Generics/index.tsx +2 -0
  61. package/src/PreviewPanel/details/MatrixResource.tsx +1 -2
  62. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +32 -0
  63. package/src/ResourceBrowserContext/ResourceBrowserContext.ts +20 -0
  64. package/src/ResourceItem/ResourceItem.tsx +7 -5
  65. package/src/ResourceList/ResourceList.spec.tsx +6 -0
  66. package/src/ResourceList/ResourceList.tsx +12 -4
  67. package/src/ResourcePicker/ResetButton.tsx +7 -1
  68. package/src/ResourcePicker/ResourcePicker.spec.tsx +8 -4
  69. package/src/ResourcePicker/ResourcePicker.stories.tsx +2 -2
  70. package/src/ResourcePicker/ResourcePicker.tsx +21 -12
  71. package/src/ResourcePicker/States/Error.tsx +9 -3
  72. package/src/ResourcePicker/States/Selected.tsx +9 -4
  73. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +1 -1
  74. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +6 -7
  75. package/src/{ResourceError/ResourceError.spec.tsx → ResourceState/ResourceState.spec.tsx} +6 -5
  76. package/src/ResourceState/ResourceState.stories.tsx +24 -0
  77. package/src/ResourceState/ResourceState.tsx +31 -0
  78. package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
  79. package/src/SourceDropdown/SourceDropdown.tsx +3 -3
  80. package/src/SourceList/SourceList.spec.tsx +1 -40
  81. package/src/SourceList/SourceList.tsx +2 -9
  82. package/src/StatusIndicator/StatusIndicator.tsx +5 -2
  83. package/src/__mocks__/StorybookHelpers.ts +18 -13
  84. package/src/index.spec.tsx +4 -4
  85. package/src/index.stories.tsx +15 -15
  86. package/src/index.tsx +39 -54
  87. package/src/{types.d.ts → types.ts} +1 -1
  88. package/tailwind.config.cjs +5 -0
  89. package/lib/Hooks/useSources.d.ts +0 -16
  90. package/lib/Hooks/useSources.js +0 -31
  91. package/lib/ResourceError/ResourceError.d.ts +0 -6
  92. package/src/ResourceError/ResourceError.tsx +0 -27
@@ -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: [] }, []);
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export default function Back({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+ <path
7
+ d="M15.2912 7.00501H4.12124L9.00124 2.12501C9.39124 1.73501 9.39124 1.09501 9.00124 0.705006C8.61124 0.315006 7.98124 0.315006 7.59124 0.705006L1.00124 7.29501C0.61124 7.68501 0.61124 8.31501 1.00124 8.70501L7.59124 15.295C7.98124 15.685 8.61124 15.685 9.00124 15.295C9.39124 14.905 9.39124 14.275 9.00124 13.885L4.12124 9.00501H15.2912C15.8412 9.00501 16.2912 8.55501 16.2912 8.00501C16.2912 7.45501 15.8412 7.00501 15.2912 7.00501Z"
8
+ fill="#3D3D3D"
9
+ />
10
+ {!isDecorative && <title>back icon</title>}
11
+ </svg>
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export default function Empty({ isDecorative, ...props }: { isDecorative: boolean } & React.SVGProps<SVGSVGElement>) {
4
+ return (
5
+ <svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
6
+ <path
7
+ d="M1.3925 3.82749C0.612495 4.60749 0.612495 5.88749 1.3925 6.66749L4.5125 9.78749C2.0125 13.5475 0.752495 18.1875 1.3325 23.1675C2.37249 32.2475 9.75249 39.6275 18.8325 40.6675C23.8125 41.2475 28.4525 39.9875 32.2125 37.4875L35.3325 40.6075C36.1125 41.3875 37.3725 41.3875 38.1525 40.6075C38.9325 39.8275 38.9325 38.5675 38.1525 37.7875L4.21249 3.82749C3.4325 3.04749 2.17249 3.04749 1.3925 3.82749ZM21.1925 36.8075C12.3725 36.8075 5.19249 29.6275 5.19249 20.8075C5.19249 17.8475 6.01249 15.0875 7.43249 12.6875L29.3125 34.5675C26.9125 35.9875 24.1525 36.8075 21.1925 36.8075ZM13.0725 7.04749L10.1725 4.12749C13.3325 2.0275 17.1125 0.807495 21.1925 0.807495C32.2325 0.807495 41.1925 9.76749 41.1925 20.8075C41.1925 24.8875 39.9725 28.6675 37.8725 31.8275L34.9525 28.9075C36.3725 26.5275 37.1925 23.7675 37.1925 20.8075C37.1925 11.9875 30.0125 4.80749 21.1925 4.80749C18.2325 4.80749 15.4725 5.62749 13.0725 7.04749Z"
8
+ fill="#949494"
9
+ />
10
+ {!isDecorative && <title>empty icon</title>}
11
+ </svg>
12
+ );
13
+ }
@@ -1,4 +1,4 @@
1
- import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry } from '.';
1
+ import { ArrowRight, ArrowDown, Selected, Root, ResourceSelect, Close, Error, Retry, Empty, Back } from '.';
2
2
 
3
3
  // Define our map of matrix types to icons
4
4
  const GenericIconMap = {
@@ -10,6 +10,8 @@ const GenericIconMap = {
10
10
  close: Close,
11
11
  error: Error,
12
12
  retry: Retry,
13
+ empty: Empty,
14
+ back: Back,
13
15
  };
14
16
 
15
17
  // Export our map
@@ -7,3 +7,5 @@ export { default as ResourceSelect } from './ResourceSelect';
7
7
  export { default as Close } from './Close';
8
8
  export { default as Error } from './Error';
9
9
  export { default as Retry } from './Retry';
10
+ export { default as Empty } from './Empty';
11
+ export { default as Back } from './Back';
@@ -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
+ });
@@ -11,10 +11,10 @@ interface ResourceItem<T> {
11
11
  selected?: boolean;
12
12
  label: string;
13
13
  type: string;
14
- childCount: number;
14
+ childCount?: number;
15
15
  previewModalState: OverlayTriggerState;
16
16
  onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
17
- onDrillDown: (node: T) => void;
17
+ onDrillDown?: (node: T) => void;
18
18
  className: string;
19
19
  allowedTypes?: string[] | undefined;
20
20
  }
@@ -36,15 +36,16 @@ 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}
43
43
  isDisabled={isDisabled}
44
44
  onPress={() => onSelect(item, overlayProps)}
45
+ aria-label={childCount === undefined ? `Drill down to ${label} children` : ''}
45
46
  className={`
46
47
  relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${
47
- childCount > 0 ? 'mr-2' : ''
48
+ childCount !== undefined && childCount > 0 ? 'mr-2' : ''
48
49
  } ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
49
50
  `}
50
51
  title={title}
@@ -59,8 +60,9 @@ const ResourceItem = <T,>({
59
60
  <span className="line-clamp-2 text-left break-word">{label}</span>
60
61
  {selected && <Icon icon={'selected' as IconOptions} aria-label="selected" className="absolute -right-8" />}
61
62
  </span>
63
+ {childCount === undefined && <Icon icon={'arrow-right' as IconOptions} className="absolute right-5" />}
62
64
  </ModalOpeningButton>
63
- {childCount > 0 && (
65
+ {childCount !== undefined && childCount > 0 && onDrillDown && (
64
66
  <button
65
67
  type="button"
66
68
  aria-label={`Drill down to ${label} children`}
@@ -39,6 +39,7 @@ describe('ResourceList', () => {
39
39
  onResourceDrillDown={() => {}}
40
40
  error={null}
41
41
  handleReload={reload}
42
+ handleReturnToRoot={reload}
42
43
  />
43
44
  );
44
45
  }}
@@ -65,6 +66,7 @@ describe('ResourceList', () => {
65
66
  onResourceDrillDown={() => {}}
66
67
  error={null}
67
68
  handleReload={reload}
69
+ handleReturnToRoot={reload}
68
70
  />
69
71
  );
70
72
  }}
@@ -91,6 +93,7 @@ describe('ResourceList', () => {
91
93
  onResourceDrillDown={() => {}}
92
94
  error={null}
93
95
  handleReload={reload}
96
+ handleReturnToRoot={reload}
94
97
  />
95
98
  );
96
99
  }}
@@ -122,6 +125,7 @@ describe('ResourceList', () => {
122
125
  onResourceDrillDown={() => {}}
123
126
  error={null}
124
127
  handleReload={reload}
128
+ handleReturnToRoot={reload}
125
129
  />
126
130
  );
127
131
  }}
@@ -153,6 +157,7 @@ describe('ResourceList', () => {
153
157
  onResourceDrillDown={onResourceDrillDown}
154
158
  error={null}
155
159
  handleReload={reload}
160
+ handleReturnToRoot={reload}
156
161
  />
157
162
  );
158
163
  }}
@@ -182,6 +187,7 @@ describe('ResourceList', () => {
182
187
  onResourceDrillDown={() => {}}
183
188
  error={new Error('This is a resource error!')}
184
189
  handleReload={reload}
190
+ handleReturnToRoot={reload}
185
191
  />
186
192
  );
187
193
  }}
@@ -5,7 +5,7 @@ import { DOMAttributes, FocusableElement } from '@react-types/shared';
5
5
  import ResourceItem from '../ResourceItem/ResourceItem';
6
6
  import { Resource } from '../types';
7
7
  import { SkeletonListItem } from '../Skeleton/ListItem/SkeletonListItem';
8
- import ResourceError from '../ResourceError/ResourceError';
8
+ import ResourceState from '../ResourceState/ResourceState';
9
9
 
10
10
  export interface ResourceListProps {
11
11
  resources: Array<Resource>;
@@ -15,8 +15,9 @@ export interface ResourceListProps {
15
15
  onResourceSelect: (resource: Resource, overlayProps: DOMAttributes<FocusableElement>) => void;
16
16
  onResourceDrillDown: (resource: Resource) => void;
17
17
  allowedTypes?: string[] | undefined;
18
- error: Error | null;
18
+ handleReturnToRoot: () => void;
19
19
  handleReload: () => void;
20
+ error: Error | null;
20
21
  }
21
22
 
22
23
  const ResourceList = function ({
@@ -27,8 +28,9 @@ const ResourceList = function ({
27
28
  onResourceSelect,
28
29
  onResourceDrillDown,
29
30
  allowedTypes,
30
- error,
31
+ handleReturnToRoot,
31
32
  handleReload,
33
+ error,
32
34
  }: ResourceListProps) {
33
35
  const listRef = useRef<HTMLUListElement>(null);
34
36
 
@@ -56,7 +58,13 @@ const ResourceList = function ({
56
58
  </>
57
59
  )}
58
60
 
59
- {!isLoading && error && <ResourceError errorMessage={error.message} handleReload={handleReload} />}
61
+ {/* Error State */}
62
+ {!isLoading && error && <ResourceState state="error" message={error.message} handleReload={handleReload} />}
63
+
64
+ {/* Empty State */}
65
+ {!isLoading && !error && resources.length === 0 && (
66
+ <ResourceState state="empty" handleReload={handleReturnToRoot} />
67
+ )}
60
68
 
61
69
  {!isLoading &&
62
70
  !error &&
@@ -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({});
@@ -1,10 +1,9 @@
1
1
  import React from 'react';
2
-
3
- import { Resource } from '../types';
4
2
  import AdsClickRoundedIcon from '@mui/icons-material/AdsClickRounded';
5
3
  import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
6
4
  import PhotoLibraryRoundedIcon from '@mui/icons-material/PhotoLibraryRounded';
7
-
5
+ import { DOMAttributes } from '@react-types/shared';
6
+ import { Resource } from '../types';
8
7
  import ModalTrigger from '../Modal/ModalTrigger';
9
8
  import { ErrorState } from './States/Error';
10
9
  import { LoadingState } from './States/Loading';
@@ -12,16 +11,26 @@ import { SelectedState } from './States/Selected';
12
11
  import clsx from 'clsx';
13
12
 
14
13
  export type ResourcePickerProps = {
15
- resource: Resource;
14
+ resource: Resource | null;
16
15
  allowedTypes: string[] | undefined;
17
- isError: boolean;
16
+ error: Error | null;
18
17
  isLoading: boolean;
19
- isDisabled: boolean;
18
+ isDisabled?: boolean;
19
+ children: (onClose: () => void, titleProps: DOMAttributes) => React.ReactElement;
20
+ onClear: () => void;
20
21
  };
21
22
 
22
- const ResourcePicker = ({ resource, allowedTypes, isError, isLoading, isDisabled }: ResourcePickerProps) => {
23
- const isImagePicker = allowedTypes && allowedTypes.length === 1 && (allowedTypes.includes('image') as boolean);
24
- const isEmpty = resource === null && !isLoading && (!isError as boolean);
23
+ const ResourcePicker = ({
24
+ resource,
25
+ allowedTypes,
26
+ error,
27
+ isLoading,
28
+ isDisabled,
29
+ children,
30
+ onClear,
31
+ }: ResourcePickerProps) => {
32
+ const isImagePicker = allowedTypes && allowedTypes.length === 1 && allowedTypes.includes('image');
33
+ const isEmpty = resource === null && !isLoading && !error;
25
34
 
26
35
  return (
27
36
  <div className={clsx('resource-picker', isDisabled && 'bg-gray-300')}>
@@ -37,14 +46,14 @@ const ResourcePicker = ({ resource, allowedTypes, isError, isLoading, isDisabled
37
46
  icon={<AddCircleOutlineRoundedIcon aria-hidden className="!w-4 !h-4" />}
38
47
  isDisabled={isDisabled}
39
48
  >
40
- {() => <div>Resource browser here</div>}
49
+ {children}
41
50
  </ModalTrigger>
42
51
  ) : (
43
52
  <div className="resource-picker-info">
44
53
  <div className="resource-picker-info__layout">
45
54
  {isLoading && <LoadingState />}
46
- {isError && <ErrorState isDisabled={isDisabled} />}
47
- {resource !== null && <SelectedState resource={resource} isDisabled={isDisabled} />}
55
+ {error && <ErrorState error={error} onClear={onClear} />}
56
+ {resource && <SelectedState resource={resource} isDisabled={isDisabled} onClear={onClear} />}
48
57
  </div>
49
58
  </div>
50
59
  )}
@@ -3,10 +3,16 @@ import React from 'react';
3
3
  import Icon, { IconOptions } from '../../Icons/Icon';
4
4
  import { ResetButton } from '../ResetButton';
5
5
 
6
- export const ErrorState = ({ isDisabled }: { isDisabled: boolean }) => (
6
+ export type ErrorStateProps = {
7
+ error: Error;
8
+ isDisabled?: boolean;
9
+ onClear: () => void;
10
+ };
11
+
12
+ export const ErrorState = ({ error, isDisabled, onClear }: ErrorStateProps) => (
7
13
  <>
8
14
  <Icon icon={'error' as IconOptions} aria-hidden className="w-6 h-6 text-red-300" />
9
- <div className="text-red-300">Failed to retrieve asset info due to a Component Service API key problem.</div>
10
- <ResetButton isDisabled={isDisabled} />
15
+ <div className="text-red-300">{error.message}</div>
16
+ <ResetButton isDisabled={isDisabled} onClick={onClear} />
11
17
  </>
12
18
  );
@@ -8,10 +8,15 @@ import { ResetButton } from '../ResetButton';
8
8
 
9
9
  export type SelectedStateProps = {
10
10
  resource: Resource;
11
- isDisabled: boolean;
11
+ isDisabled?: boolean;
12
+ onClear: () => void;
12
13
  };
13
14
 
14
- export const SelectedState = ({ resource: { id, type, name, status, squizImage }, isDisabled }: SelectedStateProps) => {
15
+ export const SelectedState = ({
16
+ resource: { id, type, name, status, squizImage },
17
+ isDisabled,
18
+ onClear,
19
+ }: SelectedStateProps) => {
15
20
  const fileSize = squizImage?.imageVariations?.original?.byteSize;
16
21
  const fileWidth = squizImage?.imageVariations?.original?.width;
17
22
  const fileHeight = squizImage?.imageVariations?.original?.height;
@@ -23,9 +28,9 @@ export const SelectedState = ({ resource: { id, type, name, status, squizImage }
23
28
  {/* Center column */}
24
29
  <div className="justify-self-start self-center">{name}</div>
25
30
  {/* End column */}
26
- <ResetButton isDisabled={isDisabled} />
31
+ <ResetButton isDisabled={isDisabled} onClick={onClear} />
27
32
  </>
28
- <dl className="col-start-2 col-end-2 flex flex-column gap-1 justify-self-start items-center font-normal text-sm">
33
+ <dl className="col-start-2 col-end-2 flex flex-row gap-1 justify-self-start items-center font-normal text-sm">
29
34
  <div>
30
35
  <dt className="hidden">Status: {status.name}</dt>
31
36
  <dd className="flex items-center">
@@ -324,7 +324,7 @@ describe('ResourcePickerContainer', () => {
324
324
  });
325
325
 
326
326
  const user = userEvent.setup();
327
- user.click(screen.getByRole('button', { name: 'site Test Website' }));
327
+ user.click(screen.getByRole('button', { name: 'Drill down to Test Website children' }));
328
328
 
329
329
  expect(mockModalState).toEqual(
330
330
  expect.objectContaining({
@@ -10,8 +10,8 @@ import SourceDropdown from '../SourceDropdown/SourceDropdown';
10
10
 
11
11
  import { Source, Resource, HydratedResourceReference, ScopedSource } from '../types';
12
12
  import { useResourcePath } from '../Hooks/useResourcePath';
13
+ import { useAsync } from '../Hooks/useAsync';
13
14
  import { useChildResources } from '../Hooks/useChildResources';
14
- import { useSources } from '../Hooks/useSources';
15
15
 
16
16
  interface ResourcePickerContainerProps {
17
17
  title: string;
@@ -38,16 +38,16 @@ function ResourcePickerContainer({
38
38
  const { source, currentResource, hierarchy, setSource, push, popUntil } = useResourcePath();
39
39
 
40
40
  const {
41
- sources,
41
+ data: sources,
42
42
  isLoading: isSourceLoading,
43
43
  reload: handleSourceReload,
44
44
  error: sourceError,
45
- } = useSources({ onRequestSources });
45
+ } = useAsync({ callback: onRequestSources, defaultValue: [] }, []);
46
46
 
47
47
  const {
48
- resources,
48
+ data: resources,
49
49
  isLoading: isResourcesLoading,
50
- reloadResources: handleResourceReload,
50
+ reload: handleResourceReload,
51
51
  error: resourceError,
52
52
  } = useChildResources({ source, currentResource, onRequestChildren });
53
53
 
@@ -136,8 +136,6 @@ function ResourcePickerContainer({
136
136
  previewModalState={previewModalState}
137
137
  isLoading={isSourceLoading}
138
138
  onSourceSelect={handleSourceDrilldown}
139
- onSourceDrillDown={handleSourceDrilldown}
140
- allowedTypes={allowedTypes}
141
139
  handleReload={handleSourceReload}
142
140
  error={sourceError}
143
141
  />
@@ -151,6 +149,7 @@ function ResourcePickerContainer({
151
149
  onResourceSelect={handleResourceSelected}
152
150
  onResourceDrillDown={handleResourceDrillDown}
153
151
  allowedTypes={allowedTypes}
152
+ handleReturnToRoot={handleReturnToRoot}
154
153
  handleReload={handleResourceReload}
155
154
  error={resourceError}
156
155
  />
@@ -1,22 +1,23 @@
1
1
  import React from 'react';
2
2
  import { render, fireEvent } from '@testing-library/react';
3
- import ResourceError from './ResourceError';
3
+ import ResourceState from './ResourceState';
4
4
 
5
5
  const defaultProps: any = {
6
- errorMessage: 'This is a test error!',
6
+ state: 'error',
7
+ message: 'This is a test error!',
7
8
  handleReload: jest.fn(),
8
9
  };
9
10
 
10
11
  describe('ResourceError', () => {
11
12
  it('should render the component with the correct error message', () => {
12
- const { getByText } = render(<ResourceError {...defaultProps} />);
13
- const errorMessage = getByText(defaultProps.errorMessage);
13
+ const { getByText } = render(<ResourceState {...defaultProps} />);
14
+ const errorMessage = getByText(defaultProps.message);
14
15
 
15
16
  expect(errorMessage).toBeInTheDocument();
16
17
  });
17
18
 
18
19
  it('should call the reload function when the button is pressed', () => {
19
- const { getByRole } = render(<ResourceError {...defaultProps} />);
20
+ const { getByRole } = render(<ResourceState {...defaultProps} />);
20
21
  const buttonElement = getByRole('button');
21
22
  fireEvent.click(buttonElement);
22
23
 
@@ -0,0 +1,24 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+
4
+ import ResourceState from './ResourceState';
5
+
6
+ export default {
7
+ title: 'Resource State',
8
+ component: ResourceState,
9
+ } as Meta<typeof ResourceState>;
10
+
11
+ const Template: StoryFn<typeof ResourceState> = ({ state, message }) => (
12
+ <ResourceState state={state} message={message} handleReload={() => alert('Resource browser reload')} />
13
+ );
14
+
15
+ export const Error = Template.bind({});
16
+ Error.args = {
17
+ state: 'error',
18
+ message: 'This is a resource browser error!',
19
+ };
20
+
21
+ export const Empty = Template.bind({});
22
+ Empty.args = {
23
+ state: 'empty',
24
+ };
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import Icon, { IconOptions } from '../Icons/Icon';
3
+
4
+ interface ResourceState {
5
+ state: 'error' | 'empty';
6
+ message?: string;
7
+ handleReload: () => void;
8
+ }
9
+
10
+ const ResourceState = function ({ state, message, handleReload }: ResourceState) {
11
+ return (
12
+ <div className="flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
13
+ <Icon icon={state as IconOptions} aria-hidden />
14
+ {/* Message */}
15
+ <span className="text-md text-gray-800 font-semibold leading-5">
16
+ {state === 'empty' ? 'There are no items to display' : message}
17
+ </span>
18
+ {/* Retry button */}
19
+ <button
20
+ type="button"
21
+ onClick={handleReload}
22
+ className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3"
23
+ >
24
+ <Icon icon={state === 'empty' ? 'back' : 'retry'} aria-hidden />
25
+ {state === 'empty' ? 'Back to source list' : 'Try again'}
26
+ </button>
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default ResourceState;