@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.
- package/lib/Hooks/useAsync.d.ts +21 -0
- package/lib/Hooks/useAsync.js +53 -0
- package/lib/Hooks/useChildResources.d.ts +3 -7
- package/lib/Hooks/useChildResources.js +5 -29
- package/lib/Hooks/useResource.d.ts +15 -0
- package/lib/Hooks/useResource.js +12 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Icons/Generics/Back.d.ts +4 -0
- package/lib/Icons/Generics/Back.js +12 -0
- package/lib/Icons/Generics/Empty.d.ts +4 -0
- package/lib/Icons/Generics/Empty.js +12 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +3 -1
- package/lib/Icons/Generics/GenericIconMap.js +2 -0
- package/lib/Icons/Generics/index.d.ts +2 -0
- package/lib/Icons/Generics/index.js +5 -1
- package/lib/Icons/Icon.d.ts +2 -0
- package/lib/PreviewPanel/details/MatrixResource.js +2 -1
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +8 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +18 -0
- package/lib/ResourceItem/ResourceItem.d.ts +2 -2
- package/lib/ResourceItem/ResourceItem.js +6 -5
- package/lib/ResourceList/ResourceList.d.ts +3 -2
- package/lib/ResourceList/ResourceList.js +4 -3
- package/lib/ResourcePicker/ResetButton.d.ts +5 -0
- package/lib/ResourcePicker/ResetButton.js +11 -0
- package/lib/ResourcePicker/ResourcePicker.d.ts +14 -0
- package/lib/ResourcePicker/ResourcePicker.js +26 -0
- package/lib/ResourcePicker/States/Error.d.ts +6 -0
- package/lib/ResourcePicker/States/Error.js +14 -0
- package/lib/ResourcePicker/States/Loading.d.ts +1 -0
- package/lib/ResourcePicker/States/Loading.js +11 -0
- package/lib/ResourcePicker/States/Selected.d.ts +7 -0
- package/lib/ResourcePicker/States/Selected.js +43 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +5 -5
- package/lib/ResourceState/ResourceState.d.ts +7 -0
- package/lib/{ResourceError/ResourceError.js → ResourceState/ResourceState.js} +7 -7
- package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
- package/lib/SourceDropdown/SourceDropdown.js +3 -3
- package/lib/SourceList/SourceList.d.ts +1 -3
- package/lib/SourceList/SourceList.js +4 -4
- package/lib/StatusIndicator/StatusIndicator.d.ts +2 -1
- package/lib/StatusIndicator/StatusIndicator.js +3 -2
- package/lib/index.css +9 -3
- package/lib/index.d.ts +8 -7
- package/lib/index.js +35 -13
- package/lib/types.d.ts +67 -0
- package/lib/types.js +2 -0
- package/package.json +3 -3
- package/src/Hooks/useAsync.spec.ts +106 -0
- package/src/Hooks/useAsync.ts +62 -0
- package/src/Hooks/useChildResources.spec.ts +2 -23
- package/src/Hooks/useChildResources.ts +9 -34
- package/src/Hooks/useResource.spec.ts +32 -0
- package/src/Hooks/useResource.ts +19 -0
- package/src/Hooks/useSources.spec.ts +2 -14
- package/src/Hooks/useSources.ts +3 -26
- package/src/Icons/Generics/Back.tsx +13 -0
- package/src/Icons/Generics/Empty.tsx +13 -0
- package/src/Icons/Generics/GenericIconMap.ts +3 -1
- package/src/Icons/Generics/index.tsx +2 -0
- package/src/PreviewPanel/details/MatrixResource.tsx +1 -2
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +32 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +20 -0
- package/src/ResourceItem/ResourceItem.tsx +7 -5
- package/src/ResourceList/ResourceList.spec.tsx +6 -0
- package/src/ResourceList/ResourceList.tsx +12 -4
- package/src/ResourcePicker/ResetButton.tsx +7 -1
- package/src/ResourcePicker/ResourcePicker.spec.tsx +8 -4
- package/src/ResourcePicker/ResourcePicker.stories.tsx +2 -2
- package/src/ResourcePicker/ResourcePicker.tsx +21 -12
- package/src/ResourcePicker/States/Error.tsx +9 -3
- package/src/ResourcePicker/States/Selected.tsx +9 -4
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +6 -7
- package/src/{ResourceError/ResourceError.spec.tsx → ResourceState/ResourceState.spec.tsx} +6 -5
- package/src/ResourceState/ResourceState.stories.tsx +24 -0
- package/src/ResourceState/ResourceState.tsx +31 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
- package/src/SourceDropdown/SourceDropdown.tsx +3 -3
- package/src/SourceList/SourceList.spec.tsx +1 -40
- package/src/SourceList/SourceList.tsx +2 -9
- package/src/StatusIndicator/StatusIndicator.tsx +5 -2
- package/src/__mocks__/StorybookHelpers.ts +18 -13
- package/src/index.spec.tsx +4 -4
- package/src/index.stories.tsx +15 -15
- package/src/index.tsx +39 -54
- package/src/{types.d.ts → types.ts} +1 -1
- package/tailwind.config.cjs +5 -0
- package/lib/Hooks/useSources.d.ts +0 -16
- package/lib/Hooks/useSources.js +0 -31
- package/lib/ResourceError/ResourceError.d.ts +0 -6
- 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.
|
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.
|
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
|
});
|
package/src/Hooks/useSources.ts
CHANGED
@@ -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
|
-
|
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
|
14
|
+
childCount?: number;
|
15
15
|
previewModalState: OverlayTriggerState;
|
16
16
|
onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
|
17
|
-
onDrillDown
|
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
|
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
|
-
|
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
|
-
|
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
|
-
{
|
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
|
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
|
-
|
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(
|
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
|
-
|
66
|
-
|
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
|
-
|
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
|
-
|
16
|
+
error: Error | null;
|
18
17
|
isLoading: boolean;
|
19
|
-
isDisabled
|
18
|
+
isDisabled?: boolean;
|
19
|
+
children: (onClose: () => void, titleProps: DOMAttributes) => React.ReactElement;
|
20
|
+
onClear: () => void;
|
20
21
|
};
|
21
22
|
|
22
|
-
const ResourcePicker = ({
|
23
|
-
|
24
|
-
|
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
|
-
{
|
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
|
-
{
|
47
|
-
{resource
|
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
|
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">
|
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
|
11
|
+
isDisabled?: boolean;
|
12
|
+
onClear: () => void;
|
12
13
|
};
|
13
14
|
|
14
|
-
export const SelectedState = ({
|
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-
|
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: '
|
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
|
-
} =
|
45
|
+
} = useAsync({ callback: onRequestSources, defaultValue: [] }, []);
|
46
46
|
|
47
47
|
const {
|
48
|
-
resources,
|
48
|
+
data: resources,
|
49
49
|
isLoading: isResourcesLoading,
|
50
|
-
|
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
|
3
|
+
import ResourceState from './ResourceState';
|
4
4
|
|
5
5
|
const defaultProps: any = {
|
6
|
-
|
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(<
|
13
|
-
const errorMessage = getByText(defaultProps.
|
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(<
|
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;
|