@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
@@ -29,6 +29,8 @@ describe('Plugin', () => {
29
29
  };
30
30
  },
31
31
  plugin: null,
32
+ pluginMode: null,
33
+ searchEnabled: false,
32
34
  source: null,
33
35
  sources: [],
34
36
  isLoading: false,
@@ -19,6 +19,7 @@ describe('ResourceBrowserContext', () => {
19
19
  expect(defaultContext).toEqual({
20
20
  onRequestSources: expect.any(Function),
21
21
  plugins: [],
22
+ searchEnabled: false,
22
23
  });
23
24
  expect(() => defaultContext?.onRequestSources()).toThrow('onRequestSources has not been configured.');
24
25
  });
@@ -33,6 +34,7 @@ describe('ResourceBrowserContext', () => {
33
34
  value={{
34
35
  onRequestSources,
35
36
  plugins: [],
37
+ searchEnabled: false,
36
38
  }}
37
39
  >
38
40
  <ResourceBrowserContext.Consumer>
@@ -63,6 +65,7 @@ describe('ResourceBrowserContext', () => {
63
65
  value={{
64
66
  onRequestSources,
65
67
  plugins: [],
68
+ searchEnabled: false,
66
69
  }}
67
70
  >
68
71
  <ResourceBrowserContext.Consumer>
@@ -6,6 +6,7 @@ import ExpiryMap from 'expiry-map';
6
6
 
7
7
  export type ResourceBrowserContextProps = {
8
8
  onRequestSources: OnRequestSources;
9
+ searchEnabled: boolean;
9
10
  plugins: Array<ResourceBrowserPlugin>;
10
11
  };
11
12
 
@@ -17,6 +18,7 @@ export const ResourceBrowserContext = React.createContext<ResourceBrowserContext
17
18
  onRequestSources: () => {
18
19
  throw new Error('onRequestSources has not been configured.');
19
20
  },
21
+ searchEnabled: false,
20
22
  plugins: [],
21
23
  });
22
24
 
@@ -25,6 +25,8 @@ describe('Resource browser input', () => {
25
25
  useResource={mockUseResource}
26
26
  onChange={mockChange}
27
27
  plugin={null}
28
+ pluginMode={null}
29
+ searchEnabled={false}
28
30
  source={null}
29
31
  sources={[]}
30
32
  isLoading={false}
@@ -149,6 +151,11 @@ describe('Resource browser input', () => {
149
151
  return <></>;
150
152
  };
151
153
  },
154
+ sourceSearchComponent: () => {
155
+ return () => {
156
+ return <></>;
157
+ };
158
+ },
152
159
  };
153
160
 
154
161
  renderComponent({ value: { sourceId: source.id, resourceId: '100' }, plugin: plugin as ResourceBrowserPlugin });
@@ -1,7 +1,14 @@
1
1
  import React from 'react';
2
2
  import MainContainer from '../MainContainer/MainContainer';
3
3
  import { ResourcePicker } from '../ResourcePicker/ResourcePicker';
4
- import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserUnresolvedResource, ResourceBrowserResource } from '../types';
4
+ import {
5
+ ResourceBrowserPlugin,
6
+ ResourceBrowserSource,
7
+ ResourceBrowserUnresolvedResource,
8
+ ResourceBrowserResource,
9
+ ResourceBrowserSourceWithPlugin,
10
+ PluginLaunchMode,
11
+ } from '../types';
5
12
 
6
13
  export type ResourceBrowserInputProps = {
7
14
  modalTitle: string;
@@ -19,11 +26,13 @@ export type ResourceBrowserInputProps = {
19
26
  onChange(resource: ResourceBrowserResource | null): void;
20
27
  onClear?(): void;
21
28
  plugin: ResourceBrowserPlugin | null;
29
+ pluginMode: PluginLaunchMode | null;
30
+ searchEnabled: boolean;
22
31
  source: ResourceBrowserSource | null;
23
- sources: ResourceBrowserSource[];
32
+ sources: ResourceBrowserSourceWithPlugin[];
24
33
  isLoading: boolean;
25
34
  error: Error | null;
26
- setSource(source: ResourceBrowserSource | null): void;
35
+ setSource(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
27
36
  isModalOpen: boolean;
28
37
  onModalStateChange(isOpen: boolean): void;
29
38
  };
@@ -37,6 +46,8 @@ export const ResourceBrowserInput = ({
37
46
  isDisabled,
38
47
  onClear,
39
48
  plugin,
49
+ pluginMode,
50
+ searchEnabled,
40
51
  source,
41
52
  sources,
42
53
  isLoading,
@@ -68,6 +79,8 @@ export const ResourceBrowserInput = ({
68
79
  sources={sources}
69
80
  preselectedResource={resource}
70
81
  plugin={plugin}
82
+ pluginMode={pluginMode}
83
+ searchEnabled={searchEnabled}
71
84
  title={modalTitle}
72
85
  titleAriaProps={titleProps}
73
86
  allowedTypes={allowedTypes}
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import ResourceLauncher from './ResourceLauncher';
4
+ import { PluginLaunchModeType } from '../types';
5
+
6
+ import { mockPlugin, mockSourceWithPlugin, mockResource } from '../__mocks__/MockModels';
7
+
8
+ describe('ResourceLauncher', () => {
9
+ const RenderResourceLauncher = jest.fn().mockReturnValue(<div>Resource Launcher UI</div>);
10
+ const defaultPlugin = mockPlugin({
11
+ renderResourceLauncher: () => {
12
+ return RenderResourceLauncher;
13
+ },
14
+ });
15
+
16
+ it('should render <li> for each provided source', () => {
17
+ const sources = [mockSourceWithPlugin({ plugin: defaultPlugin }), mockSourceWithPlugin({ id: '2', plugin: defaultPlugin })];
18
+ const onSourceSelect = jest.fn();
19
+ render(<ResourceLauncher sources={sources} onSourceSelect={onSourceSelect} />);
20
+
21
+ expect(screen.getByLabelText('sources list')).toBeInTheDocument();
22
+ expect(screen.getByLabelText('sources list').children.length).toEqual(2);
23
+ expect(screen.queryAllByText('Resource Launcher UI').length).toEqual(2);
24
+ });
25
+
26
+ it('onSourceSelect query', async () => {
27
+ const sources = [mockSourceWithPlugin({ plugin: defaultPlugin })];
28
+ const onSourceSelect = jest.fn();
29
+ render(<ResourceLauncher sources={sources} onSourceSelect={onSourceSelect} />);
30
+
31
+ // Get the first plugin function launcher components called props
32
+ const { onSearch } = (RenderResourceLauncher as jest.Mock).mock.calls[0][0];
33
+
34
+ // Invoke the onSearch handler
35
+ onSearch('test');
36
+ expect(onSourceSelect).toHaveBeenCalledWith(sources[0], { type: PluginLaunchModeType.Search, args: { query: 'test' } });
37
+ });
38
+
39
+ it('onSourceSelect browse', async () => {
40
+ const sources = [mockSourceWithPlugin({ plugin: defaultPlugin })];
41
+ const onSourceSelect = jest.fn();
42
+ render(<ResourceLauncher sources={sources} onSourceSelect={onSourceSelect} />);
43
+
44
+ // Get the first plugin function launcher components called props
45
+ const { onBrowse } = (RenderResourceLauncher as jest.Mock).mock.calls[0][0];
46
+
47
+ // Invoke the onBrowse handler
48
+ onBrowse();
49
+ expect(onSourceSelect).toHaveBeenCalledWith(sources[0], { type: PluginLaunchModeType.Browse, args: { browseTo: undefined } });
50
+ });
51
+
52
+ it('onSourceSelect browseTo', async () => {
53
+ const sources = [mockSourceWithPlugin({ plugin: defaultPlugin })];
54
+ const onSourceSelect = jest.fn();
55
+ render(<ResourceLauncher sources={sources} onSourceSelect={onSourceSelect} />);
56
+
57
+ // Get the first plugin function launcher components called props
58
+ const { onBrowse } = (RenderResourceLauncher as jest.Mock).mock.calls[0][0];
59
+
60
+ // Invoke the onBrowse handler
61
+ const browseTo = mockResource();
62
+ onBrowse(browseTo);
63
+ expect(onSourceSelect).toHaveBeenCalledWith(sources[0], { type: PluginLaunchModeType.Browse, args: { browseTo } });
64
+ });
65
+ });
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserSource, ResourceBrowserSourceWithPlugin, PluginLaunchMode, PluginLaunchModeType } from '../types';
3
+
4
+ interface ResourceLauncherProps {
5
+ sources: ResourceBrowserSourceWithPlugin[];
6
+ onSourceSelect(source: ResourceBrowserSource, mode: PluginLaunchMode): void;
7
+ }
8
+
9
+ function ResourceLauncher({ sources, onSourceSelect }: ResourceLauncherProps) {
10
+ return (
11
+ <div className="overflow-y-scroll w-screen max-w-[400px] min-h-[290px] flex-1 grow-[3] border-r border-gray-300 bg-gray-100 pl-6 pr-6 pb-6 pt-4">
12
+ <ul tabIndex={-1} aria-label={`sources list`} className="flex flex-col bg-gray-100 min-h-full focus-visible:outline-0">
13
+ {sources.map((source, index) => {
14
+ const SourceLauncher = source.plugin.renderResourceLauncher();
15
+
16
+ return (
17
+ <li key={index} className="flex items-stretch relative">
18
+ <SourceLauncher
19
+ source={source}
20
+ onSearch={(query) => onSourceSelect(source, { type: PluginLaunchModeType.Search, args: { query } })}
21
+ onBrowse={(browseTo) => onSourceSelect(source, { type: PluginLaunchModeType.Browse, args: { browseTo } })}
22
+ />
23
+ </li>
24
+ );
25
+ })}
26
+ </ul>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export default ResourceLauncher;
@@ -15,7 +15,7 @@ const Template: StoryFn<typeof SourceDropdown> = (props) => {
15
15
  const [selectedSource, setSelectedSource] = useState<ResourceBrowserSource>(sources[0]);
16
16
 
17
17
  return (
18
- <div className="m-3">
18
+ <div className="m-3 w-[200px]">
19
19
  <SourceDropdown
20
20
  selectedSource={selectedSource}
21
21
  sources={sources}
@@ -26,5 +26,4 @@ const Template: StoryFn<typeof SourceDropdown> = (props) => {
26
26
  };
27
27
 
28
28
  export const Primary = Template.bind({});
29
-
30
29
  Primary.args = {};
@@ -1,8 +1,9 @@
1
1
  import React, { useRef, useState } from 'react';
2
2
 
3
- import { ResourceBrowserSource } from '../types';
3
+ import { ResourceBrowserSource, PluginLaunchMode } from '../types';
4
4
  import { Icon, IconOptions, uuid } from '@squiz/generic-browser-lib';
5
5
  import { useFocusWithin, useKeyboard } from 'react-aria';
6
+ import { ArrowDownIcon } from '../Icons/ArrowDownIcon';
6
7
 
7
8
  export default function SourceDropdown({
8
9
  sources,
@@ -11,7 +12,7 @@ export default function SourceDropdown({
11
12
  }: {
12
13
  sources: ResourceBrowserSource[];
13
14
  selectedSource: ResourceBrowserSource;
14
- onSourceSelect(source: ResourceBrowserSource): void;
15
+ onSourceSelect(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
15
16
  }) {
16
17
  const [uniqueId] = useState(uuid());
17
18
  const buttonRef = useRef<HTMLButtonElement>(null);
@@ -45,7 +46,7 @@ export default function SourceDropdown({
45
46
  }
46
47
 
47
48
  return (
48
- <div {...focusWithinProps} {...keyboardProps} className="relative w-72 border-2 rounded border-gray-300">
49
+ <div {...focusWithinProps} {...keyboardProps} className="relative w-full ">
49
50
  <button
50
51
  ref={buttonRef}
51
52
  type="button"
@@ -53,17 +54,16 @@ export default function SourceDropdown({
53
54
  aria-expanded={isOpen}
54
55
  aria-controls={`${uniqueId}-button-menu`}
55
56
  onClick={() => setIsOpen(!isOpen)}
56
- className="relative flex items-center text-sm font-semibold p-1.5 w-full"
57
+ className="relative flex items-center p-2 w-full rounded bg-blue-100 hover:bg-blue-150"
57
58
  >
58
59
  <span className="sr-only">current source </span>
59
- <Icon icon={selectedSource.type as IconOptions} aria-hidden className="mr-2.5 h-[20px] w-[20px]" />
60
- <div className="truncate max-w-[200px]">{selectedSource.name}</div>
61
- <Icon icon={'arrow-down' as IconOptions} aria-hidden className="absolute right-3" />
60
+ <div className="truncate max-w-[200px] text-md font-semibold text-blue-400">{selectedSource.name}</div>
61
+ <ArrowDownIcon aria-hidden className="absolute right-2 fill-blue-300" />
62
62
  </button>
63
63
  <ul
64
64
  id={`${uniqueId}-button-menu`}
65
65
  aria-hidden={!isOpen}
66
- className={`absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border-1 shadow-md rounded border-gray-300 p-2 pb-0 overflow-y-scroll max-h-80 ${
66
+ className={`absolute z-50 top-[calc(100%+8px)] w-[100%] bg-gray-100 border-1 shadow-md rounded border-gray-300 p-2 pb-0 overflow-y-scroll max-h-80 ${
67
67
  !isOpen ? 'hidden' : ''
68
68
  }`}
69
69
  >
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import SourceDropdownContainer from './SourceDropdownContainer';
4
+
5
+ describe('SourceDropdownContainer', () => {
6
+ it('Render a dropdown container with child component(s)', async () => {
7
+ const children = <span id="dropdown-content">Hello world</span>;
8
+
9
+ const { baseElement } = render(
10
+ <SourceDropdownContainer isCollapsed={false} onExpand={jest.fn()}>
11
+ {children}
12
+ </SourceDropdownContainer>,
13
+ );
14
+
15
+ const content = baseElement.querySelector(`#dropdown-content`) as HTMLSpanElement;
16
+ expect(content).toBeTruthy();
17
+ });
18
+
19
+ it('Render collapsed state with single button to re-expand no children', async () => {
20
+ const children = <span id="dropdown-content">Hello world</span>;
21
+
22
+ const { baseElement } = render(
23
+ <SourceDropdownContainer isCollapsed={true} onExpand={jest.fn()}>
24
+ {children}
25
+ </SourceDropdownContainer>,
26
+ );
27
+
28
+ const content = baseElement.querySelector(`#dropdown-content`) as HTMLSpanElement;
29
+ expect(content).toBeFalsy();
30
+
31
+ const expandButton = screen.getByRole('button', { name: /Expand browse options/i });
32
+ expect(expandButton).toBeTruthy();
33
+ });
34
+
35
+ it('Will envoke onExpand callback function when expand button is pressed', async () => {
36
+ const onExpand = jest.fn();
37
+ const children = <span id="dropdown-content">Hello world</span>;
38
+
39
+ render(
40
+ <SourceDropdownContainer isCollapsed={true} onExpand={onExpand}>
41
+ {children}
42
+ </SourceDropdownContainer>,
43
+ );
44
+
45
+ const expandButton = screen.getByRole('button', { name: /Expand browse options/i });
46
+ fireEvent.click(expandButton);
47
+
48
+ expect(onExpand).toHaveBeenCalled();
49
+ });
50
+ });
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import { StoryFn, Meta } from '@storybook/react';
3
+ import SourceDropdownContainer from './SourceDropdownContainer';
4
+ import sampleSources from '../__mocks__/sample-sources.json';
5
+ import SourceDropdown from '../SourceDropdown/SourceDropdown';
6
+ import { ResourceBrowserSource } from '../types';
7
+
8
+ export default {
9
+ title: 'Source dropdown container',
10
+ component: SourceDropdownContainer,
11
+ } as Meta<typeof SourceDropdownContainer>;
12
+
13
+ const Template: StoryFn<typeof SourceDropdownContainer> = (props) => {
14
+ return (
15
+ <div className="m-3">
16
+ <SourceDropdownContainer {...props}>{props.children}</SourceDropdownContainer>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export const Primary = Template.bind({});
22
+ Primary.args = {
23
+ isCollapsed: false,
24
+ onExpand: () => {
25
+ console.log('expanded');
26
+ },
27
+ children: (
28
+ <div className="flex items-center h-[36px]">
29
+ <span className="mr-3">Child component</span>
30
+ </div>
31
+ ),
32
+ };
33
+
34
+ export const Collapsed = Template.bind({});
35
+ Collapsed.args = {
36
+ isCollapsed: true,
37
+ onExpand: () => {
38
+ console.log('expanded');
39
+ },
40
+ children: (
41
+ <div className="flex items-center h-[36px]">
42
+ <span className="mr-3">Child component</span>
43
+ </div>
44
+ ),
45
+ };
46
+
47
+ export const DropdownExample = Template.bind({});
48
+ DropdownExample.args = {
49
+ isCollapsed: false,
50
+ onExpand: () => {
51
+ console.log('expanded');
52
+ },
53
+ children: (
54
+ <div className="border-l border-blue-200 pl-1 w-[204px]">
55
+ <SourceDropdown
56
+ selectedSource={sampleSources[0] as ResourceBrowserSource}
57
+ sources={sampleSources as ResourceBrowserSource[]}
58
+ onSourceSelect={(source) => alert(`Source Select: ${source.name}`)}
59
+ />
60
+ </div>
61
+ ),
62
+ };
@@ -0,0 +1,27 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ import { AdsClickIcon } from '../Icons/AdsClickIcon';
3
+
4
+ export default function SourceDropdownContainer({
5
+ children,
6
+ isCollapsed,
7
+ onExpand,
8
+ }: PropsWithChildren<{ isCollapsed: boolean; onExpand: () => void }>) {
9
+ return (
10
+ <div className="inline-flex rounded-lg p-1 bg-blue-100 min-h-[44px]">
11
+ {isCollapsed && (
12
+ <button aria-label="Expand browse options" onClick={onExpand} className="flex items-center">
13
+ <AdsClickIcon aria-hidden className="mx-2 fill-blue-300" />
14
+ </button>
15
+ )}
16
+ {!isCollapsed && (
17
+ <>
18
+ <div className="flex items-center pr-1">
19
+ <AdsClickIcon aria-hidden className="mx-2 fill-blue-300" />
20
+ <span className="sr-only">Browse</span>
21
+ </div>
22
+ {children}
23
+ </>
24
+ )}
25
+ </div>
26
+ );
27
+ }
@@ -1,4 +1,4 @@
1
- import { ResourceBrowserSource, ResourceBrowserResource, ResourceBrowserPlugin } from '../types';
1
+ import { ResourceBrowserSource, ResourceBrowserResource, ResourceBrowserPlugin, ResourceBrowserSourceWithPlugin } from '../types';
2
2
 
3
3
  export type DeepPartial<T> = {
4
4
  [P in keyof T]?: T[P] extends Array<infer U>
@@ -8,7 +8,7 @@ export type DeepPartial<T> = {
8
8
  : DeepPartial<T[P]>;
9
9
  };
10
10
 
11
- export const mockSource = (properties: DeepPartial<ResourceBrowserSource> = {}): ResourceBrowserSource => {
11
+ export const mockSource = (properties: Partial<ResourceBrowserSource> = {}): ResourceBrowserSource => {
12
12
  return {
13
13
  id: '1',
14
14
  name: 'Test source',
@@ -17,6 +17,16 @@ export const mockSource = (properties: DeepPartial<ResourceBrowserSource> = {}):
17
17
  };
18
18
  };
19
19
 
20
+ export const mockSourceWithPlugin = (properties: Partial<ResourceBrowserSourceWithPlugin> = {}): ResourceBrowserSourceWithPlugin => {
21
+ return {
22
+ id: '1',
23
+ name: 'Test source',
24
+ type: 'dam',
25
+ plugin: mockPlugin(),
26
+ ...properties,
27
+ };
28
+ };
29
+
20
30
  export const mockPlugin = (properties: DeepPartial<ResourceBrowserPlugin> = {}): ResourceBrowserPlugin => {
21
31
  return {
22
32
  type: 'dam',
@@ -26,6 +36,10 @@ export const mockPlugin = (properties: DeepPartial<ResourceBrowserPlugin> = {}):
26
36
  renderSelectedResource: jest.fn(),
27
37
  // @ts-ignore
28
38
  resolveResource: jest.fn(),
39
+ // @ts-ignore
40
+ sourceSearchComponent: jest.fn(),
41
+ // @ts-ignore
42
+ renderResourceLauncher: jest.fn(),
29
43
  ...properties,
30
44
  };
31
45
  };
@@ -94,5 +94,13 @@ export default (): ResourceBrowserPlugin => {
94
94
  isLoading: false,
95
95
  };
96
96
  },
97
+
98
+ // TODO: as they are worked out
99
+ sourceSearchComponent: () => {
100
+ return () => <></>;
101
+ },
102
+ renderResourceLauncher: () => {
103
+ return () => <></>;
104
+ },
97
105
  };
98
106
  };
@@ -22,7 +22,7 @@ export const createPlugins = (callbackWait: number, headerPortal = false): Resou
22
22
  return (props) => {
23
23
  return (
24
24
  <div className="h-screen lg:h-[calc(100vh-9rem)] w-screen max-w-[52rem]">
25
- <div>THIS IS A {type} PLUGIN</div>
25
+ <div>THIS IS A {type} BROWSE PLUGIN</div>
26
26
  <button
27
27
  onClick={() => {
28
28
  props.onSelected({
@@ -38,6 +38,42 @@ export const createPlugins = (callbackWait: number, headerPortal = false): Resou
38
38
  );
39
39
  };
40
40
  },
41
+ sourceSearchComponent: () => {
42
+ return (props) => {
43
+ return (
44
+ <div className="h-screen lg:h-[calc(100vh-9rem)] w-screen max-w-[52rem]">
45
+ <div>THIS IS A {type} SEARCH PLUGIN</div>
46
+ <div>Received query: {props.query}</div>
47
+ <button
48
+ onClick={() => {
49
+ props.onSelected({
50
+ id: '1f7a25b4-380f-4540-9555-8be2dcab4019',
51
+ source: props.source,
52
+ } as ResourceBrowserResource);
53
+ }}
54
+ >
55
+ Press to set resource
56
+ </button>
57
+ {headerPortal && props.headerPortal && createPortal(<div>HeadPortal</div>, props.headerPortal)}
58
+ </div>
59
+ );
60
+ };
61
+ },
62
+ renderResourceLauncher: () => {
63
+ return (props) => {
64
+ const [query, setQuery] = useState<string>('');
65
+ return (
66
+ <div className="">
67
+ <div>Search {type}</div>
68
+ <form>
69
+ <input onChange={(event) => setQuery(event.target.value)} />
70
+ <button onClick={() => props.onSearch(query)}>Search</button>
71
+ </form>
72
+ <button onClick={props.onBrowse}>Browse</button>
73
+ </div>
74
+ );
75
+ };
76
+ },
41
77
  renderSelectedResource: (resource) => {
42
78
  return new Promise((resolve, reject) => {
43
79
  const fileSize = resource.squizImage?.imageVariations?.original?.byteSize;
@@ -9,6 +9,7 @@ export const renderWithContext = (ui: ReactElement, context: Partial<ResourceBro
9
9
  value={{
10
10
  onRequestSources: jest.fn(),
11
11
  plugins: [],
12
+ searchEnabled: false,
12
13
  ...context,
13
14
  }}
14
15
  >