@squiz/resource-browser 1.69.1 → 2.1.8-rc.0
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 +88 -35
- package/LICENSE.md +15 -0
- package/README.md +9 -0
- package/jest.config.ts +22 -21
- package/lib/Hooks/useSelectedState.d.ts +15 -0
- package/lib/Hooks/useSelectedState.js +16 -0
- package/lib/Hooks/useSources.d.ts +5 -4
- package/lib/Hooks/useSources.js +25 -1
- package/lib/MainContainer/MainContainer.d.ts +17 -0
- package/lib/MainContainer/MainContainer.js +61 -0
- package/lib/Plugin/Plugin.d.ts +13 -0
- package/lib/Plugin/Plugin.js +17 -0
- package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +2 -3
- package/lib/ResourceBrowserContext/ResourceBrowserContext.js +4 -17
- package/lib/ResourceBrowserInput/ResourceBrowserInput.d.ts +24 -0
- package/lib/ResourceBrowserInput/ResourceBrowserInput.js +16 -0
- package/lib/ResourcePicker/ResourcePicker.d.ts +6 -4
- package/lib/ResourcePicker/ResourcePicker.js +14 -8
- package/lib/ResourcePicker/States/Selected.d.ts +10 -4
- package/lib/ResourcePicker/States/Selected.js +11 -32
- package/lib/SourceDropdown/SourceDropdown.d.ts +5 -11
- package/lib/SourceDropdown/SourceDropdown.js +20 -99
- package/lib/SourceList/SourceList.d.ts +5 -16
- package/lib/SourceList/SourceList.js +14 -75
- package/lib/index.css +42 -202
- package/lib/index.d.ts +7 -7
- package/lib/index.js +69 -13
- package/lib/types.d.ts +41 -59
- package/package.json +82 -80
- package/src/Hooks/useSelectedState.spec.ts +46 -0
- package/src/Hooks/useSelectedState.ts +22 -0
- package/src/Hooks/useSources.spec.ts +30 -12
- package/src/Hooks/useSources.ts +33 -4
- package/src/Icons/CircledLoopIcon.tsx +8 -8
- package/src/MainContainer/MainContainer.spec.tsx +203 -0
- package/src/MainContainer/MainContainer.stories.tsx +62 -0
- package/src/MainContainer/MainContainer.tsx +101 -0
- package/src/Plugin/Plugin.spec.tsx +46 -0
- package/src/Plugin/Plugin.tsx +20 -0
- package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +65 -106
- package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +24 -39
- package/src/ResourceBrowserInput/ResourceBrowserInput.spec.tsx +192 -0
- package/src/ResourceBrowserInput/ResourceBrowserInput.tsx +81 -0
- package/src/ResourcePicker/ResourcePicker.spec.tsx +159 -116
- package/src/ResourcePicker/ResourcePicker.stories.tsx +28 -24
- package/src/ResourcePicker/ResourcePicker.tsx +79 -59
- package/src/ResourcePicker/States/Error.tsx +8 -8
- package/src/ResourcePicker/States/Loading.tsx +3 -3
- package/src/ResourcePicker/States/Selected.tsx +66 -73
- package/src/ResourcePicker/mock-image-resource.json +25 -47
- package/src/ResourcePicker/mock-resource.json +11 -13
- package/src/ResourcePicker/resource-picker.scss +13 -13
- package/src/SourceDropdown/SourceDropdown.spec.tsx +65 -391
- package/src/SourceDropdown/SourceDropdown.stories.tsx +21 -24
- package/src/SourceDropdown/SourceDropdown.tsx +80 -258
- package/src/SourceList/SourceList.spec.tsx +37 -430
- package/src/SourceList/SourceList.stories.tsx +17 -37
- package/src/SourceList/SourceList.tsx +28 -155
- package/src/__mocks__/MockModels.ts +56 -25
- package/src/__mocks__/PluginExample.tsx +98 -0
- package/src/__mocks__/StorybookHelpers.tsx +141 -0
- package/src/__mocks__/renderWithContext.tsx +14 -18
- package/src/__mocks__/sample-sources.json +32 -0
- package/src/index.scss +18 -8
- package/src/index.spec.tsx +277 -99
- package/src/index.stories.tsx +65 -39
- package/src/index.tsx +119 -57
- package/src/types.ts +54 -63
- package/tailwind.config.cjs +92 -92
- package/vite.config.js +12 -12
- package/lib/Hooks/useCategorisedSources.d.ts +0 -14
- package/lib/Hooks/useCategorisedSources.js +0 -38
- package/lib/Hooks/useChildResources.d.ts +0 -16
- package/lib/Hooks/useChildResources.js +0 -13
- package/lib/Hooks/usePreselectedResourcePath.d.ts +0 -20
- package/lib/Hooks/usePreselectedResourcePath.js +0 -31
- package/lib/Hooks/useRecentLocations.d.ts +0 -5
- package/lib/Hooks/useRecentLocations.js +0 -38
- package/lib/Hooks/useRecentResourcesPaths.d.ts +0 -20
- package/lib/Hooks/useRecentResourcesPaths.js +0 -30
- package/lib/Hooks/useResource.d.ts +0 -28
- package/lib/Hooks/useResource.js +0 -23
- package/lib/Hooks/useResourcePath.d.ts +0 -16
- package/lib/Hooks/useResourcePath.js +0 -64
- package/lib/Icons/HistoryIcon.d.ts +0 -4
- package/lib/Icons/HistoryIcon.js +0 -13
- package/lib/PreviewPanel/PreviewPanel.d.ts +0 -5
- package/lib/PreviewPanel/PreviewPanel.js +0 -8
- package/lib/PreviewPanel/details/MatrixResource.d.ts +0 -7
- package/lib/PreviewPanel/details/MatrixResource.js +0 -35
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +0 -9
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +0 -54
- package/lib/ResourceList/ResourceList.d.ts +0 -18
- package/lib/ResourceList/ResourceList.js +0 -49
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +0 -17
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +0 -166
- package/lib/StatusIndicator/StatusIndicator.d.ts +0 -8
- package/lib/StatusIndicator/StatusIndicator.js +0 -27
- package/lib/utils/findBestMatchLineage.d.ts +0 -2
- package/lib/utils/findBestMatchLineage.js +0 -28
- package/lib/utils/uuid.d.ts +0 -1
- package/lib/utils/uuid.js +0 -6
- package/src/Hooks/useCategorisedSources.spec.ts +0 -39
- package/src/Hooks/useCategorisedSources.ts +0 -46
- package/src/Hooks/useChildResources.spec.ts +0 -29
- package/src/Hooks/useChildResources.ts +0 -21
- package/src/Hooks/usePreselectedResourcePath.ts +0 -54
- package/src/Hooks/useRecentLocations.spec.ts +0 -81
- package/src/Hooks/useRecentLocations.ts +0 -44
- package/src/Hooks/useRecentResourcesPaths.ts +0 -54
- package/src/Hooks/useResource.spec.ts +0 -61
- package/src/Hooks/useResource.ts +0 -38
- package/src/Hooks/useResourcePath.spec.ts +0 -120
- package/src/Hooks/useResourcePath.ts +0 -76
- package/src/Icons/HistoryIcon.tsx +0 -17
- package/src/PreviewPanel/PreviewPanel.spec.tsx +0 -198
- package/src/PreviewPanel/PreviewPanel.stories.tsx +0 -76
- package/src/PreviewPanel/PreviewPanel.tsx +0 -6
- package/src/PreviewPanel/details/MatrixResource.tsx +0 -54
- package/src/PreviewPanel/details/matrix-resource.scss +0 -16
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +0 -133
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +0 -24
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +0 -79
- package/src/ResourceBreadcrumb/resource-breadcrumb.scss +0 -28
- package/src/ResourceBreadcrumb/sample-hierarchy.json +0 -27
- package/src/ResourceList/ResourceList.spec.tsx +0 -202
- package/src/ResourceList/ResourceList.stories.tsx +0 -40
- package/src/ResourceList/ResourceList.tsx +0 -83
- package/src/ResourceList/sample-resources.json +0 -851
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +0 -780
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +0 -45
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +0 -290
- package/src/SourceList/sample-sources.json +0 -251
- package/src/StatusIndicator/StatusIndicator.stories.tsx +0 -83
- package/src/StatusIndicator/StatusIndicator.tsx +0 -38
- package/src/__mocks__/JestHelpers.ts +0 -65
- package/src/__mocks__/StorybookHelpers.ts +0 -128
- package/src/__mocks__/jestHelpers.spec.ts +0 -38
- package/src/utils/findBestMatchLineage.spec.ts +0 -81
- package/src/utils/findBestMatchLineage.ts +0 -30
- package/src/utils/uuid.ts +0 -5
@@ -0,0 +1,203 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
3
|
+
import MainContainer from './MainContainer';
|
4
|
+
import { ResourceBrowserPlugin, ResourceBrowserSource } from '../types';
|
5
|
+
|
6
|
+
import { mockSource, mockResource } from '../__mocks__/MockModels';
|
7
|
+
|
8
|
+
import SourceList from '../SourceList/SourceList'; // Import Functional Component
|
9
|
+
jest.mock('../SourceList/SourceList'); // Mock the Functional Component
|
10
|
+
const ActualSourceList = jest.requireActual('../SourceList/SourceList').default; // Grab the real copy of it as normally we don't want the mock
|
11
|
+
const MockSourceList = SourceList as jest.MockedFunction<typeof SourceList>; // Cast the mocked function so TS stops complaining
|
12
|
+
MockSourceList.mockImplementation(ActualSourceList); // Return the actual function unless overridden
|
13
|
+
|
14
|
+
import SourceDropdown from '../SourceDropdown/SourceDropdown'; // Import Functional Component
|
15
|
+
jest.mock('../SourceDropdown/SourceDropdown'); // Mock the Functional Component
|
16
|
+
const ActualSourceDropdown = jest.requireActual('../SourceDropdown/SourceDropdown').default; // Grab the real copy of it as normally we don't want the mock
|
17
|
+
const MockSourceDropdown = SourceDropdown as jest.MockedFunction<typeof SourceDropdown>; // Cast the mocked function so TS stops complaining
|
18
|
+
MockSourceDropdown.mockImplementation(ActualSourceDropdown); // Return the actual function unless overridden
|
19
|
+
|
20
|
+
const mockSourceBrowserComponent = jest.fn();
|
21
|
+
const defaultProps = {
|
22
|
+
title: '',
|
23
|
+
titleAriaProps: {},
|
24
|
+
allowedTypes: undefined,
|
25
|
+
sources: [],
|
26
|
+
selectedSource: null,
|
27
|
+
onSourceSelect: jest.fn(),
|
28
|
+
onChange: jest.fn(),
|
29
|
+
onClose: jest.fn(),
|
30
|
+
preselectedResource: null,
|
31
|
+
plugin: null,
|
32
|
+
};
|
33
|
+
|
34
|
+
describe('MainContainer', () => {
|
35
|
+
it('clicking close button should call the onClose callback', async () => {
|
36
|
+
const onClose = jest.fn();
|
37
|
+
|
38
|
+
render(<MainContainer {...defaultProps} title="Select Asset" onClose={onClose} />);
|
39
|
+
|
40
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Close Select Asset dialog' })));
|
41
|
+
expect(onClose).toHaveBeenCalled();
|
42
|
+
});
|
43
|
+
|
44
|
+
describe('no source selected', () => {
|
45
|
+
it('should render Environment Selector title', () => {
|
46
|
+
render(<MainContainer {...defaultProps} title="Select MyAsset" />);
|
47
|
+
expect(screen.getByText('Environment Selector')).toBeInTheDocument();
|
48
|
+
expect(screen.queryByText('Select MyAsset')).not.toBeInTheDocument();
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should not render source selector dropdown', () => {
|
52
|
+
render(<MainContainer {...defaultProps} />);
|
53
|
+
expect(MockSourceDropdown).not.toHaveBeenCalled();
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should not render plugin UI', () => {
|
57
|
+
render(<MainContainer {...defaultProps} />);
|
58
|
+
expect(mockSourceBrowserComponent).not.toHaveBeenCalled();
|
59
|
+
});
|
60
|
+
|
61
|
+
it('should render source list using provided props', () => {
|
62
|
+
const sources: ResourceBrowserSource[] = [];
|
63
|
+
const onSourceSelect = jest.fn();
|
64
|
+
render(<MainContainer {...defaultProps} sources={sources} onSourceSelect={onSourceSelect} />);
|
65
|
+
expect(MockSourceList).toHaveBeenCalledWith({ sources, onSourceSelect }, {});
|
66
|
+
});
|
67
|
+
});
|
68
|
+
|
69
|
+
describe('source selected', () => {
|
70
|
+
const source = mockSource({ type: 'dam' });
|
71
|
+
const plugin = {
|
72
|
+
type: 'dam',
|
73
|
+
createHeaderPortal: false,
|
74
|
+
sourceBrowserComponent: mockSourceBrowserComponent,
|
75
|
+
renderSelectedResource: jest.fn(),
|
76
|
+
useResolveResource: jest.fn(),
|
77
|
+
} as ResourceBrowserPlugin;
|
78
|
+
it('should render Environment Selector title', () => {
|
79
|
+
render(<MainContainer {...defaultProps} title="Select MyAsset" selectedSource={source} plugin={plugin} />);
|
80
|
+
expect(screen.getByText('Select MyAsset')).toBeInTheDocument();
|
81
|
+
expect(screen.queryByText('Environment Selector')).not.toBeInTheDocument();
|
82
|
+
});
|
83
|
+
|
84
|
+
it('should render source selector dropdown if more than one source exists', () => {
|
85
|
+
const sources: ResourceBrowserSource[] = [mockSource(), mockSource()];
|
86
|
+
const onSourceSelect = jest.fn();
|
87
|
+
render(
|
88
|
+
<MainContainer
|
89
|
+
{...defaultProps}
|
90
|
+
sources={sources}
|
91
|
+
selectedSource={source}
|
92
|
+
plugin={plugin}
|
93
|
+
onSourceSelect={onSourceSelect}
|
94
|
+
/>,
|
95
|
+
);
|
96
|
+
expect(MockSourceDropdown).toHaveBeenCalledWith({ sources, selectedSource: source, onSourceSelect }, {});
|
97
|
+
});
|
98
|
+
|
99
|
+
it('should not render source selector dropdown if one source exists', () => {
|
100
|
+
const sources: ResourceBrowserSource[] = [mockSource()];
|
101
|
+
const onSourceSelect = jest.fn();
|
102
|
+
render(
|
103
|
+
<MainContainer
|
104
|
+
{...defaultProps}
|
105
|
+
sources={sources}
|
106
|
+
selectedSource={source}
|
107
|
+
plugin={plugin}
|
108
|
+
onSourceSelect={onSourceSelect}
|
109
|
+
/>,
|
110
|
+
);
|
111
|
+
expect(MockSourceDropdown).not.toHaveBeenCalled();
|
112
|
+
});
|
113
|
+
|
114
|
+
it('should create header portal for plugin', async () => {
|
115
|
+
const mockFunctionalComponent = jest.fn().mockReturnValue(<div>Source UI has been rendered</div>);
|
116
|
+
const mockSourceBrowserComponent = jest.fn().mockReturnValue(mockFunctionalComponent);
|
117
|
+
const plugin = {
|
118
|
+
type: 'dam',
|
119
|
+
createHeaderPortal: true,
|
120
|
+
sourceBrowserComponent: mockSourceBrowserComponent,
|
121
|
+
renderSelectedResource: jest.fn(),
|
122
|
+
useResolveResource: jest.fn(),
|
123
|
+
} as ResourceBrowserPlugin;
|
124
|
+
render(<MainContainer {...defaultProps} selectedSource={source} plugin={plugin} />);
|
125
|
+
expect(mockSourceBrowserComponent).toHaveBeenCalled();
|
126
|
+
|
127
|
+
await waitFor(() => {
|
128
|
+
expect(mockFunctionalComponent).toHaveBeenCalledWith(
|
129
|
+
expect.objectContaining({
|
130
|
+
headerPortal: expect.any(Element),
|
131
|
+
}),
|
132
|
+
{},
|
133
|
+
);
|
134
|
+
});
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should pass expected params to plugin UI generator functional component', async () => {
|
138
|
+
const mockFunctionalComponent = jest.fn().mockReturnValue(<div>Source UI has been rendered</div>);
|
139
|
+
const mockSourceBrowserComponent = jest.fn().mockReturnValue(mockFunctionalComponent);
|
140
|
+
const plugin = {
|
141
|
+
type: 'dam',
|
142
|
+
createHeaderPortal: true,
|
143
|
+
sourceBrowserComponent: mockSourceBrowserComponent,
|
144
|
+
renderSelectedResource: jest.fn(),
|
145
|
+
useResolveResource: jest.fn(),
|
146
|
+
} as ResourceBrowserPlugin;
|
147
|
+
|
148
|
+
const resource = mockResource();
|
149
|
+
const props = {
|
150
|
+
allowedTypes: ['image'],
|
151
|
+
preselectedResource: resource,
|
152
|
+
};
|
153
|
+
|
154
|
+
render(<MainContainer {...defaultProps} selectedSource={source} plugin={plugin} {...props} />);
|
155
|
+
expect(mockSourceBrowserComponent).toHaveBeenCalled();
|
156
|
+
|
157
|
+
await waitFor(() => {
|
158
|
+
expect(mockFunctionalComponent).toHaveBeenCalledWith(
|
159
|
+
expect.objectContaining({
|
160
|
+
source: source,
|
161
|
+
allowedTypes: props.allowedTypes,
|
162
|
+
headerPortal: expect.any(Element),
|
163
|
+
preselectedResource: resource,
|
164
|
+
}),
|
165
|
+
{},
|
166
|
+
);
|
167
|
+
});
|
168
|
+
});
|
169
|
+
|
170
|
+
it('should pass onSelected to plugin UI generator FC when invoked calls change and close functions', async () => {
|
171
|
+
const mockFunctionalComponent = jest.fn().mockReturnValue(<div>Source UI has been rendered</div>);
|
172
|
+
const mockSourceBrowserComponent = jest.fn().mockReturnValue(mockFunctionalComponent);
|
173
|
+
const plugin = {
|
174
|
+
type: 'dam',
|
175
|
+
createHeaderPortal: true,
|
176
|
+
sourceBrowserComponent: mockSourceBrowserComponent,
|
177
|
+
renderSelectedResource: jest.fn(),
|
178
|
+
useResolveResource: jest.fn(),
|
179
|
+
} as ResourceBrowserPlugin;
|
180
|
+
|
181
|
+
const onChange = jest.fn();
|
182
|
+
const onClose = jest.fn();
|
183
|
+
|
184
|
+
render(<MainContainer {...defaultProps} selectedSource={source} plugin={plugin} onChange={onChange} onClose={onClose} />);
|
185
|
+
expect(mockSourceBrowserComponent).toHaveBeenCalled();
|
186
|
+
|
187
|
+
await waitFor(() => {
|
188
|
+
expect(mockFunctionalComponent).toHaveBeenCalledWith(
|
189
|
+
expect.objectContaining({
|
190
|
+
onSelected: expect.any(Function),
|
191
|
+
}),
|
192
|
+
{},
|
193
|
+
);
|
194
|
+
});
|
195
|
+
|
196
|
+
const resource = mockResource();
|
197
|
+
const { onSelected } = mockFunctionalComponent.mock.calls[0][0];
|
198
|
+
onSelected(resource);
|
199
|
+
expect(onChange).toHaveBeenCalledWith(resource);
|
200
|
+
expect(onClose).toHaveBeenCalled();
|
201
|
+
});
|
202
|
+
});
|
203
|
+
});
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
import MainContainer from './MainContainer';
|
4
|
+
import sampleSources from '../__mocks__/sample-sources.json';
|
5
|
+
import { createPlugins } from '../__mocks__/StorybookHelpers';
|
6
|
+
import { ResourceBrowserSource, ResourceBrowserPlugin } from '../types';
|
7
|
+
|
8
|
+
export default {
|
9
|
+
title: 'Main container',
|
10
|
+
component: MainContainer,
|
11
|
+
} as Meta<typeof MainContainer>;
|
12
|
+
|
13
|
+
const Template: StoryFn<typeof MainContainer> = (props) => {
|
14
|
+
const sources = sampleSources as ResourceBrowserSource[];
|
15
|
+
const plugins: [ResourceBrowserPlugin] = createPlugins(0, true);
|
16
|
+
|
17
|
+
const [selectedSource, setSelectedSource] = useState<ResourceBrowserSource | null>(props.selectedSource ?? null);
|
18
|
+
|
19
|
+
return (
|
20
|
+
<div className="flex justify-center m-3">
|
21
|
+
<MainContainer
|
22
|
+
title={props.title}
|
23
|
+
titleAriaProps={props.titleAriaProps}
|
24
|
+
allowedTypes={props.allowedTypes}
|
25
|
+
sources={sources}
|
26
|
+
selectedSource={selectedSource}
|
27
|
+
onSourceSelect={(source) => setSelectedSource(source)}
|
28
|
+
onChange={(resource) => {
|
29
|
+
if (resource) {
|
30
|
+
console.log(`Resource ${resource?.name} / ${resource.id} was selected`);
|
31
|
+
} else {
|
32
|
+
console.log(`No resource was selected`);
|
33
|
+
}
|
34
|
+
}}
|
35
|
+
onClose={() => {
|
36
|
+
console.log('onClose called');
|
37
|
+
}}
|
38
|
+
plugin={plugins[0]}
|
39
|
+
></MainContainer>
|
40
|
+
</div>
|
41
|
+
);
|
42
|
+
};
|
43
|
+
|
44
|
+
export const NoSourceSelected = Template.bind({});
|
45
|
+
|
46
|
+
NoSourceSelected.args = {
|
47
|
+
title: 'Main Container',
|
48
|
+
titleAriaProps: {},
|
49
|
+
allowedTypes: [],
|
50
|
+
selectedSource: null,
|
51
|
+
};
|
52
|
+
|
53
|
+
export const SourceSelected = Template.bind({});
|
54
|
+
SourceSelected.args = {
|
55
|
+
...NoSourceSelected.args,
|
56
|
+
selectedSource: {
|
57
|
+
name: 'Bynder #1',
|
58
|
+
id: 'c90feac1-55f3-4e1f-9b56-c22829e3f510',
|
59
|
+
type: 'dam',
|
60
|
+
group: 'DAM',
|
61
|
+
},
|
62
|
+
};
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
2
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
|
+
|
4
|
+
import SourceDropdown from '../SourceDropdown/SourceDropdown';
|
5
|
+
import SourceList from '../SourceList/SourceList';
|
6
|
+
import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserResource } from '../types';
|
7
|
+
|
8
|
+
interface MainContainerProps {
|
9
|
+
title: string;
|
10
|
+
titleAriaProps: DOMAttributes<FocusableElement>;
|
11
|
+
allowedTypes: string[] | undefined;
|
12
|
+
sources: ResourceBrowserSource[];
|
13
|
+
selectedSource: ResourceBrowserSource | null;
|
14
|
+
onSourceSelect(source: ResourceBrowserSource | null): void;
|
15
|
+
onChange(resource: ResourceBrowserResource | null): void;
|
16
|
+
onClose: () => void;
|
17
|
+
preselectedResource?: ResourceBrowserResource | null;
|
18
|
+
plugin: ResourceBrowserPlugin | null;
|
19
|
+
}
|
20
|
+
|
21
|
+
function MainContainer({
|
22
|
+
title,
|
23
|
+
titleAriaProps,
|
24
|
+
allowedTypes,
|
25
|
+
sources,
|
26
|
+
selectedSource,
|
27
|
+
onSourceSelect,
|
28
|
+
onChange,
|
29
|
+
onClose,
|
30
|
+
preselectedResource,
|
31
|
+
plugin,
|
32
|
+
}: MainContainerProps) {
|
33
|
+
const [headerPortal, setHeaderPortal] = useState<HTMLDivElement | null>(null);
|
34
|
+
const SourceBrowser = plugin?.sourceBrowserComponent();
|
35
|
+
|
36
|
+
// Can't use a useRef as it wont update on change when a source is selected, so need to use a ref callback to store in state
|
37
|
+
const setHeaderPortalRef = useCallback(
|
38
|
+
(node: HTMLDivElement) => {
|
39
|
+
if (node !== null) {
|
40
|
+
setHeaderPortal(node);
|
41
|
+
}
|
42
|
+
},
|
43
|
+
[setHeaderPortal],
|
44
|
+
);
|
45
|
+
|
46
|
+
// MainContainer will either render the source list view if no source is set or the plugins UI if a source has been selected
|
47
|
+
return (
|
48
|
+
<div className="relative flex flex-col h-full text-gray-800">
|
49
|
+
<div className="flex items-center p-4.5">
|
50
|
+
<h2 {...titleAriaProps} className="text-xl leading-6 text-gray-800 font-semibold mr-6">
|
51
|
+
{!plugin && 'Environment Selector'}
|
52
|
+
{plugin && title}
|
53
|
+
</h2>
|
54
|
+
|
55
|
+
{plugin && selectedSource && (
|
56
|
+
<>
|
57
|
+
{sources.length > 1 && (
|
58
|
+
<div className="px-3 border-l border-gray-300 w-300px">
|
59
|
+
<SourceDropdown sources={sources} selectedSource={selectedSource} onSourceSelect={onSourceSelect} />
|
60
|
+
</div>
|
61
|
+
)}
|
62
|
+
{plugin.createHeaderPortal && (
|
63
|
+
<div ref={setHeaderPortalRef} className="px-3 border-l border-gray-300 w-300px"></div>
|
64
|
+
)}
|
65
|
+
</>
|
66
|
+
)}
|
67
|
+
|
68
|
+
<button
|
69
|
+
type="button"
|
70
|
+
aria-label={`Close ${title} dialog`}
|
71
|
+
onClick={onClose}
|
72
|
+
className="absolute top-2 right-2 p-2.5 rounded hover:bg-blue-100 focus:bg-blue-100"
|
73
|
+
>
|
74
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
75
|
+
<path
|
76
|
+
d="M13.3 0.710017C13.1131 0.522765 12.8595 0.417532 12.595 0.417532C12.3305 0.417532 12.0768 0.522765 11.89 0.710017L6.99997 5.59002L2.10997 0.700017C1.92314 0.512765 1.66949 0.407532 1.40497 0.407532C1.14045 0.407532 0.886802 0.512765 0.699971 0.700017C0.309971 1.09002 0.309971 1.72002 0.699971 2.11002L5.58997 7.00002L0.699971 11.89C0.309971 12.28 0.309971 12.91 0.699971 13.3C1.08997 13.69 1.71997 13.69 2.10997 13.3L6.99997 8.41002L11.89 13.3C12.28 13.69 12.91 13.69 13.3 13.3C13.69 12.91 13.69 12.28 13.3 11.89L8.40997 7.00002L13.3 2.11002C13.68 1.73002 13.68 1.09002 13.3 0.710017Z"
|
77
|
+
fill="currentColor"
|
78
|
+
/>
|
79
|
+
</svg>
|
80
|
+
</button>
|
81
|
+
</div>
|
82
|
+
<div className="border-t border-gray-300 h-[calc(100%-72px)]">
|
83
|
+
{plugin && selectedSource && SourceBrowser && (
|
84
|
+
<SourceBrowser
|
85
|
+
source={selectedSource}
|
86
|
+
allowedTypes={allowedTypes}
|
87
|
+
headerPortal={plugin.createHeaderPortal && headerPortal ? headerPortal : undefined}
|
88
|
+
preselectedResource={preselectedResource || undefined}
|
89
|
+
onSelected={(resource: ResourceBrowserResource) => {
|
90
|
+
onChange(resource);
|
91
|
+
onClose();
|
92
|
+
}}
|
93
|
+
/>
|
94
|
+
)}
|
95
|
+
{!selectedSource && <SourceList sources={sources} onSourceSelect={onSourceSelect} />}
|
96
|
+
</div>
|
97
|
+
</div>
|
98
|
+
);
|
99
|
+
}
|
100
|
+
|
101
|
+
export default MainContainer;
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
3
|
+
|
4
|
+
import { PluginRender } from './Plugin';
|
5
|
+
import * as RBI from '../ResourceBrowserInput/ResourceBrowserInput';
|
6
|
+
jest.spyOn(RBI, 'ResourceBrowserInput');
|
7
|
+
|
8
|
+
describe('Plugin', () => {
|
9
|
+
it('Does not render ResourceBrowserInput if render is false', async () => {
|
10
|
+
//@ts-ignore
|
11
|
+
render(<PluginRender render={false} />);
|
12
|
+
|
13
|
+
await waitFor(() => {
|
14
|
+
expect(RBI.ResourceBrowserInput).not.toHaveBeenCalled();
|
15
|
+
});
|
16
|
+
});
|
17
|
+
|
18
|
+
it('Does render ResourceBrowserInput if render is true', async () => {
|
19
|
+
const props = {
|
20
|
+
modalTitle: 'Asset picker',
|
21
|
+
value: null,
|
22
|
+
onChange: jest.fn(),
|
23
|
+
onClear: jest.fn(),
|
24
|
+
useResource: () => {
|
25
|
+
return {
|
26
|
+
data: null,
|
27
|
+
error: null,
|
28
|
+
isLoading: false,
|
29
|
+
};
|
30
|
+
},
|
31
|
+
plugin: null,
|
32
|
+
source: null,
|
33
|
+
sources: [],
|
34
|
+
isLoading: false,
|
35
|
+
error: null,
|
36
|
+
setSource: () => {},
|
37
|
+
isModalOpen: false,
|
38
|
+
onModalStateChange: () => {},
|
39
|
+
};
|
40
|
+
render(<PluginRender render={true} {...props} />);
|
41
|
+
|
42
|
+
await waitFor(() => {
|
43
|
+
expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(props, {});
|
44
|
+
});
|
45
|
+
});
|
46
|
+
});
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* This plugin component exsits to deal with React rules of Hooks stupidity.
|
6
|
+
*
|
7
|
+
* For it to not freak out when we want to change from one plugin to another we have to render
|
8
|
+
* something to the ReactDom for each plugin and 'activate' that component when that plugin
|
9
|
+
* needs to render its UI etc.
|
10
|
+
*/
|
11
|
+
export type PluginRenderType = ResourceBrowserInputProps & {
|
12
|
+
render: boolean;
|
13
|
+
};
|
14
|
+
export const PluginRender = ({ render, ...props }: PluginRenderType) => {
|
15
|
+
if (render) {
|
16
|
+
return <ResourceBrowserInput {...props} />;
|
17
|
+
} else {
|
18
|
+
return <></>;
|
19
|
+
}
|
20
|
+
};
|
@@ -1,122 +1,81 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { render } from '@testing-library/react';
|
3
|
-
import {
|
4
|
-
|
5
|
-
ResourceBrowserContextProps,
|
6
|
-
ResourceBrowserContextProvider,
|
7
|
-
} from './ResourceBrowserContext';
|
8
|
-
import { mockResource, mockSource } from '../__mocks__/MockModels';
|
3
|
+
import { ResourceBrowserContext, ResourceBrowserContextProps, ResourceBrowserContextProvider } from './ResourceBrowserContext';
|
4
|
+
import { mockSource } from '../__mocks__/MockModels';
|
9
5
|
|
10
6
|
describe('ResourceBrowserContext', () => {
|
11
|
-
|
12
|
-
|
7
|
+
it('Should render with expected default value', () => {
|
8
|
+
let defaultContext: ResourceBrowserContextProps | undefined;
|
13
9
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
10
|
+
render(
|
11
|
+
<ResourceBrowserContext.Consumer>
|
12
|
+
{(value) => {
|
13
|
+
defaultContext = value;
|
14
|
+
return null;
|
15
|
+
}}
|
16
|
+
</ResourceBrowserContext.Consumer>,
|
17
|
+
);
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
expect(defaultContext).toEqual({
|
20
|
+
onRequestSources: expect.any(Function),
|
21
|
+
plugins: [],
|
22
|
+
});
|
23
|
+
expect(() => defaultContext?.onRequestSources()).toThrow('onRequestSources has not been configured.');
|
27
24
|
});
|
28
|
-
expect(() => defaultContext?.onRequestChildren(mockSource(), null)).toThrow(
|
29
|
-
'onRequestChildren has not been configured.',
|
30
|
-
);
|
31
|
-
expect(() => defaultContext?.onRequestResource({ source: 'source-id', resource: 'resource-id' })).toThrow(
|
32
|
-
'onRequestResource has not been configured.',
|
33
|
-
);
|
34
|
-
expect(() => defaultContext?.onRequestSources()).toThrow('onRequestSources has not been configured.');
|
35
|
-
});
|
36
25
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
42
|
-
const onRequestResource = jest.fn().mockResolvedValue(resources[0]);
|
43
|
-
const onRequestChildren = jest.fn().mockResolvedValue(resources);
|
26
|
+
it('Should add memoization to data fetching functions', async () => {
|
27
|
+
let context: ResourceBrowserContextProps | undefined;
|
28
|
+
const sources = [mockSource({ id: '10', type: 'dam' }), mockSource({ id: '20', type: 'dam' })];
|
29
|
+
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
44
30
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
);
|
31
|
+
render(
|
32
|
+
<ResourceBrowserContextProvider
|
33
|
+
value={{
|
34
|
+
onRequestSources,
|
35
|
+
plugins: [],
|
36
|
+
}}
|
37
|
+
>
|
38
|
+
<ResourceBrowserContext.Consumer>
|
39
|
+
{(value) => {
|
40
|
+
context = value;
|
41
|
+
return null;
|
42
|
+
}}
|
43
|
+
</ResourceBrowserContext.Consumer>
|
44
|
+
</ResourceBrowserContextProvider>,
|
45
|
+
);
|
61
46
|
|
62
|
-
|
63
|
-
context?.onRequestSources?.(),
|
64
|
-
context?.onRequestSources?.(),
|
65
|
-
context?.onRequestResource?.({ resource: '100', source: '10' }),
|
66
|
-
context?.onRequestResource?.({ resource: '100', source: '10' }),
|
67
|
-
context?.onRequestResource?.({ resource: '200', source: '20' }),
|
68
|
-
context?.onRequestChildren?.(sources[0], null),
|
69
|
-
context?.onRequestChildren?.(sources[0], null),
|
70
|
-
context?.onRequestChildren?.(sources[1], null),
|
71
|
-
]);
|
47
|
+
const result = await Promise.all([context?.onRequestSources?.(), context?.onRequestSources?.()]);
|
72
48
|
|
73
|
-
|
74
|
-
|
75
|
-
sources,
|
76
|
-
sources,
|
77
|
-
resources[0],
|
78
|
-
resources[0],
|
79
|
-
resources[0],
|
80
|
-
resources,
|
81
|
-
resources,
|
82
|
-
resources,
|
83
|
-
]);
|
49
|
+
// mocked data should be returned for all invocations of memoized function
|
50
|
+
expect(result).toEqual([sources, sources]);
|
84
51
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
expect(onRequestChildren).toBeCalledTimes(2);
|
89
|
-
});
|
52
|
+
// memoized function should only be called when different arguments are provided
|
53
|
+
expect(onRequestSources).toHaveBeenCalledTimes(1);
|
54
|
+
});
|
90
55
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
.fn()
|
96
|
-
.mockRejectedValueOnce(new Error('Cannot fetch sources.'))
|
97
|
-
.mockResolvedValueOnce(sources);
|
98
|
-
const onRequestResource = jest.fn();
|
99
|
-
const onRequestChildren = jest.fn();
|
56
|
+
it('Should not cache failures from data fetching functions', async () => {
|
57
|
+
let context: ResourceBrowserContextProps | undefined;
|
58
|
+
const sources = [mockSource({ id: '10' })];
|
59
|
+
const onRequestSources = jest.fn().mockRejectedValueOnce(new Error('Cannot fetch sources.')).mockResolvedValueOnce(sources);
|
100
60
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
);
|
61
|
+
render(
|
62
|
+
<ResourceBrowserContextProvider
|
63
|
+
value={{
|
64
|
+
onRequestSources,
|
65
|
+
plugins: [],
|
66
|
+
}}
|
67
|
+
>
|
68
|
+
<ResourceBrowserContext.Consumer>
|
69
|
+
{(value) => {
|
70
|
+
context = value;
|
71
|
+
return null;
|
72
|
+
}}
|
73
|
+
</ResourceBrowserContext.Consumer>
|
74
|
+
</ResourceBrowserContextProvider>,
|
75
|
+
);
|
117
76
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
77
|
+
await expect(context?.onRequestSources?.()).rejects.toThrow(new Error('Cannot fetch sources.'));
|
78
|
+
await expect(context?.onRequestSources?.()).resolves.toEqual(sources);
|
79
|
+
expect(onRequestSources).toHaveBeenCalledTimes(2);
|
80
|
+
});
|
122
81
|
});
|