@squiz/resource-browser 2.4.12 → 3.0.0-pre-alpha.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 (128) hide show
  1. package/README.md +4 -0
  2. package/lib/BrowseToSource/BrowseToSource.d.ts +8 -0
  3. package/lib/BrowseToSource/BrowseToSource.js +50 -0
  4. package/lib/Hooks/useAuth.js +11 -15
  5. package/lib/Hooks/useSelectedState.js +3 -7
  6. package/lib/Hooks/useSources.d.ts +2 -2
  7. package/lib/Hooks/useSources.js +19 -9
  8. package/lib/Icons/AdsClickIcon.d.ts +4 -0
  9. package/lib/Icons/AdsClickIcon.js +5 -0
  10. package/lib/Icons/ArrowDownIcon.d.ts +4 -0
  11. package/lib/Icons/ArrowDownIcon.js +5 -0
  12. package/lib/Icons/CircledLoopIcon.js +4 -11
  13. package/lib/MainContainer/MainContainer.d.ts +6 -4
  14. package/lib/MainContainer/MainContainer.js +33 -52
  15. package/lib/Plugin/Plugin.js +7 -14
  16. package/lib/ResourceBrowserContext/AuthProvider.js +9 -37
  17. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +1 -0
  18. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +10 -39
  19. package/lib/ResourceBrowserInput/ResourceBrowserInput.d.ts +6 -4
  20. package/lib/ResourceBrowserInput/ResourceBrowserInput.js +5 -12
  21. package/lib/ResourceLauncher/ResourceLauncher.d.ts +8 -0
  22. package/lib/ResourceLauncher/ResourceLauncher.js +11 -0
  23. package/lib/ResourcePicker/ResourcePicker.js +20 -27
  24. package/lib/ResourcePicker/States/Error.js +6 -13
  25. package/lib/ResourcePicker/States/Loading.js +4 -11
  26. package/lib/ResourcePicker/States/Selected.js +12 -19
  27. package/lib/SourceDropdown/SourceDropdown.d.ts +2 -2
  28. package/lib/SourceDropdown/SourceDropdown.js +22 -48
  29. package/lib/SourceDropdownContainer/SourceDropdownContainer.d.ts +5 -0
  30. package/lib/SourceDropdownContainer/SourceDropdownContainer.js +12 -0
  31. package/lib/SourceList/SourceList.js +11 -16
  32. package/lib/index.css +102 -26
  33. package/lib/index.d.ts +4 -1
  34. package/lib/index.js +40 -66
  35. package/lib/types.d.ts +35 -3
  36. package/lib/types.js +5 -2
  37. package/lib/utils/authUtils.js +9 -16
  38. package/lib-esm/BrowseToSource/BrowseToSource.d.ts +8 -0
  39. package/lib-esm/BrowseToSource/BrowseToSource.js +50 -0
  40. package/lib-esm/Hooks/useAuth.d.ts +7 -0
  41. package/lib-esm/Hooks/useAuth.js +54 -0
  42. package/lib-esm/Hooks/useSelectedState.d.ts +15 -0
  43. package/lib-esm/Hooks/useSelectedState.js +12 -0
  44. package/lib-esm/Hooks/useSources.d.ts +14 -0
  45. package/lib-esm/Hooks/useSources.js +44 -0
  46. package/lib-esm/Icons/AdsClickIcon.d.ts +4 -0
  47. package/lib-esm/Icons/AdsClickIcon.js +5 -0
  48. package/lib-esm/Icons/ArrowDownIcon.d.ts +4 -0
  49. package/lib-esm/Icons/ArrowDownIcon.js +5 -0
  50. package/lib-esm/Icons/CircledLoopIcon.d.ts +4 -0
  51. package/lib-esm/Icons/CircledLoopIcon.js +5 -0
  52. package/lib-esm/MainContainer/MainContainer.d.ts +19 -0
  53. package/lib-esm/MainContainer/MainContainer.js +43 -0
  54. package/lib-esm/Plugin/Plugin.d.ts +13 -0
  55. package/lib-esm/Plugin/Plugin.js +12 -0
  56. package/lib-esm/ResourceBrowserContext/AuthProvider.d.ts +16 -0
  57. package/lib-esm/ResourceBrowserContext/AuthProvider.js +18 -0
  58. package/lib-esm/ResourceBrowserContext/ResourceBrowserContext.d.ts +15 -0
  59. package/lib-esm/ResourceBrowserContext/ResourceBrowserContext.js +26 -0
  60. package/lib-esm/ResourceBrowserInput/ResourceBrowserInput.d.ts +26 -0
  61. package/lib-esm/ResourceBrowserInput/ResourceBrowserInput.js +9 -0
  62. package/lib-esm/ResourceLauncher/ResourceLauncher.d.ts +8 -0
  63. package/lib-esm/ResourceLauncher/ResourceLauncher.js +11 -0
  64. package/lib-esm/ResourcePicker/ResourcePicker.d.ts +16 -0
  65. package/lib-esm/ResourcePicker/ResourcePicker.js +25 -0
  66. package/lib-esm/ResourcePicker/States/Error.d.ts +7 -0
  67. package/lib-esm/ResourcePicker/States/Error.js +6 -0
  68. package/lib-esm/ResourcePicker/States/Loading.d.ts +2 -0
  69. package/lib-esm/ResourcePicker/States/Loading.js +4 -0
  70. package/lib-esm/ResourcePicker/States/Selected.d.ts +15 -0
  71. package/lib-esm/ResourcePicker/States/Selected.js +20 -0
  72. package/lib-esm/SourceDropdown/SourceDropdown.d.ts +7 -0
  73. package/lib-esm/SourceDropdown/SourceDropdown.js +46 -0
  74. package/lib-esm/SourceDropdownContainer/SourceDropdownContainer.d.ts +5 -0
  75. package/lib-esm/SourceDropdownContainer/SourceDropdownContainer.js +12 -0
  76. package/lib-esm/SourceList/SourceList.d.ts +8 -0
  77. package/lib-esm/SourceList/SourceList.js +16 -0
  78. package/lib-esm/index.d.ts +18 -0
  79. package/lib-esm/index.js +79 -0
  80. package/lib-esm/types.d.ts +97 -0
  81. package/lib-esm/types.js +5 -0
  82. package/lib-esm/utils/authUtils.d.ts +5 -0
  83. package/lib-esm/utils/authUtils.js +31 -0
  84. package/package.json +18 -6
  85. package/src/BrowseToSource/BrowseToSource.spec.tsx +111 -0
  86. package/src/BrowseToSource/BrowseToSource.stories.tsx +29 -0
  87. package/src/BrowseToSource/BrowseToSource.tsx +111 -0
  88. package/src/Hooks/useSources.spec.ts +8 -4
  89. package/src/Hooks/useSources.ts +28 -13
  90. package/src/Icons/AdsClickIcon.tsx +11 -0
  91. package/src/Icons/ArrowDownIcon.tsx +11 -0
  92. package/src/MainContainer/MainContainer.spec.tsx +322 -108
  93. package/src/MainContainer/MainContainer.tsx +67 -27
  94. package/src/Plugin/Plugin.spec.tsx +2 -0
  95. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +3 -0
  96. package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +2 -0
  97. package/src/ResourceBrowserInput/ResourceBrowserInput.spec.tsx +7 -0
  98. package/src/ResourceBrowserInput/ResourceBrowserInput.tsx +16 -3
  99. package/src/ResourceLauncher/ResourceLauncher.spec.tsx +65 -0
  100. package/src/ResourceLauncher/ResourceLauncher.tsx +31 -0
  101. package/src/SourceDropdown/SourceDropdown.stories.tsx +1 -2
  102. package/src/SourceDropdown/SourceDropdown.tsx +8 -8
  103. package/src/SourceDropdownContainer/SourceDropdownContainer.spec.tsx +50 -0
  104. package/src/SourceDropdownContainer/SourceDropdownContainer.stories.tsx +62 -0
  105. package/src/SourceDropdownContainer/SourceDropdownContainer.tsx +27 -0
  106. package/src/__mocks__/MockModels.ts +16 -2
  107. package/src/__mocks__/PluginExample.tsx +8 -0
  108. package/src/__mocks__/StorybookHelpers.tsx +37 -1
  109. package/src/__mocks__/renderWithContext.tsx +1 -0
  110. package/src/index.spec.tsx +135 -41
  111. package/src/index.stories.tsx +12 -1
  112. package/src/index.tsx +45 -16
  113. package/src/types.ts +43 -3
  114. package/.eslintrc +0 -40
  115. package/.storybook/main.ts +0 -23
  116. package/.storybook/preview-body.html +0 -1
  117. package/.storybook/preview-head.html +0 -12
  118. package/.storybook/preview.ts +0 -16
  119. package/CHANGELOG.md +0 -244
  120. package/LICENSE.md +0 -15
  121. package/build.js +0 -21
  122. package/jest.config.ts +0 -30
  123. package/postcss.config.js +0 -21
  124. package/tailwind.config.cjs +0 -98
  125. package/tsconfig.json +0 -22
  126. package/tsconfig.storybook.json +0 -4
  127. package/tsconfig.test.json +0 -12
  128. package/vite.config.js +0 -20
