@squiz/resource-browser 1.69.2 → 2.1.9-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.
Files changed (141) hide show
  1. package/CHANGELOG.md +89 -38
  2. package/LICENSE.md +15 -0
  3. package/README.md +9 -0
  4. package/jest.config.ts +22 -21
  5. package/lib/Hooks/useSelectedState.d.ts +15 -0
  6. package/lib/Hooks/useSelectedState.js +16 -0
  7. package/lib/Hooks/useSources.d.ts +6 -6
  8. package/lib/Hooks/useSources.js +26 -1
  9. package/lib/MainContainer/MainContainer.d.ts +17 -0
  10. package/lib/MainContainer/MainContainer.js +61 -0
  11. package/lib/Plugin/Plugin.d.ts +13 -0
  12. package/lib/Plugin/Plugin.js +17 -0
  13. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +2 -3
  14. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +4 -17
  15. package/lib/ResourceBrowserInput/ResourceBrowserInput.d.ts +24 -0
  16. package/lib/ResourceBrowserInput/ResourceBrowserInput.js +16 -0
  17. package/lib/ResourcePicker/ResourcePicker.d.ts +6 -4
  18. package/lib/ResourcePicker/ResourcePicker.js +14 -8
  19. package/lib/ResourcePicker/States/Selected.d.ts +10 -4
  20. package/lib/ResourcePicker/States/Selected.js +11 -32
  21. package/lib/SourceDropdown/SourceDropdown.d.ts +5 -11
  22. package/lib/SourceDropdown/SourceDropdown.js +20 -99
  23. package/lib/SourceList/SourceList.d.ts +5 -16
  24. package/lib/SourceList/SourceList.js +14 -75
  25. package/lib/index.css +42 -202
  26. package/lib/index.d.ts +7 -7
  27. package/lib/index.js +69 -13
  28. package/lib/types.d.ts +41 -59
  29. package/package.json +82 -80
  30. package/src/Hooks/useSelectedState.spec.ts +46 -0
  31. package/src/Hooks/useSelectedState.ts +22 -0
  32. package/src/Hooks/useSources.spec.ts +60 -13
  33. package/src/Hooks/useSources.ts +35 -5
  34. package/src/Icons/CircledLoopIcon.tsx +8 -8
  35. package/src/MainContainer/MainContainer.spec.tsx +203 -0
  36. package/src/MainContainer/MainContainer.stories.tsx +62 -0
  37. package/src/MainContainer/MainContainer.tsx +101 -0
  38. package/src/Plugin/Plugin.spec.tsx +46 -0
  39. package/src/Plugin/Plugin.tsx +20 -0
  40. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +65 -106
  41. package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +24 -39
  42. package/src/ResourceBrowserInput/ResourceBrowserInput.spec.tsx +192 -0
  43. package/src/ResourceBrowserInput/ResourceBrowserInput.tsx +81 -0
  44. package/src/ResourcePicker/ResourcePicker.spec.tsx +159 -116
  45. package/src/ResourcePicker/ResourcePicker.stories.tsx +28 -24
  46. package/src/ResourcePicker/ResourcePicker.tsx +79 -59
  47. package/src/ResourcePicker/States/Error.tsx +8 -8
  48. package/src/ResourcePicker/States/Loading.tsx +3 -3
  49. package/src/ResourcePicker/States/Selected.tsx +66 -73
  50. package/src/ResourcePicker/mock-image-resource.json +25 -47
  51. package/src/ResourcePicker/mock-resource.json +11 -13
  52. package/src/ResourcePicker/resource-picker.scss +13 -13
  53. package/src/SourceDropdown/SourceDropdown.spec.tsx +65 -391
  54. package/src/SourceDropdown/SourceDropdown.stories.tsx +21 -24
  55. package/src/SourceDropdown/SourceDropdown.tsx +80 -258
  56. package/src/SourceList/SourceList.spec.tsx +37 -430
  57. package/src/SourceList/SourceList.stories.tsx +17 -37
  58. package/src/SourceList/SourceList.tsx +28 -155
  59. package/src/__mocks__/MockModels.ts +56 -25
  60. package/src/__mocks__/PluginExample.tsx +98 -0
  61. package/src/__mocks__/StorybookHelpers.tsx +141 -0
  62. package/src/__mocks__/renderWithContext.tsx +14 -18
  63. package/src/__mocks__/sample-sources.json +32 -0
  64. package/src/index.scss +18 -8
  65. package/src/index.spec.tsx +277 -99
  66. package/src/index.stories.tsx +65 -39
  67. package/src/index.tsx +119 -57
  68. package/src/types.ts +54 -63
  69. package/tailwind.config.cjs +92 -92
  70. package/vite.config.js +12 -12
  71. package/lib/Hooks/useCategorisedSources.d.ts +0 -14
  72. package/lib/Hooks/useCategorisedSources.js +0 -38
  73. package/lib/Hooks/useChildResources.d.ts +0 -16
  74. package/lib/Hooks/useChildResources.js +0 -13
  75. package/lib/Hooks/usePreselectedResourcePath.d.ts +0 -20
  76. package/lib/Hooks/usePreselectedResourcePath.js +0 -31
  77. package/lib/Hooks/useRecentLocations.d.ts +0 -5
  78. package/lib/Hooks/useRecentLocations.js +0 -38
  79. package/lib/Hooks/useRecentResourcesPaths.d.ts +0 -20
  80. package/lib/Hooks/useRecentResourcesPaths.js +0 -30
  81. package/lib/Hooks/useResource.d.ts +0 -28
  82. package/lib/Hooks/useResource.js +0 -25
  83. package/lib/Hooks/useResourcePath.d.ts +0 -16
  84. package/lib/Hooks/useResourcePath.js +0 -64
  85. package/lib/Icons/HistoryIcon.d.ts +0 -4
  86. package/lib/Icons/HistoryIcon.js +0 -13
  87. package/lib/PreviewPanel/PreviewPanel.d.ts +0 -5
  88. package/lib/PreviewPanel/PreviewPanel.js +0 -8
  89. package/lib/PreviewPanel/details/MatrixResource.d.ts +0 -7
  90. package/lib/PreviewPanel/details/MatrixResource.js +0 -35
  91. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +0 -9
  92. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +0 -54
  93. package/lib/ResourceList/ResourceList.d.ts +0 -18
  94. package/lib/ResourceList/ResourceList.js +0 -49
  95. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +0 -17
  96. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +0 -166
  97. package/lib/StatusIndicator/StatusIndicator.d.ts +0 -8
  98. package/lib/StatusIndicator/StatusIndicator.js +0 -27
  99. package/lib/utils/findBestMatchLineage.d.ts +0 -2
  100. package/lib/utils/findBestMatchLineage.js +0 -28
  101. package/lib/utils/uuid.d.ts +0 -1
  102. package/lib/utils/uuid.js +0 -6
  103. package/src/Hooks/useCategorisedSources.spec.ts +0 -39
  104. package/src/Hooks/useCategorisedSources.ts +0 -46
  105. package/src/Hooks/useChildResources.spec.ts +0 -29
  106. package/src/Hooks/useChildResources.ts +0 -21
  107. package/src/Hooks/usePreselectedResourcePath.ts +0 -54
  108. package/src/Hooks/useRecentLocations.spec.ts +0 -81
  109. package/src/Hooks/useRecentLocations.ts +0 -44
  110. package/src/Hooks/useRecentResourcesPaths.ts +0 -54
  111. package/src/Hooks/useResource.spec.ts +0 -61
  112. package/src/Hooks/useResource.ts +0 -40
  113. package/src/Hooks/useResourcePath.spec.ts +0 -120
  114. package/src/Hooks/useResourcePath.ts +0 -76
  115. package/src/Icons/HistoryIcon.tsx +0 -17
  116. package/src/PreviewPanel/PreviewPanel.spec.tsx +0 -198
  117. package/src/PreviewPanel/PreviewPanel.stories.tsx +0 -76
  118. package/src/PreviewPanel/PreviewPanel.tsx +0 -6
  119. package/src/PreviewPanel/details/MatrixResource.tsx +0 -54
  120. package/src/PreviewPanel/details/matrix-resource.scss +0 -16
  121. package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +0 -133
  122. package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +0 -24
  123. package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +0 -79
  124. package/src/ResourceBreadcrumb/resource-breadcrumb.scss +0 -28
  125. package/src/ResourceBreadcrumb/sample-hierarchy.json +0 -27
  126. package/src/ResourceList/ResourceList.spec.tsx +0 -202
  127. package/src/ResourceList/ResourceList.stories.tsx +0 -40
  128. package/src/ResourceList/ResourceList.tsx +0 -83
  129. package/src/ResourceList/sample-resources.json +0 -851
  130. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +0 -780
  131. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +0 -45
  132. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +0 -290
  133. package/src/SourceList/sample-sources.json +0 -251
  134. package/src/StatusIndicator/StatusIndicator.stories.tsx +0 -83
  135. package/src/StatusIndicator/StatusIndicator.tsx +0 -38
  136. package/src/__mocks__/JestHelpers.ts +0 -65
  137. package/src/__mocks__/StorybookHelpers.ts +0 -128
  138. package/src/__mocks__/jestHelpers.spec.ts +0 -38
  139. package/src/utils/findBestMatchLineage.spec.ts +0 -81
  140. package/src/utils/findBestMatchLineage.ts +0 -30
  141. 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
