@squiz/resource-browser 3.3.8 → 3.3.10

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 3.3.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 8d3c8e0: source alias support
8
+ - Updated dependencies [8d3c8e0]
9
+ - @squiz/resource-browser-ui-lib@1.2.6
10
+
11
+ ## 3.3.9
12
+
13
+ ### Patch Changes
14
+
15
+ - 53de26d: add ref support and modal state change callback
16
+ - Updated dependencies [53de26d]
17
+ - @squiz/resource-browser-ui-lib@1.2.5
18
+
3
19
  ## 3.3.8
4
20
 
5
21
  ### Patch Changes
@@ -14,5 +14,6 @@ export type PluginRenderType = ResourceBrowserInputProps & {
14
14
  inline: boolean;
15
15
  inlineType?: InlineType;
16
16
  onRetry: () => void;
17
+ ref?: React.Ref<HTMLButtonElement>;
17
18
  };
18
- export declare const PluginRender: ({ render, inline, inlineType, ...props }: PluginRenderType) => React.JSX.Element;
19
+ export declare const PluginRender: React.ForwardRefExoticComponent<Omit<PluginRenderType, "ref"> & React.RefAttributes<HTMLButtonElement>>;
@@ -1,18 +1,37 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
4
24
  };
5
25
  Object.defineProperty(exports, "__esModule", { value: true });
6
26
  exports.PluginRender = void 0;
7
- const react_1 = __importDefault(require("react"));
27
+ const react_1 = __importStar(require("react"));
8
28
  const ResourceBrowserInput_1 = require("../ResourceBrowserInput/ResourceBrowserInput");
9
29
  const ResourceBrowserInlineButton_1 = require("../ResourceBrowserInlineButton/ResourceBrowserInlineButton");
10
30
  const AuthProvider_1 = require("../ResourceBrowserContext/AuthProvider");
11
- const PluginRender = ({ render, inline, inlineType, ...props }) => {
31
+ exports.PluginRender = (0, react_1.forwardRef)(({ render, inline, inlineType, ...props }, forwardRef) => {
12
32
  if (!render)
13
33
  return react_1.default.createElement(react_1.default.Fragment, null);
14
34
  const requiresAuth = Boolean(props.source?.configuration?.authUrl);
15
- const content = inline && inlineType ? react_1.default.createElement(ResourceBrowserInlineButton_1.ResourceBrowserInlineButton, { inlineType: inlineType, ...props }) : react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props });
35
+ const content = inline && inlineType ? (react_1.default.createElement(ResourceBrowserInlineButton_1.ResourceBrowserInlineButton, { ref: forwardRef, inlineType: inlineType, ...props })) : (react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props }));
16
36
  return requiresAuth ? react_1.default.createElement(AuthProvider_1.AuthProvider, { authConfig: props.source }, content) : content;
17
- };
18
- exports.PluginRender = PluginRender;
37
+ });
@@ -5,4 +5,7 @@ export type ResourceBrowserInlineButtonProps = ResourceBrowserInputProps & {
5
5
  inlineType: InlineType;
6
6
  onRetry: () => void;
7
7
  };
