@squiz/resource-browser 1.66.3 → 1.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/lib/Hooks/usePreselectedResourcePath.d.ts +20 -0
- package/lib/Hooks/usePreselectedResourcePath.js +26 -0
- package/lib/Hooks/useResourcePath.d.ts +1 -1
- package/lib/Hooks/useResourcePath.js +2 -2
- package/lib/Hooks/useSources.d.ts +14 -0
- package/lib/Hooks/useSources.js +9 -0
- package/lib/Icons/CircledLoopIcon.d.ts +4 -0
- package/lib/Icons/CircledLoopIcon.js +12 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +12 -5
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +52 -2
- package/lib/ResourcePicker/ResourcePicker.js +1 -1
- package/lib/ResourcePicker/States/Selected.d.ts +2 -1
- package/lib/ResourcePicker/States/Selected.js +6 -2
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +7 -4
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +35 -11
- package/lib/SourceDropdown/SourceDropdown.js +1 -1
- package/lib/index.css +19 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +3 -2
- package/lib/types.d.ts +7 -0
- package/lib/utils/findBestMatchLineage.d.ts +2 -0
- package/lib/utils/findBestMatchLineage.js +28 -0
- package/package.json +6 -4
- package/src/Hooks/usePreselectedResourcePath.ts +50 -0
- package/src/Hooks/useResourcePath.ts +2 -2
- package/src/Hooks/useSources.ts +1 -1
- package/src/Icons/CircledLoopIcon.tsx +14 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +93 -3
- package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +56 -0
- package/src/ResourceList/sample-resources.json +684 -439
- package/src/ResourcePicker/ResourcePicker.tsx +8 -1
- package/src/ResourcePicker/States/Selected.tsx +23 -3
- package/src/ResourcePicker/resource-picker.scss +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +146 -32
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +64 -18
- package/src/SourceDropdown/SourceDropdown.tsx +1 -1
- package/src/SourceList/sample-sources.json +4 -4
- package/src/__mocks__/MockModels.ts +1 -0
- package/src/__mocks__/StorybookHelpers.ts +33 -4
- package/src/__mocks__/renderWithContext.tsx +23 -0
- package/src/index.spec.tsx +81 -21
- package/src/index.stories.tsx +4 -4
- package/src/index.tsx +10 -2
- package/src/types.ts +9 -0
- package/src/utils/findBestMatchLineage.spec.ts +81 -0
- package/src/utils/findBestMatchLineage.ts +30 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.ts +0 -20
- /package/lib/{uuid.d.ts → utils/uuid.d.ts} +0 -0
- /package/lib/{uuid.js → utils/uuid.js} +0 -0
- /package/src/{uuid.ts → utils/uuid.ts} +0 -0
package/src/index.spec.tsx
CHANGED
@@ -1,31 +1,40 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import {
|
3
|
-
import { ResourceBrowserInput } from './index';
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
const
|
9
|
-
|
10
|
-
const
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
}
|
18
|
-
|
19
|
-
|
2
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
|
3
|
+
import { ResourceBrowserInput, ResourceBrowserInputProps, ResourceReference } from './index';
|
4
|
+
import { mockResource } from './__mocks__/MockModels';
|
5
|
+
import { renderWithContext } from './__mocks__/renderWithContext';
|
6
|
+
|
7
|
+
describe('Resource browser input', () => {
|
8
|
+
const mockRequestSources = jest.fn().mockResolvedValue([]);
|
9
|
+
const mockRequestChildren = jest.fn().mockResolvedValue([]);
|
10
|
+
const mockRequestResource = jest.fn();
|
11
|
+
const mockChange = jest.fn();
|
12
|
+
const renderComponent = (props: Partial<ResourceBrowserInputProps> = {}) => {
|
13
|
+
return renderWithContext(
|
14
|
+
<ResourceBrowserInput
|
15
|
+
modalTitle="Asset picker"
|
16
|
+
allowedTypes={['site', 'image', 'physical_file']}
|
17
|
+
onChange={mockChange}
|
18
|
+
value={null}
|
19
|
+
{...props}
|
20
|
+
/>,
|
21
|
+
{
|
22
|
+
onRequestSources: mockRequestSources,
|
23
|
+
onRequestChildren: mockRequestChildren,
|
24
|
+
onRequestResource: mockRequestResource,
|
25
|
+
},
|
26
|
+
);
|
27
|
+
};
|
28
|
+
|
20
29
|
it('should render the related asset picker with the default label', () => {
|
21
|
-
|
30
|
+
renderComponent();
|
22
31
|
const pickerLabel = screen.getByText('Choose asset');
|
23
32
|
|
24
33
|
expect(pickerLabel).toBeInTheDocument();
|
25
34
|
});
|
26
35
|
|
27
36
|
it('should display the generic asset picking icon', () => {
|
28
|
-
|
37
|
+
renderComponent();
|
29
38
|
const pickerLabel = screen.getByText('Choose asset');
|
30
39
|
const pickerIcon = screen.getByTestId('AdsClickRoundedIcon');
|
31
40
|
|
@@ -34,11 +43,62 @@ describe('Related Asset Picker', () => {
|
|
34
43
|
});
|
35
44
|
|
36
45
|
it('should display the image icon when only images are allowed', () => {
|
37
|
-
|
46
|
+
renderComponent({ allowedTypes: ['image'] });
|
38
47
|
const pickerLabel = screen.getByText('Choose image');
|
39
48
|
const pickerIcon = screen.getByTestId('PhotoLibraryRoundedIcon');
|
40
49
|
|
41
50
|
expect(pickerLabel).toBeInTheDocument();
|
42
51
|
expect(pickerIcon).toBeInTheDocument();
|
43
52
|
});
|
53
|
+
|
54
|
+
it('should disallow removing and replacing selection if the input is disabled', async () => {
|
55
|
+
mockRequestResource.mockResolvedValue(
|
56
|
+
mockResource({
|
57
|
+
id: '100',
|
58
|
+
name: 'Mocked resource',
|
59
|
+
}),
|
60
|
+
);
|
61
|
+
renderComponent({ value: { source: '1', resource: '100' }, isDisabled: true });
|
62
|
+
|
63
|
+
await waitFor(() => expect(screen.queryByLabelText('Loading selection')).not.toBeInTheDocument());
|
64
|
+
|
65
|
+
expect(screen.getByRole('button', { name: 'Remove selection' })).toBeDisabled();
|
66
|
+
expect(screen.getByRole('button', { name: 'Replace selection' })).toBeDisabled();
|
67
|
+
});
|
68
|
+
|
69
|
+
it('clicking the replace selection button should open the resource browser', async () => {
|
70
|
+
mockRequestResource.mockResolvedValue(
|
71
|
+
mockResource({
|
72
|
+
id: '100',
|
73
|
+
name: 'Mocked resource',
|
74
|
+
}),
|
75
|
+
);
|
76
|
+
renderComponent({ value: { source: '1', resource: '100' } });
|
77
|
+
|
78
|
+
await waitFor(() => expect(screen.queryByLabelText('Loading selection')).not.toBeInTheDocument());
|
79
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Replace selection' })));
|
80
|
+
expect(screen.getByRole('button', { name: 'Close Asset picker dialog' })).toBeInTheDocument();
|
81
|
+
});
|
82
|
+
|
83
|
+
it.each([
|
84
|
+
['resource selected', { source: '1', resource: '100' }, 1],
|
85
|
+
['resource not selected', null, 0],
|
86
|
+
])(
|
87
|
+
'should only display the "replace" button if a resource has already been selected - %s',
|
88
|
+
async (description: string, value: ResourceReference | null, expectedReplaceSelectionButtons: number) => {
|
89
|
+
mockRequestResource.mockResolvedValue(
|
90
|
+
mockResource({
|
91
|
+
id: '100',
|
92
|
+
name: 'Mocked resource',
|
93
|
+
}),
|
94
|
+
);
|
95
|
+
renderComponent({ value });
|
96
|
+
|
97
|
+
await waitFor(() => expect(screen.queryByLabelText('Loading selection')).not.toBeInTheDocument());
|
98
|
+
|
99
|
+
expect(screen.queryAllByRole('button', { name: 'Replace selection' })).toHaveLength(
|
100
|
+
expectedReplaceSelectionButtons,
|
101
|
+
);
|
102
|
+
},
|
103
|
+
);
|
44
104
|
});
|
package/src/index.stories.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { StoryFn, Meta } from '@storybook/react';
|
2
|
-
import { ResourceBrowserInput,
|
2
|
+
import { ResourceBrowserInput, ResourceBrowserContextProvider } from './index';
|
3
3
|
import { createResourceBrowserCallbacks } from './__mocks__/StorybookHelpers';
|
4
4
|
|
5
5
|
export default {
|
@@ -17,9 +17,9 @@ const Template: StoryFn<typeof ResourceBrowserInput> = (props) => {
|
|
17
17
|
|
18
18
|
return (
|
19
19
|
<div className="w-[400px] m-3">
|
20
|
-
<
|
20
|
+
<ResourceBrowserContextProvider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
|
21
21
|
<ResourceBrowserInput {...other} onChange={onChange} />
|
22
|
-
</
|
22
|
+
</ResourceBrowserContextProvider>
|
23
23
|
</div>
|
24
24
|
);
|
25
25
|
};
|
@@ -36,7 +36,7 @@ Primary.args = {
|
|
36
36
|
export const Selected = Template.bind({});
|
37
37
|
Selected.args = {
|
38
38
|
...Primary.args,
|
39
|
-
value: { resource: '
|
39
|
+
value: { resource: '33', source: '1' },
|
40
40
|
};
|
41
41
|
|
42
42
|
export const ImagesOnly = Template.bind({});
|
package/src/index.tsx
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
import React, { useContext } from 'react';
|
2
2
|
import ResourcePickerContainer from './ResourcePickerContainer/ResourcePickerContainer';
|
3
3
|
import { HydratedResourceReference, Resource, ResourceReference, Source } from './types';
|
4
|
-
import {
|
4
|
+
import {
|
5
|
+
ResourceBrowserContext,
|
6
|
+
ResourceBrowserContextProvider,
|
7
|
+
ResourceBrowserContextProps,
|
8
|
+
} from './ResourceBrowserContext/ResourceBrowserContext';
|
5
9
|
import ResourcePicker from './ResourcePicker/ResourcePicker';
|
6
10
|
import { useResource } from './Hooks/useResource';
|
7
11
|
|
8
12
|
export type { HydratedResourceReference, Resource, ResourceReference, Source, ResourceBrowserContextProps };
|
9
|
-
export { ResourceBrowserContext };
|
13
|
+
export { ResourceBrowserContext, ResourceBrowserContextProvider };
|
10
14
|
|
11
15
|
export type ResourceBrowserInputProps = {
|
12
16
|
modalTitle: string;
|
@@ -29,6 +33,7 @@ export const ResourceBrowserInput = ({
|
|
29
33
|
const { data: resource, error, isLoading } = useResource({ onRequestResource, reference: value });
|
30
34
|
const defaultOnClear = () => onChange(null);
|
31
35
|
const onClearFunction = onClear ?? defaultOnClear;
|
36
|
+
|
32
37
|
return (
|
33
38
|
<div className="squiz-rb-scope">
|
34
39
|
<ResourcePicker
|
@@ -41,11 +46,14 @@ export const ResourceBrowserInput = ({
|
|
41
46
|
>
|
42
47
|
{(onClose, titleProps) => (
|
43
48
|
<ResourcePickerContainer
|
49
|
+
preselectedSourceId={value?.source}
|
50
|
+
preselectedResource={resource}
|
44
51
|
title={modalTitle}
|
45
52
|
titleAriaProps={titleProps}
|
46
53
|
allowedTypes={allowedTypes}
|
47
54
|
onClose={onClose}
|
48
55
|
onRequestSources={onRequestSources}
|
56
|
+
onRequestResource={onRequestResource}
|
49
57
|
onRequestChildren={onRequestChildren}
|
50
58
|
onChange={onChange}
|
51
59
|
/>
|
package/src/types.ts
CHANGED
@@ -19,6 +19,11 @@ export type Resource = {
|
|
19
19
|
urls: string[];
|
20
20
|
childCount: number;
|
21
21
|
squizImage?: SquizImageType['__shape__'];
|
22
|
+
lineages?: ResourceLineage[];
|
23
|
+
};
|
24
|
+
|
25
|
+
export type ResourceLineage = {
|
26
|
+
resourceIds: string[];
|
22
27
|
};
|
23
28
|
|
24
29
|
/**
|
@@ -70,3 +75,7 @@ export type DeepPartial<T> = {
|
|
70
75
|
? ReadonlyArray<DeepPartial<U>>
|
71
76
|
: DeepPartial<T[P]>;
|
72
77
|
};
|
78
|
+
|
79
|
+
export type OnRequestSources = () => Promise<Source[]>;
|
80
|
+
export type OnRequestResource = (reference: ResourceReference) => Promise<Resource>;
|
81
|
+
export type OnRequestChildren = (source: Source, resource: Resource | null) => Promise<Resource[]>;
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import { Resource } from '../types';
|
2
|
+
import { findBestMatchLineage } from './findBestMatchLineage';
|
3
|
+
import { mockResource, mockSource } from '../__mocks__/MockModels';
|
4
|
+
|
5
|
+
describe('findBestMatchLineage', () => {
|
6
|
+
it.each([
|
7
|
+
[
|
8
|
+
'Resource lineage is not returned',
|
9
|
+
mockResource({
|
10
|
+
lineages: undefined,
|
11
|
+
}),
|
12
|
+
[],
|
13
|
+
],
|
14
|
+
[
|
15
|
+
'Resource has no lineages',
|
16
|
+
mockResource({
|
17
|
+
lineages: [],
|
18
|
+
}),
|
19
|
+
[],
|
20
|
+
],
|
21
|
+
[
|
22
|
+
'Resource does not have lineage which is beneath a configured root node',
|
23
|
+
mockResource({
|
24
|
+
id: '3000',
|
25
|
+
lineages: [{ resourceIds: ['1', '30', '300', '3000'] }],
|
26
|
+
}),
|
27
|
+
[],
|
28
|
+
],
|
29
|
+
[
|
30
|
+
'Resource has a lineage under one of the root nodes',
|
31
|
+
mockResource({
|
32
|
+
id: '100',
|
33
|
+
lineages: [{ resourceIds: ['1', '20', '200', '2000'] }],
|
34
|
+
}),
|
35
|
+
['20', '200', '2000'],
|
36
|
+
],
|
37
|
+
[
|
38
|
+
'Resource has a lineage beneath multiple of the root nodes',
|
39
|
+
mockResource({
|
40
|
+
id: '1000',
|
41
|
+
lineages: [{ resourceIds: ['1', '10', '100', '1000'] }, { resourceIds: ['1', '20', '200', '1000'] }],
|
42
|
+
}),
|
43
|
+
// first match is returned.
|
44
|
+
['10', '100', '1000'],
|
45
|
+
],
|
46
|
+
[
|
47
|
+
'Resource has multiple lineages under a single configured root node',
|
48
|
+
mockResource({
|
49
|
+
id: '1000',
|
50
|
+
lineages: [{ resourceIds: ['1', '10', '100', '1000'] }, { resourceIds: ['1', '10', '101', '1000'] }],
|
51
|
+
}),
|
52
|
+
// first match is returned.
|
53
|
+
['10', '100', '1000'],
|
54
|
+
],
|
55
|
+
[
|
56
|
+
'Resource ID is a configured root node',
|
57
|
+
mockResource({
|
58
|
+
id: '10',
|
59
|
+
lineages: [{ resourceIds: ['1', '10'] }],
|
60
|
+
}),
|
61
|
+
[],
|
62
|
+
],
|
63
|
+
[
|
64
|
+
'Resource ID is a configured root node and also nested under a root node',
|
65
|
+
mockResource({
|
66
|
+
id: '10',
|
67
|
+
lineages: [{ resourceIds: ['1', '10'] }, { resourceIds: ['1', '20', '10'] }],
|
68
|
+
}),
|
69
|
+
['20', '10'],
|
70
|
+
],
|
71
|
+
])(
|
72
|
+
'Returns best match lineage which can be identified from provided source - %s',
|
73
|
+
(description: string, resource: Resource, expected: string[]) => {
|
74
|
+
const source = mockSource({
|
75
|
+
nodes: [mockResource({ id: '10' }), mockResource({ id: '20' })],
|
76
|
+
});
|
77
|
+
|
78
|
+
expect(findBestMatchLineage(source, resource)).toEqual(expected);
|
79
|
+
},
|
80
|
+
);
|
81
|
+
});
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { Source, Resource } from '../types';
|
2
|
+
|
3
|
+
export const findBestMatchLineage = (source: Source, resource: Resource): string[] => {
|
4
|
+
if (resource.lineages) {
|
5
|
+
for (const lineage of resource.lineages) {
|
6
|
+
// Lineage must:
|
7
|
+
// 1. Appear beneath the root node.
|
8
|
+
// 2. Not be the root node itself (root nodes can't be selected, to be changed in FEAAS-760).
|
9
|
+
// TODO: FEAAS-760 update as necessary so the lineage will be returned even if it ends at the root node, eg:
|
10
|
+
// const rootNode = source.nodes.find(node => lineage.resourceIds.includes(node.id));
|
11
|
+
const rootNode = source.nodes.find((node) => {
|
12
|
+
const index = lineage.resourceIds.indexOf(node.id);
|
13
|
+
|
14
|
+
return index >= 0 && index < lineage.resourceIds.length - 1;
|
15
|
+
});
|
16
|
+
|
17
|
+
if (rootNode) {
|
18
|
+
const rootNodeIndex = lineage.resourceIds.indexOf(rootNode.id);
|
19
|
+
|
20
|
+
// Return the lineage starting from the root node. eg.
|
21
|
+
// * Full lineage is: 1 > 10 > 100 > 1000 > 10000
|
22
|
+
// * The source has a node with an ID of: 100
|
23
|
+
// * The returned lineage will be: 100 > 1000 > 10000
|
24
|
+
return lineage.resourceIds.slice(rootNodeIndex);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
return [];
|
30
|
+
};
|
@@ -1,20 +0,0 @@
|
|
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
|
-
});
|
File without changes
|
File without changes
|
File without changes
|