@@ -3,7 +3,7 @@ import { waitFor, act } from '@testing-library/react';
3
3
  import { ResourceBrowser, ResourceBrowserProps } from './index';
4
4
  import { mockSource, mockResource } from './__mocks__/MockModels';
5
5
  import { renderWithContext } from './__mocks__/renderWithContext';
6
- import { ResourceBrowserPlugin } from './types';
6
+ import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserSourceWithPlugin, PluginLaunchModeType } from './types';
7
7
 
8
8
  import * as RBI from './ResourceBrowserInput/ResourceBrowserInput';
9
9
  jest.spyOn(RBI, 'ResourceBrowserInput');
@@ -27,18 +27,29 @@ describe('Resource browser input', () => {
27
27
  renderSelectedResource: mockRenderSelectedResource,
28
28
  sourceBrowserComponent: mockSourceBrowserComponent,
29
29
  useResolveResource: mockUseResolveResource,
30
+ sourceSearchComponent: jest.fn(),
31
+ renderResourceLauncher: jest.fn(),
30
32
  } as unknown as ResourceBrowserPlugin;
31
33
 
32
- const renderComponent = (props: Partial<ResourceBrowserProps> = {}) => {
34
+ const renderComponent = (props: Partial<ResourceBrowserProps> = {}, searchEnabled?: boolean) => {
33
35
  return renderWithContext(
34
36
  <ResourceBrowser modalTitle="Asset picker" value={null} onChange={mockChange} onClear={mockOnClear} {...props} />,
35
37
  {
36
38
  onRequestSources: mockRequestSources,
37
39
  plugins: [mockDamPlugin],
40
+ searchEnabled: !!searchEnabled,
38
41
  },
39
42
  );
40
43
  };
41
44
 
45
+ // Internally the plugin is attached to the source so handle that for tests expectation
46
+ const calculateExpectedSource = (source: ResourceBrowserSource): ResourceBrowserSourceWithPlugin => {
47
+ return {
48
+ ...source,
49
+ plugin: mockDamPlugin,
50
+ };
51
+ };
52
+
42
53
  it('If only one valid source is provided will default to its Source and Plugin', async () => {
43
54
  const source = mockSource({ type: 'dam' });
44
55
  mockRequestSources.mockResolvedValueOnce([source]);
@@ -47,7 +58,7 @@ describe('Resource browser input', () => {
47
58
  await waitFor(() => {
48
59
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
49
60
  expect.objectContaining({
50
- source,
61
+ source: calculateExpectedSource(source),
51
62
  plugin: mockDamPlugin,
52
63
  }),
53
64
  {},
@@ -55,22 +66,6 @@ describe('Resource browser input', () => {
55
66
  });
56
67
  });
57
68
 
58
- it('If multiple valid source is provided will not default to its Source', async () => {
59
- const sources = [mockSource(), mockSource()];
60
- mockRequestSources.mockResolvedValueOnce(sources);
61
- renderComponent();
62
-
63
- await waitFor(() => {
64
- expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
65
- expect.objectContaining({
66
- sources,
67
- plugin: null,
68
- }),
69
- {},
70
- );
71
- });
72
- });
73
-
74
69
  it('If a resource is provided will default to its Source and Plugin to match', async () => {
75
70
  const source = mockSource({ type: 'dam' });
76
71
  mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]);
@@ -80,8 +75,9 @@ describe('Resource browser input', () => {
80
75
  await waitFor(() => {
81
76
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
82
77
  expect.objectContaining({
83
- source,
78
+ source: calculateExpectedSource(source),
84
79
  plugin: mockDamPlugin,
80
+ pluginMode: null,
85
81
  }),
86
82
  {},
87
83
  );
@@ -107,15 +103,16 @@ describe('Resource browser input', () => {
107
103
  });
108
104
 
109
105
  it('onSourceSelect will alter source passed to ResourceBrowserInput', async () => {
110
- const sources = [mockSource({ type: 'dam' }), mockSource()];
111
- mockRequestSources.mockResolvedValueOnce(sources);
106
+ const sourcesInput = [mockSource({ type: 'dam' }), mockSource()];
107
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
108
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
112
109
  renderComponent();
113
110
 
114
111
  // Expect no source or plugin
115
112
  await waitFor(() => {
116
113
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
117
114
  expect.objectContaining({
118
- sources,
115
+ sources: calculatedSources,
119
116
  source: null,
120
117
  plugin: null,
121
118
  }),
@@ -127,15 +124,54 @@ describe('Resource browser input', () => {
127
124
  const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
128
125
  // Invoke it
129
126
  act(() => {
130
- setSource(sources[0]);
127
+ setSource(calculatedSources[0]);
131
128
  });
132
129
 
133
130
  // Expect the source and plugin to be loaded
134
131
  await waitFor(() => {
135
132
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
136
133
  expect.objectContaining({
137
- source: sources[0],
134
+ source: calculatedSources[0],
138
135
  plugin: mockDamPlugin,
136
+ pluginMode: null,
137
+ }),
138
+ {},
139
+ );
140
+ });
141
+ });
142
+
143
+ it('onSourceSelect will set mode is provided', async () => {
144
+ const sourcesInput = [mockSource({ type: 'dam' }), mockSource()];
145
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
146
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
147
+ renderComponent();
148
+
149
+ // Expect no source or plugin
150
+ await waitFor(() => {
151
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
152
+ expect.objectContaining({
153
+ sources: calculatedSources,
154
+ source: null,
155
+ plugin: null,
156
+ }),
157
+ {},
158
+ );
159
+ });
160
+
161
+ // Get the provided callback
162
+ const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
163
+ // Invoke it
164
+ act(() => {
165
+ setSource(calculatedSources[0], { type: PluginLaunchModeType.Search, args: { query: 'myQuery' } });
166
+ });
167
+
168
+ // Expect the source and plugin to be loaded
169
+ await waitFor(() => {
170
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
171
+ expect.objectContaining({
172
+ source: calculatedSources[0],
173
+ plugin: mockDamPlugin,
174
+ pluginMode: { type: PluginLaunchModeType.Search, args: { query: 'myQuery' } },
139
175
  }),
140
176
  {},
141
177
  );
@@ -152,7 +188,7 @@ describe('Resource browser input', () => {
152
188
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
153
189
  expect.objectContaining({
154
190
  value: resource,
155
- source,
191
+ source: calculateExpectedSource(source),
156
192
  plugin: mockDamPlugin,
157
193
  }),
158
194
  {},
@@ -160,20 +196,21 @@ describe('Resource browser input', () => {
160
196
  });
161
197
 
162
198
  await waitFor(() => {
163
- expect(mockUseResolveResource).toHaveBeenCalledWith(resource.resourceId, source);
199
+ expect(mockUseResolveResource).toHaveBeenCalledWith(resource.resourceId, calculateExpectedSource(source));
164
200
  });
165
201
  });
166
202
 
167
203
  it('onModalStateChange called with false will reset the Source and Plugin', async () => {
168
- const sources = [mockSource({ type: 'dam' }), mockSource()];
169
- mockRequestSources.mockResolvedValueOnce(sources);
204
+ const sourcesInput = [mockSource({ type: 'dam' }), mockSource()];
205
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
206
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
170
207
  renderComponent();
171
208
 
172
209
  // Expect no source or plugin
173
210
  await waitFor(() => {
174
211
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
175
212
  expect.objectContaining({
176
- sources,
213
+ sources: calculatedSources,
177
214
  source: null,
178
215
  plugin: null,
179
216
  }),
@@ -185,14 +222,14 @@ describe('Resource browser input', () => {
185
222
  const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
186
223
  // Invoke it
187
224
  act(() => {
188
- setSource(sources[0]);
225
+ setSource(calculatedSources[0]);
189
226
  });
190
227
 
191
228
  // Expect the source and plugin to be loaded
192
229
  await waitFor(() => {
193
230
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
194
231
  expect.objectContaining({
195
- source: sources[0],
232
+ source: calculatedSources[0],
196
233
  plugin: mockDamPlugin,
197
234
  }),
198
235
  {},
@@ -207,9 +244,10 @@ describe('Resource browser input', () => {
207
244
  await waitFor(() => {
208
245
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
209
246
  expect.objectContaining({
210
- sources,
247
+ sources: calculatedSources,
211
248
  source: null,
212
249
  plugin: null,
250
+ pluginMode: null,
213
251
  }),
214
252
  {},
215
253
  );
@@ -225,7 +263,7 @@ describe('Resource browser input', () => {
225
263
  await waitFor(() => {
226
264
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
227
265
  expect.objectContaining({
228
- source: sources[0],
266
+ source: calculateExpectedSource(sources[0]),
229
267
  plugin: mockDamPlugin,
230
268
  }),
231
269
  {},
@@ -241,7 +279,7 @@ describe('Resource browser input', () => {
241
279
  await waitFor(() => {
242
280
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
243
281
  expect.objectContaining({
244
- source: sources[0],
282
+ source: calculateExpectedSource(sources[0]),
245
283
  plugin: mockDamPlugin,
246
284
  }),
247
285
  {},
@@ -249,16 +287,72 @@ describe('Resource browser input', () => {
249
287
  });
250
288
  });
251
289
 
290
+ it('onModalStateChange called with false will reset Source and Plugin if only one source exists AND search is enabled', async () => {
291
+ const sourcesInput = [mockSource({ type: 'dam' })];
292
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
293
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
294
+ renderComponent({}, true);
295
+
296
+ // Expect no source or plugin
297
+ await waitFor(() => {
298
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
299
+ expect.objectContaining({
300
+ sources: calculatedSources,
301
+ source: null,
302
+ plugin: null,
303
+ }),
304
+ {},
305
+ );
306
+ });
307
+
308
+ // Get the provided callback
309
+ const { setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
310
+ // Invoke it
311
+ act(() => {
312
+ setSource(calculatedSources[0]);
313
+ });
314
+
315
+ // Expect the source and plugin to be loaded
316
+ await waitFor(() => {
317
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
318
+ expect.objectContaining({
319
+ source: calculatedSources[0],
320
+ plugin: mockDamPlugin,
321
+ }),
322
+ {},
323
+ );
324
+ });
325
+
326
+ // Invoke modal close callback
327
+ const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
328
+ onModalStateChange(false);
329
+
330
+ // Expect no source or plugin
331
+ await waitFor(() => {
332
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
333
+ expect.objectContaining({
334
+ sources: calculatedSources,
335
+ source: null,
336
+ plugin: null,
337
+ pluginMode: null,
338
+ }),
339
+ {},
340
+ );
341
+ });
342
+ });
343
+
252
344
  it('onModalStateChange called with false will not reset the Source and Plugin if a value exists', async () => {
253
- const sources = [mockSource({ type: 'dam' }), mockSource()];
254
- mockRequestSources.mockResolvedValueOnce(sources);
255
- renderComponent({ value: { sourceId: sources[0].id, resourceId: '123456' } });
345
+ const sourcesInput = [mockSource({ type: 'dam' }), mockSource()];
346
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
347
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
348
+
349
+ renderComponent({ value: { sourceId: sourcesInput[0].id, resourceId: '123456' } });
256
350
 
257
351
  await waitFor(() => {
258
352
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
259
353
  expect.objectContaining({
260
- sources,
261
- source: sources[0],
354
+ sources: calculatedSources,
355
+ source: calculatedSources[0],
262
356
  plugin: mockDamPlugin,
263
357
  }),
264
358
  {},
@@ -274,7 +368,7 @@ describe('Resource browser input', () => {
274
368
  await waitFor(() => {
275
369
  expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
276
370
  expect.objectContaining({
277
- source: sources[0],
371
+ source: calculatedSources[0],
278
372
  plugin: mockDamPlugin,
279
373
  }),
280
374
  {},
@@ -37,7 +37,11 @@ const Template: StoryFn<typeof ResourceBrowser> = (props) => {
37
37
  return (
38
38
  <div className="w-[400px] m-3">
39
39
  <ResourceBrowserContextProvider
40
- value={{ onRequestSources: onRequestSources as () => Promise<ResourceBrowserSource[]>, plugins }}
40
+ value={{
41
+ onRequestSources: onRequestSources as () => Promise<ResourceBrowserSource[]>,
42
+ plugins,
43
+ searchEnabled: props.searchEnabled,
44
+ }}
41
45
  >
42
46
  <ResourceBrowser {...props} value={resource} onChange={onChange} onClear={undefined} />
43
47
  </ResourceBrowserContextProvider>
@@ -55,6 +59,7 @@ Primary.args = {
55
59
  callbackWait: 0,
56
60
  singleSource: false,
57
61
  headerPortal: false,
62
+ searchEnabled: false,
58
63
  };
59
64
 
60
65
  export const Selected = Template.bind({});
@@ -77,3 +82,9 @@ SingleSource.args = {
77
82
  ...Primary.args,
78
83
  singleSource: true,
79
84
  };
85
+
86
+ export const SearchEnabled = Template.bind({});
87
+ SearchEnabled.args = {
88
+ ...Primary.args,
89
+ searchEnabled: true,
90
+ };
package/src/index.tsx CHANGED
@@ -1,12 +1,31 @@
1
1
  import React, { useState, useContext, useEffect, useCallback } from 'react';
2
2
 
3
3
  import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
4
- import { ResourceBrowserSource, ResourceBrowserUnresolvedResource, ResourceBrowserResource, ResourceBrowserPlugin } from './types';
4
+ import {
5
+ PluginLaunchMode,
6
+ ResourceBrowserUnresolvedResource,
7
+ ResourceBrowserResource,
8
+ ResourceBrowserPlugin,
9
+ ResourceBrowserSourceWithPlugin,
10
+ } from './types';
5
11
  import { useSources } from './Hooks/useSources';
6
12
  import { PluginRender } from './Plugin/Plugin';
7
13
  import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
8
14
 
9
- export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext };
15
+ import BrowseToSource from './BrowseToSource/BrowseToSource';
16
+ import SourceDropdown from './SourceDropdown/SourceDropdown';
17
+ import SourceDropdownContainer from './SourceDropdownContainer/SourceDropdownContainer';
18
+
19
+ export {
20
+ ResourceBrowserContext,
21
+ ResourceBrowserContextProvider,
22
+ useAuthContext,
23
+ AuthProvider,
24
+ AuthContext,
25
+ BrowseToSource,
26
+ SourceDropdown,
27
+ SourceDropdownContainer,
28
+ };
10
29
  export * from './types';
11
30
 
12
31
  export type ResourceBrowserProps = {
@@ -21,24 +40,26 @@ export type ResourceBrowserProps = {
21
40
  export const ResourceBrowser = (props: ResourceBrowserProps) => {
22
41
  const { value } = props;
23
42
  const [error, setError] = useState<Error | null>(null);
24
- const { onRequestSources, plugins } = useContext(ResourceBrowserContext);
43
+ const { onRequestSources, searchEnabled, plugins } = useContext(ResourceBrowserContext);
25
44
 
26
45
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
27
- const [source, setSource] = useState<ResourceBrowserSource | null>(null);
46
+ const [source, setSource] = useState<ResourceBrowserSourceWithPlugin | null>(null);
47
+ const [mode, setMode] = useState<PluginLaunchMode | null>(null);
28
48
  const { data: sources, isLoading, error: sourcesError } = useSources({ onRequestSources, plugins });
29
49
  const [plugin, setPlugin] = useState<ResourceBrowserPlugin | null>(null);
30
50
 
31
51
  // MainContainer will render a list of sources of one is not provided to it, callback to allow it to set the source once a user selects
32
52
  const handleSourceSelect = useCallback(
33
- (source: ResourceBrowserSource | null) => {
53
+ (source: ResourceBrowserSourceWithPlugin, mode?: PluginLaunchMode) => {
34
54
  setSource(source);
55
+ setMode(mode || null);
35
56
  },
36
- [setSource],
57
+ [setSource, setMode],
37
58
  );
38
59
 
39
- // Try to auto set the source
60
+ // If an existing resource is passed in auto select its source
40
61
  useEffect(() => {
41
- let source: ResourceBrowserSource | null = null;
62
+ let source: ResourceBrowserSourceWithPlugin | null = null;
42
63
  setError(null);
43
64
 
44
65
  // If there is a provided value try to use its source
@@ -50,34 +71,38 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
50
71
  // Set an error as the passed in value's source wasnt returned by onRequestSources
51
72
  setError(new Error('Unable to find resource source.'));
52
73
  }
53
- } else if (sources?.length === 1) {
54
- // If only one source is passed in select it automatically
74
+ } else if (sources?.length === 1 && !searchEnabled) {
75
+ // If only one source is passed and search is not enabled select it automatically
55
76
  source = sources[0];
56
77
  }
57
78
 
58
79
  setSource(source);
80
+ setMode(null); // Passed in resource will always use the default mode
59
81
  }, [value, isLoading, sources, setSource, setError]);
60
82
 
83
+ // When a source is selected update our plugin reference to match (legacy support)
84
+ // the plugin is now attached to the source directly when fetched from the context so use that instead when possible
61
85
  useEffect(() => {
62
- if (source) {
63
- const plugin = plugins.find((plugin) => {
64
- return plugin.type === source.type;
65
- });
66
- setPlugin(plugin || null);
86
+ if (source?.plugin) {
87
+ setPlugin(source.plugin);
67
88
  } else {
68
89
  setPlugin(null);
69
90
  }
70
91
  }, [plugins, source]);
71
92
 
93
+ // The modal has some control over it own open/closed state (for WCAG reasons) so keep this in sync with our state
72
94
  const handleModalStateChange = useCallback(
73
95
  (isOpen: boolean) => {
74
96
  setIsModalOpen(isOpen);
75
97
  },
76
98
  [setIsModalOpen],
77
99
  );
100
+
101
+ // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
78
102
  useEffect(() => {
79
- if (!isModalOpen && !value && sources?.length > 1) {
103
+ if (!isModalOpen && !value && (sources?.length > 1 || searchEnabled)) {
80
104
  setSource(null);
105
+ setMode(null);
81
106
  }
82
107
  }, [sources, isModalOpen]);
83
108
 
@@ -94,6 +119,8 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
94
119
  isLoading={isLoading}
95
120
  error={sourcesError || error}
96
121
  plugin={plugin}
122
+ pluginMode={mode}
123
+ searchEnabled={searchEnabled}
97
124
  useResource={() => {
98
125
  return {
99
126
  data: null,
@@ -116,6 +143,8 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
116
143
  isLoading={isLoading}
117
144
  error={error}
118
145
  plugin={plugin}
146
+ pluginMode={mode}
147
+ searchEnabled={searchEnabled}
119
148
  useResource={thisPlugin.useResolveResource}
120
149
  isModalOpen={isModalOpen}
121
150
  onModalStateChange={handleModalStateChange}
package/src/types.ts CHANGED
@@ -20,10 +20,25 @@ export interface ResourceBrowserSource {
20
20
  type: ResourceBrowserPluginType;
21
21
  }
22
22
 
23
+ export interface ResourceBrowserSourceWithPlugin extends ResourceBrowserSource {
24
+ // Sources plugin for quick reference
25
+ plugin: ResourceBrowserPlugin;
26
+ }
27
+
23
28
  export interface ResourceBrowserSourceWithConfig extends ResourceBrowserSource {
24
29
  configuration?: AuthenticationConfiguration;
25
30
  }
26
31
 
32
+ export enum PluginLaunchModeType {
33
+ 'Browse' = 'browse',
34
+ 'Search' = 'search',
35
+ }
36
+
37
+ export type PluginLaunchMode = {
38
+ type: PluginLaunchModeType;
39
+ args?: ResourceBrowserSearchUIArgs | ResourceBrowserUIArgs;
40
+ };
41
+
27
42
  export type ResourceBrowserUnresolvedResource = {
28
43
  sourceId: string;
29
44
  resourceId: string;
@@ -48,12 +63,31 @@ export type ResourceBrowserSelectedState = {
48
63
  description: Array<ReactElement>;
49
64
  };
50
65
 
51
- export type ResourceBrowserUIProps = {
66
+ type ResourceBrowserProps = {
52
67
  source: ResourceBrowserSource;
68
+ onSourceSelect(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
69
+ sources: ResourceBrowserSourceWithPlugin[];
53
70
  allowedTypes?: string[];
54
71
  headerPortal?: Element;
55
- preselectedResource?: ResourceBrowserResource;
56
72
  onSelected: (resource: ResourceBrowserResource) => void;
73
+ searchEnabled: boolean;
74
+ };
75
+
76
+ export type ResourceBrowserUIArgs = {
77
+ preselectedResource?: ResourceBrowserResource;
78
+ browseTo?: ResourceBrowserUnresolvedResource;
79
+ };
80
+ export type ResourceBrowserUIProps = ResourceBrowserProps & ResourceBrowserUIArgs;
81
+
82
+ export type ResourceBrowserSearchUIArgs = {
83
+ query?: string;
84
+ };
85
+ export type ResourceBrowserSearchUIProps = ResourceBrowserProps & ResourceBrowserSearchUIArgs;
86
+
87
+ export type ResourceBrowserLauncherProps = {
88
+ source: ResourceBrowserSource;
89
+ onSearch: (query: string) => void;
90
+ onBrowse: (browseTo?: ResourceBrowserUnresolvedResource) => void;
57
91
  };
58
92
 
59
93
  export type useResolveResourceResponse = {
@@ -72,9 +106,15 @@ export interface ResourceBrowserPlugin {
72
106
  // Setting to create a header portal
73
107
  createHeaderPortal?: boolean;
74
108
 
75
- /** React Functional Component to provde the UI to render to allow a user to select a resource to use */
109
+ /** React Functional Component to provde the UI to render to allow a user to browse for resource to use */
76
110
  sourceBrowserComponent: () => React.FunctionComponent<ResourceBrowserUIProps>;
77
111
 
112
+ /** React Functional Component to provde the UI to render to allow a user to search for resource to use */
113
+ sourceSearchComponent: () => React.FunctionComponent<ResourceBrowserSearchUIProps>;
114
+
115
+ /** React Functional Component to provde the sources launcher view */
116
+ renderResourceLauncher: () => React.FunctionComponent<ResourceBrowserLauncherProps>;
117
+
78
118
  /** Function to provde the the summary information to show what resource is currently selected */
79
119
  renderSelectedResource: (resource: ResourceBrowserResource) => Promise<ResourceBrowserSelectedState>;
80
120
 
package/.eslintrc DELETED
@@ -1,40 +0,0 @@
1
- {
2
- "extends": ["@squiz"],
3
- "plugins": ["import", "prettier"],
4
- "rules": {
5
- "prettier/prettier": "error",
6
- },
7
- "settings": {
8
- "import/parsers": {
9
- "@typescript-eslint/parser": [".ts", ".tsx"],
10
- },
11
- "import/resolver": {
12
- "typescript": {
13
- "alwaysTryTypes": true,
14
- }
15
- }
16
- },
17
- "ignorePatterns": ["**/lib/"],
18
- "overrides": [
19
- {
20
- "files": ["*.spec.ts", "*.spec.tsx"],
21
- "rules": {
22
- "no-var": 0,
23
- "import/no-extraneous-dependencies": 0,
24
- }
25
- },
26
- {
27
- "files": ["*.config.ts", "*.config.js", "build.js"],
28
- "rules": {
29
- "import/no-extraneous-dependencies": 0,
30
- }
31
- },
32
- {
33
- "files": ["*.stories.ts", "*.stories.tsx"],
34
- "rules": {
35
- "import/no-extraneous-dependencies": 0,
36
- "no-console": "off"
37
- }
38
- }
39
- ]
40
- }
@@ -1,23 +0,0 @@
1
- import type { StorybookConfig } from '@storybook/react-vite';
2
- const config: StorybookConfig = {
3
- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
4
- addons: [
5
- '@storybook/addon-links',
6
- '@storybook/addon-essentials',
7
- '@storybook/addon-interactions',
8
- {
9
- name: '@storybook/addon-styling',
10
- options: {
11
- postCss: true,
12
- },
13
- },
14
- ],
15
- framework: {
16
- name: '@storybook/react-vite',
17
- options: {},
18
- },
19
- docs: {
20
- autodocs: 'tag',
21
- },
22
- };
23
- export default config;
@@ -1 +0,0 @@
1
- <body class="squiz-rb-scope"></body>
@@ -1,12 +0,0 @@
1
- <script>
2
- window.global = window;
3
- </script>
4
-
5
- <style>
6
- .sb-show-main.sb-main-padded {
7
- padding: 0;
8
- }
9
- body {
10
- font-family: 'Open Sans';
11
- }
12
- </style>
@@ -1,16 +0,0 @@
1
- import type { Preview } from '@storybook/react';
2
- import '../src/index.scss';
3
-
4
- const preview: Preview = {
5
- parameters: {
6
- actions: { argTypesRegex: '^on[A-Z].*' },
7
- controls: {
8
- matchers: {
9
- color: /(background|color)$/i,
10
- date: /Date$/,
11
- },
12
- },
13
- },
14
- };
15
-
16
- export default preview;