- ResourceBrowserContext,
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
- it('Should render with expected default value', () => {
12
- let defaultContext: ResourceBrowserContextProps | undefined;
7
+ it('Should render with expected default value', () => {
8
+ let defaultContext: ResourceBrowserContextProps | undefined;
13
9
 
14
- render(
15
- <ResourceBrowserContext.Consumer>
16
- {(value) => {
17
- defaultContext = value;
18
- return null;
19
- }}
20
- </ResourceBrowserContext.Consumer>,
21
- );
10
+ render(
11
+ <ResourceBrowserContext.Consumer>
12
+ {(value) => {
13
+ defaultContext = value;
14
+ return null;
15
+ }}
16
+ </ResourceBrowserContext.Consumer>,
17
+ );
22
18
 
23
- expect(defaultContext).toEqual({
24
- onRequestChildren: expect.any(Function),
25
- onRequestResource: expect.any(Function),
26
- onRequestSources: expect.any(Function),
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
- it('Should add memoization to data fetching functions', async () => {
38
- let context: ResourceBrowserContextProps | undefined;
39
- const sources = [mockSource({ id: '10' }), mockSource({ id: '20' })];
40
- const resources = [mockResource({ id: '100' }), mockResource({ id: '200' })];
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
- render(
46
- <ResourceBrowserContextProvider
47
- value={{
48
- onRequestSources,
49
- onRequestResource,
50
- onRequestChildren,
51
- }}
52
- >
53
- <ResourceBrowserContext.Consumer>
54
- {(value) => {
55
- context = value;
56
- return null;
57
- }}
58
- </ResourceBrowserContext.Consumer>
59
- </ResourceBrowserContextProvider>,
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
- const result = await Promise.all([
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
- // mocked data should be returned for all invocations of memoized function
74
- expect(result).toEqual([
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
- // memoized function should only be called when different arguments are provided
86
- expect(onRequestSources).toBeCalledTimes(1);
87
- expect(onRequestResource).toBeCalledTimes(2);
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
- it('Should not cache failures from data fetching functions', async () => {
92
- let context: ResourceBrowserContextProps | undefined;
93
- const sources = [mockSource({ id: '10' })];
94
- const onRequestSources = jest
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
- render(
102
- <ResourceBrowserContextProvider
103
- value={{
104
- onRequestSources,
105
- onRequestResource,
106
- onRequestChildren,
107
- }}
108
- >
109
- <ResourceBrowserContext.Consumer>
110
- {(value) => {
111
- context = value;
112
- return null;
113
- }}
114
- </ResourceBrowserContext.Consumer>
115
- </ResourceBrowserContextProvider>,
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
- await expect(context?.onRequestSources?.()).rejects.toThrow(new Error('Cannot fetch sources.'));
119
- await expect(context?.onRequestSources?.()).resolves.toEqual(sources);
120
- expect(onRequestSources).toBeCalledTimes(2);
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
  });