8
- export declare const ResourceBrowserInlineButton: ({ inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }: ResourceBrowserInlineButtonProps) => React.JSX.Element;
8
+ export declare const ResourceBrowserInlineButton: React.ForwardRefExoticComponent<ResourceBrowserInputProps & {
9
+ inlineType: InlineType;
10
+ onRetry: () => void;
11
+ } & React.RefAttributes<HTMLButtonElement>>;
@@ -34,7 +34,8 @@ const InlineLoadingErrorState_1 = require("./InlineLoadingErrorState/InlineLoadi
34
34
  const ImageInline_1 = require("../Icons/ImageInline");
35
35
  const LinkInline_1 = require("../Icons/LinkInline");
36
36
  const AdsClickIcon_1 = require("../Icons/AdsClickIcon");
37
- const ResourceBrowserInlineButton = ({ inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }) => {
37
+ exports.ResourceBrowserInlineButton = (0, react_1.forwardRef)((props, forwardRef) => {
38
+ const { inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, } = props;
38
39
  // If an error happens loading the resource the inline browser just opens without it as if no preselectedResource is provided
39
40
  const { data: resource, isLoading: isResourceLoading } = useResource(value || null, source);
40
41
  const inlineTypePickerLabels = (0, react_1.useMemo)(() => ({
@@ -48,12 +49,11 @@ const ResourceBrowserInlineButton = ({ inlineType, modalTitle, allowedTypes, onC
48
49
  resource: react_1.default.createElement(AdsClickIcon_1.AdsClickIcon, { "aria-label": "change resource" }),
49
50
  }), [inlineType]);
50
51
  return (react_1.default.createElement("div", { className: "inline-launch-button" },
51
- react_1.default.createElement(resource_browser_ui_lib_1.ModalTrigger, { overlayTriggerState: {
52
+ react_1.default.createElement(resource_browser_ui_lib_1.ModalTrigger, { ref: forwardRef, overlayTriggerState: {
52
53
  isOpen: isModalOpen,
53
54
  onOpenChange: onModalStateChange,
54
55
  }, showLabel: false, label: "", icon: inlineTypePickerIcons[inlineType], isDisabled: isDisabled, scope: "squiz-rb-scope", containerClasses: "inline-launch-button__button" }, (onClose, titleProps) => (react_1.default.createElement(react_1.default.Fragment, null,
55
56
  (isLoading || isResourceLoading || error) && (react_1.default.createElement(InlineLoadingErrorState_1.InlineLoadingErrorState, { title: modalTitle, titleAriaProps: titleProps, onClose: onClose, onRetry: onRetry, isLoading: isLoading || isResourceLoading, error: error })),
56
57
  !(isLoading || isResourceLoading) && !error && (react_1.default.createElement(MainContainer_1.default, { selectedSource: source, sources: sources, preselectedResource: resource, plugin: plugin, pluginMode: pluginMode, searchEnabled: searchEnabled, title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onSourceSelect: setSource, onClose: onClose, onChange: onChange }))))),
57
58
  react_1.default.createElement("div", { className: "inline-launch-button__label" }, inlineTypePickerLabels[inlineType])));
58
- };
59
- exports.ResourceBrowserInlineButton = ResourceBrowserInlineButton;
59
+ });
package/lib/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export type ResourceBrowserProps = {
16
16
  inline?: boolean;
17
17
  inlineType?: InlineType;
18
18
  onChange(resource: ResourceBrowserResource | null): void;
19
+ onModalStateChange?(isOpen: boolean): void;
19
20
  onClear?(): void;
20
21
  };
21
- export declare const ResourceBrowser: (props: ResourceBrowserProps) => React.JSX.Element;
22
+ export declare const ResourceBrowser: React.ForwardRefExoticComponent<ResourceBrowserProps & React.RefAttributes<HTMLButtonElement>>;
package/lib/index.js CHANGED
@@ -47,8 +47,8 @@ exports.SourceDropdown = SourceDropdown_1.default;
47
47
  const SourceDropdownContainer_1 = __importDefault(require("./SourceDropdownContainer/SourceDropdownContainer"));
48
48
  exports.SourceDropdownContainer = SourceDropdownContainer_1.default;
49
49
  __exportStar(require("./types"), exports);
50
- const ResourceBrowser = (props) => {
51
- const { value, inline, inlineType, allowedPlugins } = props;
50
+ exports.ResourceBrowser = react_1.default.forwardRef((props, forwardRef) => {
51
+ const { value, inline, inlineType, allowedPlugins, onModalStateChange: onModalStateChangeExternalNotification } = props;
52
52
  const [error, setError] = (0, react_1.useState)(null);
53
53
  const { onRequestSources, searchEnabled, plugins: allPlugins } = (0, react_1.useContext)(ResourceBrowserContext_1.ResourceBrowserContext);
54
54
  const [isModalOpen, setIsModalOpen] = (0, react_1.useState)(false);
@@ -64,6 +64,23 @@ const ResourceBrowser = (props) => {
64
64
  const [mode, setMode] = (0, react_1.useState)(null);
65
65
  const { data: sources, isLoading, error: sourcesError, reload: reloadSources } = (0, useSources_1.useSources)({ onRequestSources, plugins });
66
66
  const plugin = source?.plugin || null;
67
+ // Find source by its id or alias
68
+ const findSourceById = (value, sources) => {
69
+ let newSource = sources.find((source) => source.id === value?.sourceId) || null;
70
+ if (!newSource) {
71
+ newSource =
72
+ sources.find((source) => {
73
+ if (!source.aliases)
74
+ return false;
75
+ return source.aliases.includes(value.sourceId);
76
+ }) || null;
77
+ }
78
+ return newSource;
79
+ };
80
+ // Check if the value is for the source
81
+ const isValueForSource = (value, source) => {
82
+ return value?.sourceId === source.id || (source.aliases && source.aliases.includes(value?.sourceId));
83
+ };
67
84
  // 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
68
85
  const handleSourceSelect = (0, react_1.useCallback)((source, mode) => {
69
86
  setSource(source);
@@ -79,7 +96,7 @@ const ResourceBrowser = (props) => {
79
96
  // If there is a provided value try to use its source
80
97
  if (value) {
81
98
  // Search the sources for it matching against the value.source property
82
- newSource = sources.find((source) => source.id === value?.sourceId) || null;
99
+ newSource = findSourceById(value, sources);
83
100
  // If the source is null and we arent loading the sources
84
101
  if (newSource === null && !isLoading) {
85
102
  // Set an error as the passed in value's source wasnt returned by onRequestSources
@@ -96,7 +113,8 @@ const ResourceBrowser = (props) => {
96
113
  // The modal has some control over it own open/closed state (for WCAG reasons) so keep this in sync with our state
97
114
  const handleModalStateChange = (0, react_1.useCallback)((isOpen) => {
98
115
  setIsModalOpen(isOpen);
99
- }, [setIsModalOpen]);
116
+ onModalStateChangeExternalNotification?.(isOpen);
117
+ }, [setIsModalOpen, onModalStateChangeExternalNotification]);
100
118
  // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
101
119
  (0, react_1.useEffect)(() => {
102
120
  // If modal is closed and we dont have a value
@@ -109,8 +127,7 @@ const ResourceBrowser = (props) => {
109
127
  setMode(null);
110
128
  // If there is a value passed in, reset the source so the preselected asset preview renders correctly
111
129
  if (value && value.sourceId !== source?.id) {
112
- const source = sources.find((source) => source.id === value?.sourceId) || null;
113
- setSource(source);
130
+ setSource(findSourceById(value, sources));
114
131
  }
115
132
  }
116
133
  }, [sources, isModalOpen]);
@@ -120,7 +137,7 @@ const ResourceBrowser = (props) => {
120
137
  }, [reloadSources]);
121
138
  // Render a default "plugin" and one for each item in the plugins array. They are conditionally rendered based on what is selected
122
139
  return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
123
- react_1.default.createElement(Plugin_1.PluginRender, { key: "default", type: null, render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
140
+ react_1.default.createElement(Plugin_1.PluginRender, { key: "default", ref: forwardRef, type: null, render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
124
141
  return {
125
142
  data: null,
126
143
  error: null,
@@ -128,7 +145,6 @@ const ResourceBrowser = (props) => {
128
145
  };
129
146
  }, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }),
130
147
  plugins.map((thisPlugin) => {
131
- return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, type: thisPlugin.type, render: thisPlugin.type === plugin?.type, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (value.sourceId === source.id ? value : null) : null, isOtherSourceValue: value && source ? (value.sourceId !== source.id ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
148
+ return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, ref: forwardRef, type: thisPlugin.type, render: thisPlugin.type === plugin?.type, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (isValueForSource(value, source) ? value : null) : null, isOtherSourceValue: value && source ? (!isValueForSource(value, source) ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
132
149
  })));
133
- };
134
- exports.ResourceBrowser = ResourceBrowser;
150
+ });
package/lib/types.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface ResourceBrowserSource {
13
13
  name?: string;
14
14
  id: string;
15
15
  type: ResourceBrowserPluginType;
16
+ aliases?: string[];
16
17
  }
17
18
  export interface ResourceBrowserSourceWithPlugin extends ResourceBrowserSource {
18
19
  plugin: ResourceBrowserPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "3.3.8",
3
+ "version": "3.3.10",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -28,7 +28,7 @@
28
28
  "@react-types/shared": "^3.23.1",
29
29
  "@squiz/dx-json-schema-lib": "^1.67.0",
30
30
  "@squiz/generic-browser-lib": "^1.67.5",
31
- "@squiz/resource-browser-ui-lib": "^1.2.4",
31
+ "@squiz/resource-browser-ui-lib": "^1.2.6",
32
32
  "clsx": "^2.1.0",
33
33
  "expiry-map": "^2.0.0",
34
34
  "p-memoize": "^4.0.4",
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { render, waitFor } from '@testing-library/react';
3
3
 
4
4
  import { PluginRender } from './Plugin';
@@ -8,7 +8,9 @@ import * as RBInlineButton from '../ResourceBrowserInlineButton/ResourceBrowserI
8
8
  import * as AuthContext from '../ResourceBrowserContext/AuthProvider';
9
9
 
10
10
  jest.spyOn(RBI, 'ResourceBrowserInput');
11
- jest.spyOn(RBInlineButton, 'ResourceBrowserInlineButton');
11
+ jest.mock('../ResourceBrowserInlineButton/ResourceBrowserInlineButton', () => ({
12
+ ResourceBrowserInlineButton: jest.fn(() => <div data-testid="mocked-inline-button" />),
13
+ }));
12
14
  jest.spyOn(AuthContext, 'AuthProvider');
13
15
 
14
16
  const baseProps = {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
3
3
  import { ResourceBrowserInlineButton } from '../ResourceBrowserInlineButton/ResourceBrowserInlineButton';
4
4
  import { AuthProvider } from '../ResourceBrowserContext/AuthProvider';
@@ -17,14 +17,19 @@ export type PluginRenderType = ResourceBrowserInputProps & {
17
17
  inline: boolean;
18
18
  inlineType?: InlineType;
19
19
  onRetry: () => void;
20
+ ref?: React.Ref<HTMLButtonElement>;
20
21
  };
21
- export const PluginRender = ({ render, inline, inlineType, ...props }: PluginRenderType) => {
22
+ export const PluginRender = forwardRef<HTMLButtonElement, PluginRenderType>(({ render, inline, inlineType, ...props }, forwardRef) => {
22
23
  if (!render) return <></>;
23
24
 
24
25
  const requiresAuth = Boolean((props.source as ResourceBrowserSourceWithConfig)?.configuration?.authUrl);
25
26
 
26
27
  const content =
27
- inline && inlineType ? <ResourceBrowserInlineButton inlineType={inlineType} {...props} /> : <ResourceBrowserInput {...props} />;
28
+ inline && inlineType ? (
29
+ <ResourceBrowserInlineButton ref={forwardRef} inlineType={inlineType} {...props} />
30
+ ) : (
31
+ <ResourceBrowserInput {...props} />
32
+ );
28
33
 
29
34
  return requiresAuth ? <AuthProvider authConfig={props.source}>{content}</AuthProvider> : content;
30
- };
35
+ });
@@ -1,4 +1,4 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { forwardRef, useMemo } from 'react';
2
2
  import MainContainer from '../MainContainer/MainContainer';
3
3
  import { ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
4
4
  import { ModalTrigger } from '@squiz/resource-browser-ui-lib';
@@ -13,26 +13,28 @@ export type ResourceBrowserInlineButtonProps = ResourceBrowserInputProps & {
13
13
  onRetry: () => void;
14
14
  };
15
15
 
16
- export const ResourceBrowserInlineButton = ({
17
- inlineType,
18
- modalTitle,
19
- allowedTypes,
20
- onChange,
21
- onRetry,
22
- value,
23
- useResource,
24
- isDisabled,
25
- plugin,
26
- pluginMode,
27
- searchEnabled,
28
- source,
29
- sources,
30
- isLoading,
31
- error,
32
- setSource,
33
- isModalOpen,
34
- onModalStateChange,
35
- }: ResourceBrowserInlineButtonProps) => {
16
+ export const ResourceBrowserInlineButton = forwardRef<HTMLButtonElement, ResourceBrowserInlineButtonProps>((props, forwardRef) => {
17
+ const {
18
+ inlineType,
19
+ modalTitle,
20
+ allowedTypes,
21
+ onChange,
22
+ onRetry,
23
+ value,
24
+ useResource,
25
+ isDisabled,
26
+ plugin,
27
+ pluginMode,
28
+ searchEnabled,
29
+ source,
30
+ sources,
31
+ isLoading,
32
+ error,
33
+ setSource,
34
+ isModalOpen,
35
+ onModalStateChange,
36
+ } = props;
37
+
36
38
  // If an error happens loading the resource the inline browser just opens without it as if no preselectedResource is provided
37
39
  const { data: resource, isLoading: isResourceLoading } = useResource(value || null, source);
38
40
 
@@ -56,6 +58,7 @@ export const ResourceBrowserInlineButton = ({
56
58
  return (
57
59
  <div className="inline-launch-button">
58
60
  <ModalTrigger
61
+ ref={forwardRef}
59
62
  overlayTriggerState={{
60
63
  isOpen: isModalOpen,
61
64
  onOpenChange: onModalStateChange,
@@ -101,4 +104,4 @@ export const ResourceBrowserInlineButton = ({
101
104
  <div className="inline-launch-button__label">{inlineTypePickerLabels[inlineType]}</div>
102
105
  </div>
103
106
  );
104
- };
107
+ });
@@ -3,7 +3,8 @@
3
3
  "name": "Bynder #1",
4
4
  "id": "c90feac1-55f3-4e1f-9b56-c22829e3f510",
5
5
  "type": "dam",
6
- "group": "DAM"
6
+ "group": "DAM",
7
+ "aliases": ["bynder-789"]
7
8
  },
8
9
  {
9
10
  "name": "Bynder #2",
@@ -141,6 +141,24 @@ describe('Resource browser input', () => {
141
141
  });
142
142
  });
143
143
 
144
+ it('If an aliased resource is provided will default to its true Source and Plugin to match', async () => {
145
+ const source = mockSource({ type: 'dam', aliases: ['alias-source-id'] });
146
+ mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]);
147
+ mockResolveResource.mockResolvedValueOnce(mockResource({ source }));
148
+ renderComponent({ value: { sourceId: 'alias-source-id', resourceId: '100' } });
149
+
150
+ await waitFor(() => {
151
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
152
+ expect.objectContaining({
153
+ source: calculateExpectedSource(source),
154
+ plugin: mockDamPlugin,
155
+ pluginMode: null,
156
+ }),
157
+ {},
158
+ );
159
+ });
160
+ });
161
+
144
162
  it('If a resource is provided but its Source cannot be found it will error', async () => {
145
163
  const source = mockSource({ type: 'dam' });
146
164
  mockRequestSources.mockResolvedValueOnce([source, mockSource({ type: 'matrix' })]);
@@ -510,6 +528,73 @@ describe('Resource browser input', () => {
510
528
  });
511
529
  });
512
530
 
531
+ it('onModalStateChange called with false will reset the Source and Plugin to the aliases true source if the value uses an alias', async () => {
532
+ const originalSource = mockSource({
533
+ id: 'original-source-id',
534
+ name: 'Original Source',
535
+ type: 'dam',
536
+ aliases: ['alias-source-id'],
537
+ });
538
+ const aliasSourceId = 'alias-source-id';
539
+ const valueWithAlias = {
540
+ sourceId: aliasSourceId,
541
+ resourceId: '123456',
542
+ };
543
+
544
+ const sourcesInput = [originalSource, mockSource({ id: '2' })];
545
+ const calculatedSources = sourcesInput.map((source) => calculateExpectedSource(source));
546
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
547
+
548
+ renderComponent({ value: valueWithAlias });
549
+
550
+ await waitFor(() => {
551
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
552
+ expect.objectContaining({
553
+ value: valueWithAlias,
554
+ sources: calculatedSources,
555
+ source: calculatedSources[0],
556
+ plugin: mockDamPlugin,
557
+ }),
558
+ {},
559
+ );
560
+ });
561
+
562
+ const { onModalStateChange, setSource } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
563
+ // Invoke open and close the modal
564
+ act(() => {
565
+ onModalStateChange(true);
566
+ });
567
+
568
+ // Change the source
569
+ act(() => {
570
+ setSource(calculatedSources[1]);
571
+ });
572
+
573
+ await waitFor(() => {
574
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
575
+ expect.objectContaining({
576
+ source: calculatedSources[1],
577
+ plugin: mockDamPlugin,
578
+ }),
579
+ {},
580
+ );
581
+ });
582
+
583
+ act(() => {
584
+ onModalStateChange(false);
585
+ });
586
+
587
+ await waitFor(() => {
588
+ expect(RBI.ResourceBrowserInput).toHaveBeenLastCalledWith(
589
+ expect.objectContaining({
590
+ source: calculatedSources[0],
591
+ plugin: mockDamPlugin,
592
+ }),
593
+ {},
594
+ );
595
+ });
596
+ });
597
+
513
598
  it('onModalStateChange called with false will reset the Mode', async () => {
514
599
  const sourcesInput = [mockSource({ type: 'dam' }), mockSource()];
515
600
  mockRequestSources.mockResolvedValueOnce(sourcesInput);
@@ -568,12 +653,47 @@ describe('Resource browser input', () => {
568
653
  });
569
654
  });
570
655
 
656
+ it('onModalStateChange calls onModalStateChangeExternalNotification when provided', async () => {
657
+ const mockOnModalStateChange = jest.fn();
658
+ const sourcesInput = [mockSource({ type: 'dam' })];
659
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
660
+
661
+ renderComponent({ onModalStateChange: mockOnModalStateChange });
662
+ await waitFor(() => {
663
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalled();
664
+ });
665
+
666
+ const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
667
+
668
+ // Test opening modal
669
+ act(() => {
670
+ onModalStateChange(true);
671
+ });
672
+
673
+ await waitFor(() => {
674
+ expect(mockOnModalStateChange).toHaveBeenCalledWith(true);
675
+ });
676
+
677
+ // Test closing modal
678
+ act(() => {
679
+ onModalStateChange(false);
680
+ });
681
+
682
+ await waitFor(() => {
683
+ expect(mockOnModalStateChange).toHaveBeenCalledWith(false);
684
+ });
685
+
686
+ expect(mockOnModalStateChange).toHaveBeenCalledTimes(2);
687
+ });
688
+
571
689
  describe('Resource browser plugin', () => {
690
+ let PluginRenderSpy: jest.SpyInstance;
572
691
  beforeEach(() => {
573
- jest.spyOn(Plugin, 'PluginRender');
692
+ //@ts-ignore
693
+ PluginRenderSpy = jest.spyOn(Plugin.PluginRender, 'render');
574
694
  });
575
695
  afterEach(() => {
576
- (Plugin.PluginRender as jest.Mock).mockRestore();
696
+ PluginRenderSpy.mockRestore();
577
697
  });
578
698
 
579
699
  it('Will default to a non plugin based render for initial load and selection of first source', async () => {
@@ -583,29 +703,29 @@ describe('Resource browser input', () => {
583
703
 
584
704
  // Will render a default with no selected source
585
705
  await waitFor(() => {
586
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
706
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
587
707
  expect.objectContaining({
588
708
  render: true,
589
709
  type: null,
590
710
  plugin: null,
591
711
  }),
592
- {},
712
+ null,
593
713
  );
594
714
 
595
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
715
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
596
716
  expect.objectContaining({
597
717
  render: false,
598
718
  type: 'dam',
599
719
  }),
600
- {},
720
+ null,
601
721
  );
602
722
 
603
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
723
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
604
724
  expect.objectContaining({
605
725
  render: false,
606
726
  type: 'matrix',
607
727
  }),
608
- {},
728
+ null,
609
729
  );
610
730
  });
611
731
  });
@@ -619,28 +739,28 @@ describe('Resource browser input', () => {
619
739
 
620
740
  // Will render a default with no selected source
621
741
  await waitFor(() => {
622
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
742
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
623
743
  expect.objectContaining({
624
744
  render: false,
625
745
  type: null,
626
746
  }),
627
- {},
747
+ null,
628
748
  );
629
749
 
630
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
750
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
631
751
  expect.objectContaining({
632
752
  render: true,
633
753
  type: 'dam',
634
754
  }),
635
- {},
755
+ null,
636
756
  );
637
757
 
638
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
758
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
639
759
  expect.objectContaining({
640
760
  render: false,
641
761
  type: 'matrix',
642
762
  }),
643
- {},
763
+ null,
644
764
  );
645
765
  });
646
766
 
@@ -672,28 +792,28 @@ describe('Resource browser input', () => {
672
792
 
673
793
  // Will render a default with no selected source
674
794
  await waitFor(() => {
675
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
795
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
676
796
  expect.objectContaining({
677
797
  render: true,
678
798
  type: null,
679
799
  }),
680
- {},
800
+ null,
681
801
  );
682
802
 
683
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
803
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
684
804
  expect.objectContaining({
685
805
  render: true,
686
806
  type: 'dam',
687
807
  }),
688
- {},
808
+ null,
689
809
  );
690
810
 
691
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
811
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
692
812
  expect.objectContaining({
693
813
  render: false,
694
814
  type: 'matrix',
695
815
  }),
696
- {},
816
+ null,
697
817
  );
698
818
  });
699
819
  });
@@ -724,20 +844,20 @@ describe('Resource browser input', () => {
724
844
  );
725
845
  });
726
846
 
727
- (Plugin.PluginRender as jest.Mock).mockClear();
847
+ (PluginRenderSpy as unknown as jest.Mock).mockClear();
728
848
 
729
849
  act(() => {
730
850
  setSource({ ...matrixSource, plugin: pluginB });
731
851
  });
732
852
 
733
853
  await waitFor(() => {
734
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
854
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
735
855
  expect.objectContaining({
736
856
  plugin: pluginB,
737
857
  }),
738
858
  expect.any(Object),
739
859
  );
740
- expect(Plugin.PluginRender).not.toHaveBeenCalledWith(
860
+ expect(PluginRenderSpy).not.toHaveBeenCalledWith(
741
861
  expect.objectContaining({
742
862
  plugin: pluginA,
743
863
  }),
@@ -745,5 +865,49 @@ describe('Resource browser input', () => {
745
865
  );
746
866
  });
747
867
  });
868
+
869
+ it('When resource browser is called with a value containing an alias source, Plugin is called with the original source not the alias value', async () => {
870
+ const originalSource = mockSource({
871
+ id: 'original-source-id',
872
+ name: 'Original Source',
873
+ type: 'dam',
874
+ aliases: ['alias-source-id'],
875
+ });
876
+ const aliasSourceId = 'alias-source-id';
877
+
878
+ mockRequestSources.mockResolvedValueOnce([originalSource]);
879
+
880
+ // Create a value that uses the alias source ID
881
+ const valueWithAlias = {
882
+ sourceId: aliasSourceId,
883
+ resourceId: '123456',
884
+ };
885
+
886
+ renderComponent({ value: valueWithAlias });
887
+
888
+ await waitFor(() => {
889
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalledWith(
890
+ expect.objectContaining({
891
+ value: valueWithAlias,
892
+ source: calculateExpectedSource(originalSource), // Should be the original source, not alias
893
+ plugin: mockDamPlugin,
894
+ }),
895
+ {},
896
+ );
897
+ });
898
+
899
+ // Verify that the PluginRender is called with the original source
900
+ await waitFor(() => {
901
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
902
+ expect.objectContaining({
903
+ render: true,
904
+ type: 'dam',
905
+ source: calculateExpectedSource(originalSource), // Should be the original source
906
+ isOtherSourceValue: false,
907
+ }),
908
+ null,
909
+ );
910
+ });
911
+ });
748
912
  });
749
913
  });
@@ -103,6 +103,15 @@ RestrictedSource.args = {
103
103
  searchEnabled: true,
104
104
  };
105
105
 
106
+ export const SourceByAlias = Template.bind({});
107
+ SourceByAlias.args = {
108
+ ...Primary.args,
109
+ value: {
110
+ resourceId: '1f7a25b4-380f-4540-9555-8be2dcab4019',
111
+ sourceId: 'bynder-789',
112
+ },
113
+ };
114
+
106
115
  export const SearchEnabled = Template.bind({});
107
116
  SearchEnabled.args = {
108
117
  ...Primary.args,
package/src/index.tsx CHANGED
@@ -38,11 +38,12 @@ export type ResourceBrowserProps = {
38
38
  inline?: boolean; // Will render open button only, no input / showing of existing selection
39
39
  inlineType?: InlineType; // Type of inline button to show
40
40
  onChange(resource: ResourceBrowserResource | null): void;
41
+ onModalStateChange?(isOpen: boolean): void;
41
42
  onClear?(): void;
42
43
  };
43
44
 
44
- export const ResourceBrowser = (props: ResourceBrowserProps) => {
45
- const { value, inline, inlineType, allowedPlugins } = props;
45
+ export const ResourceBrowser = React.forwardRef<HTMLButtonElement, ResourceBrowserProps>((props, forwardRef) => {
46
+ const { value, inline, inlineType, allowedPlugins, onModalStateChange: onModalStateChangeExternalNotification } = props;
46
47
  const [error, setError] = useState<Error | null>(null);
47
48
  const { onRequestSources, searchEnabled, plugins: allPlugins } = useContext(ResourceBrowserContext);
48
49
 
@@ -59,6 +60,24 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
59
60
  const { data: sources, isLoading, error: sourcesError, reload: reloadSources } = useSources({ onRequestSources, plugins });
60
61
  const plugin = source?.plugin || null;
61
62
 
63
+ // Find source by its id or alias
64
+ const findSourceById = (value: ResourceBrowserUnresolvedResource, sources: ResourceBrowserSourceWithPlugin[]) => {
65
+ let newSource = sources.find((source) => source.id === value?.sourceId) || null;
66
+ if (!newSource) {
67
+ newSource =
68
+ sources.find((source) => {
69
+ if (!source.aliases) return false;
70
+ return source.aliases.includes(value.sourceId);
71
+ }) || null;
72
+ }
73
+ return newSource;
74
+ };
75
+
76
+ // Check if the value is for the source
77
+ const isValueForSource = (value: ResourceBrowserUnresolvedResource, source: ResourceBrowserSourceWithPlugin) => {
78
+ return value?.sourceId === source.id || (source.aliases && source.aliases.includes(value?.sourceId));
79
+ };
80
+
62
81
  // 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
63
82
  const handleSourceSelect = useCallback(
64
83
  (source: ResourceBrowserSourceWithPlugin, mode?: PluginLaunchMode) => {
@@ -79,7 +98,8 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
79
98
  // If there is a provided value try to use its source
80
99
  if (value) {
81
100
  // Search the sources for it matching against the value.source property
82
- newSource = sources.find((source) => source.id === value?.sourceId) || null;
101
+ newSource = findSourceById(value, sources);
102
+
83
103
  // If the source is null and we arent loading the sources
84
104
  if (newSource === null && !isLoading) {
85
105
  // Set an error as the passed in value's source wasnt returned by onRequestSources
@@ -98,8 +118,9 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
98
118
  const handleModalStateChange = useCallback(
99
119
  (isOpen: boolean) => {
100
120
  setIsModalOpen(isOpen);
121
+ onModalStateChangeExternalNotification?.(isOpen);
101
122
  },
102
- [setIsModalOpen],
123
+ [setIsModalOpen, onModalStateChangeExternalNotification],
103
124
  );
104
125
 
105
126
  // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
@@ -114,8 +135,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
114
135
 
115
136
  // If there is a value passed in, reset the source so the preselected asset preview renders correctly
116
137
  if (value && value.sourceId !== source?.id) {
117
- const source = sources.find((source) => source.id === value?.sourceId) || null;
118
- setSource(source);
138
+ setSource(findSourceById(value, sources));
119
139
  }
120
140
  }
121
141
  }, [sources, isModalOpen]);
@@ -130,6 +150,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
130
150
  <div className="squiz-rb-scope">
131
151
  <PluginRender
132
152
  key="default"
153
+ ref={forwardRef} // This is passed to every plugin but only actually render on one at a time
133
154
  type={null}
134
155
  render={plugin === null}
135
156
  inline={!!inline}
@@ -159,13 +180,14 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
159
180
  return (
160
181
  <PluginRender
161
182
  key={thisPlugin.type}
183
+ ref={forwardRef} // This is passed to every plugin but only actually render on one at a time
162
184
  type={thisPlugin.type}
163
185
  render={thisPlugin.type === plugin?.type}
164
186
  inline={!!inline}
165
187
  inlineType={inlineType}
166
188
  {...props}
167
- value={value && source ? (value.sourceId === source.id ? value : null) : null} // Don't pass value if value isn't for selected source
168
- isOtherSourceValue={value && source ? (value.sourceId !== source.id ? true : false) : false} // Block render of existing asset if not from this source
189
+ value={value && source ? (isValueForSource(value, source) ? value : null) : null} // Don't pass value if value isn't for selected source
190
+ isOtherSourceValue={value && source ? (!isValueForSource(value, source) ? true : false) : false} // Block render of existing asset if not from this source
169
191
  source={source}
170
192
  sources={sources}
171
193
  setSource={handleSourceSelect}
@@ -183,4 +205,4 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
183
205
  })}
184
206
  </div>
185
207
  );
186
- };
208
+ });
package/src/types.ts CHANGED
@@ -19,6 +19,9 @@ export interface ResourceBrowserSource {
19
19
  id: string;
20
20
  // Source type e.g. Matrix, DAM etc determines what plugin will be used for resource selection
21
21
  type: ResourceBrowserPluginType;
22
+
23
+ //alias' for the source to identify plugin to use for the source
24
+ aliases?: string[];
22
25
  }
23
26
 
24
27
  export interface ResourceBrowserSourceWithPlugin extends ResourceBrowserSource {