@squiz/resource-browser 3.2.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/Plugin/Plugin.js +4 -5
- package/lib/index.d.ts +2 -1
- package/lib/index.js +10 -2
- package/package.json +1 -1
- package/src/Plugin/Plugin.spec.tsx +47 -52
- package/src/Plugin/Plugin.stories.tsx +94 -0
- package/src/Plugin/Plugin.tsx +9 -14
- package/src/index.spec.tsx +32 -1
- package/src/index.stories.tsx +7 -0
- package/src/index.tsx +12 -3
package/CHANGELOG.md
CHANGED
package/lib/Plugin/Plugin.js
CHANGED
@@ -9,11 +9,10 @@ const ResourceBrowserInput_1 = require("../ResourceBrowserInput/ResourceBrowserI
|
|
9
9
|
const ResourceBrowserInlineButton_1 = require("../ResourceBrowserInlineButton/ResourceBrowserInlineButton");
|
10
10
|
const AuthProvider_1 = require("../ResourceBrowserContext/AuthProvider");
|
11
11
|
const PluginRender = ({ render, inline, inlineType, ...props }) => {
|
12
|
-
if (render)
|
13
|
-
return (react_1.default.createElement(AuthProvider_1.AuthProvider, { authConfig: props.source }, inline && inlineType ? (react_1.default.createElement(ResourceBrowserInlineButton_1.ResourceBrowserInlineButton, { inlineType: inlineType, ...props })) : (react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props }))));
|
14
|
-
}
|
15
|
-
else {
|
12
|
+
if (!render)
|
16
13
|
return react_1.default.createElement(react_1.default.Fragment, null);
|
17
|
-
|
14
|
+
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 });
|
16
|
+
return requiresAuth ? react_1.default.createElement(AuthProvider_1.AuthProvider, { authConfig: props.source }, content) : content;
|
18
17
|
};
|
19
18
|
exports.PluginRender = PluginRender;
|
package/lib/index.d.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
|
3
|
-
import { InlineType, ResourceBrowserUnresolvedResource, ResourceBrowserResource } from './types';
|
3
|
+
import { InlineType, ResourceBrowserUnresolvedResource, ResourceBrowserResource, ResourceBrowserPluginType } from './types';
|
4
4
|
import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
|
5
5
|
import BrowseToSource from './BrowseToSource/BrowseToSource';
|
6
6
|
import SourceDropdown from './SourceDropdown/SourceDropdown';
|
@@ -10,6 +10,7 @@ export * from './types';
|
|
10
10
|
export type ResourceBrowserProps = {
|
11
11
|
modalTitle: string;
|
12
12
|
allowedTypes?: string[];
|
13
|
+
allowedPlugins?: ResourceBrowserPluginType[];
|
13
14
|
isDisabled?: boolean;
|
14
15
|
value: ResourceBrowserUnresolvedResource | null;
|
15
16
|
inline?: boolean;
|
package/lib/index.js
CHANGED
@@ -48,11 +48,19 @@ const SourceDropdownContainer_1 = __importDefault(require("./SourceDropdownConta
|
|
48
48
|
exports.SourceDropdownContainer = SourceDropdownContainer_1.default;
|
49
49
|
__exportStar(require("./types"), exports);
|
50
50
|
const ResourceBrowser = (props) => {
|
51
|
-
const { value, inline, inlineType } = props;
|
51
|
+
const { value, inline, inlineType, allowedPlugins } = props;
|
52
52
|
const [error, setError] = (0, react_1.useState)(null);
|
53
|
-
const { onRequestSources, searchEnabled, plugins } = (0, react_1.useContext)(ResourceBrowserContext_1.ResourceBrowserContext);
|
53
|
+
const { onRequestSources, searchEnabled, plugins: allPlugins } = (0, react_1.useContext)(ResourceBrowserContext_1.ResourceBrowserContext);
|
54
54
|
const [isModalOpen, setIsModalOpen] = (0, react_1.useState)(false);
|
55
55
|
const [source, setSource] = (0, react_1.useState)(null);
|
56
|
+
const plugins = (0, react_1.useMemo)(() => {
|
57
|
+
// Allow some usages e.g. plugin specific like MatrixAssetWidget restrict what plugins show
|
58
|
+
if (!allowedPlugins)
|
59
|
+
return allPlugins; // No restrictions, return all
|
60
|
+
return allPlugins.filter((plugin) => {
|
61
|
+
return allowedPlugins.includes(plugin.type); // Restrict based on plugin type
|
62
|
+
});
|
63
|
+
}, [allowedPlugins, allPlugins]);
|
56
64
|
const [mode, setMode] = (0, react_1.useState)(null);
|
57
65
|
const { data: sources, isLoading, error: sourcesError, reload: reloadSources } = (0, useSources_1.useSources)({ onRequestSources, plugins });
|
58
66
|
const plugin = source?.plugin || null;
|
package/package.json
CHANGED
@@ -2,10 +2,35 @@ import React from 'react';
|
|
2
2
|
import { render, waitFor } from '@testing-library/react';
|
3
3
|
|
4
4
|
import { PluginRender } from './Plugin';
|
5
|
+
import { ResourceBrowserPluginType } from '../types';
|
5
6
|
import * as RBI from '../ResourceBrowserInput/ResourceBrowserInput';
|
6
7
|
import * as RBInlineButton from '../ResourceBrowserInlineButton/ResourceBrowserInlineButton';
|
8
|
+
import * as AuthContext from '../ResourceBrowserContext/AuthProvider';
|
9
|
+
|
7
10
|
jest.spyOn(RBI, 'ResourceBrowserInput');
|
8
11
|
jest.spyOn(RBInlineButton, 'ResourceBrowserInlineButton');
|
12
|
+
jest.spyOn(AuthContext, 'AuthProvider');
|
13
|
+
|
14
|
+
const baseProps = {
|
15
|
+
type: null,
|
16
|
+
modalTitle: 'Asset picker',
|
17
|
+
value: null,
|
18
|
+
isOtherSourceValue: false,
|
19
|
+
onChange: jest.fn(),
|
20
|
+
onClear: jest.fn(),
|
21
|
+
useResource: () => ({ data: null, error: null, isLoading: false }),
|
22
|
+
plugin: null,
|
23
|
+
pluginMode: null,
|
24
|
+
searchEnabled: false,
|
25
|
+
source: null,
|
26
|
+
sources: [],
|
27
|
+
isLoading: false,
|
28
|
+
error: null,
|
29
|
+
setSource: jest.fn(),
|
30
|
+
isModalOpen: false,
|
31
|
+
onModalStateChange: jest.fn(),
|
32
|
+
onRetry: jest.fn(),
|
33
|
+
};
|
9
34
|
|
10
35
|
describe('Plugin', () => {
|
11
36
|
it('Does not render ResourceBrowserInput if render is false', async () => {
|
@@ -18,32 +43,7 @@ describe('Plugin', () => {
|
|
18
43
|
});
|
19
44
|
|
20
45
|
it('Does render ResourceBrowserInput if render is true', async () => {
|
21
|
-
const props =
|
22
|
-
type: null,
|
23
|
-
modalTitle: 'Asset picker',
|
24
|
-
value: null,
|
25
|
-
isOtherSourceValue: false,
|
26
|
-
onChange: jest.fn(),
|
27
|
-
onClear: jest.fn(),
|
28
|
-
useResource: () => {
|
29
|
-
return {
|
30
|
-
data: null,
|
31
|
-
error: null,
|
32
|
-
isLoading: false,
|
33
|
-
};
|
34
|
-
},
|
35
|
-
plugin: null,
|
36
|
-
pluginMode: null,
|
37
|
-
searchEnabled: false,
|
38
|
-
source: null,
|
39
|
-
sources: [],
|
40
|
-
isLoading: false,
|
41
|
-
error: null,
|
42
|
-
setSource: () => {},
|
43
|
-
isModalOpen: false,
|
44
|
-
onModalStateChange: () => {},
|
45
|
-
onRetry: () => {},
|
46
|
-
};
|
46
|
+
const props = baseProps;
|
47
47
|
render(<PluginRender render={true} inline={false} {...props} />);
|
48
48
|
|
49
49
|
await waitFor(() => {
|
@@ -52,36 +52,31 @@ describe('Plugin', () => {
|
|
52
52
|
});
|
53
53
|
|
54
54
|
it('Does render ResourceBrowserInlineButton if inline is true', async () => {
|
55
|
-
const props =
|
56
|
-
type: null,
|
57
|
-
modalTitle: 'Asset picker',
|
58
|
-
value: null,
|
59
|
-
isOtherSourceValue: false,
|
60
|
-
onChange: jest.fn(),
|
61
|
-
onClear: jest.fn(),
|
62
|
-
useResource: () => {
|
63
|
-
return {
|
64
|
-
data: null,
|
65
|
-
error: null,
|
66
|
-
isLoading: false,
|
67
|
-
};
|
68
|
-
},
|
69
|
-
plugin: null,
|
70
|
-
pluginMode: null,
|
71
|
-
searchEnabled: false,
|
72
|
-
source: null,
|
73
|
-
sources: [],
|
74
|
-
isLoading: false,
|
75
|
-
error: null,
|
76
|
-
setSource: () => {},
|
77
|
-
isModalOpen: false,
|
78
|
-
onModalStateChange: () => {},
|
79
|
-
onRetry: () => {},
|
80
|
-
};
|
55
|
+
const props = baseProps;
|
81
56
|
render(<PluginRender render={true} inline={true} inlineType="image" {...props} />);
|
82
57
|
|
83
58
|
await waitFor(() => {
|
84
59
|
expect(RBInlineButton.ResourceBrowserInlineButton).toHaveBeenCalledWith({ ...props, inlineType: 'image' }, {});
|
85
60
|
});
|
86
61
|
});
|
62
|
+
|
63
|
+
it('Wraps content in AuthProvider when source requires auth', async () => {
|
64
|
+
const sourceWithAuth = {
|
65
|
+
id: '123',
|
66
|
+
type: 'dam' as ResourceBrowserPluginType,
|
67
|
+
configuration: {
|
68
|
+
authUrl: 'https://example.com/auth',
|
69
|
+
redirectUrl: 'https://example.com/redirect',
|
70
|
+
clientId: 'abc123',
|
71
|
+
scope: 'scope1',
|
72
|
+
},
|
73
|
+
};
|
74
|
+
|
75
|
+
const props = { ...baseProps, source: sourceWithAuth };
|
76
|
+
render(<PluginRender render={true} inline={false} {...props} />);
|
77
|
+
|
78
|
+
await waitFor(() => {
|
79
|
+
expect(AuthContext.AuthProvider).toHaveBeenCalledWith({ authConfig: sourceWithAuth, children: expect.anything() }, {});
|
80
|
+
});
|
81
|
+
});
|
87
82
|
});
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
import { PluginRender, PluginRenderType } from './Plugin';
|
4
|
+
|
5
|
+
export default {
|
6
|
+
title: 'Plugin renderer',
|
7
|
+
component: PluginRender,
|
8
|
+
} as Meta<typeof PluginRender>;
|
9
|
+
|
10
|
+
const noop = () => {};
|
11
|
+
|
12
|
+
const Template: StoryFn<PluginRenderType> = (args) => <PluginRender {...args} />;
|
13
|
+
|
14
|
+
export const WithoutAuth = Template.bind({});
|
15
|
+
WithoutAuth.args = {
|
16
|
+
render: true,
|
17
|
+
inline: false,
|
18
|
+
type: null,
|
19
|
+
modalTitle: 'Test Plugin (No Auth)',
|
20
|
+
onRetry: noop,
|
21
|
+
useResource: () => ({ data: null, error: null, isLoading: false }),
|
22
|
+
source: {
|
23
|
+
id: 'source-no-auth',
|
24
|
+
type: 'dummy',
|
25
|
+
configuration: {},
|
26
|
+
},
|
27
|
+
};
|
28
|
+
|
29
|
+
export const WithAuth = Template.bind({});
|
30
|
+
WithAuth.args = {
|
31
|
+
render: true,
|
32
|
+
inline: false,
|
33
|
+
type: null,
|
34
|
+
modalTitle: 'Test Plugin (With Auth)',
|
35
|
+
onRetry: noop,
|
36
|
+
useResource: () => ({ data: null, error: null, isLoading: false }),
|
37
|
+
source: {
|
38
|
+
id: 'source-with-auth',
|
39
|
+
type: 'dummy',
|
40
|
+
configuration: {
|
41
|
+
authUrl: 'https://example.com/auth',
|
42
|
+
redirectUrl: 'https://example.com/redirect',
|
43
|
+
clientId: 'client123',
|
44
|
+
scope: 'scope1 scope2',
|
45
|
+
},
|
46
|
+
},
|
47
|
+
};
|
48
|
+
|
49
|
+
export const InlineWithoutAuth = Template.bind({});
|
50
|
+
InlineWithoutAuth.args = {
|
51
|
+
render: true,
|
52
|
+
inline: true,
|
53
|
+
inlineType: 'image',
|
54
|
+
type: null,
|
55
|
+
modalTitle: 'Test Inline Plugin (No Auth)',
|
56
|
+
onRetry: noop,
|
57
|
+
useResource: () => ({ data: null, error: null, isLoading: false }),
|
58
|
+
source: {
|
59
|
+
id: 'inline-source-no-auth',
|
60
|
+
type: 'dummy',
|
61
|
+
configuration: {},
|
62
|
+
},
|
63
|
+
};
|
64
|
+
|
65
|
+
export const InlineWithAuth = Template.bind({});
|
66
|
+
InlineWithAuth.args = {
|
67
|
+
render: true,
|
68
|
+
inline: true,
|
69
|
+
inlineType: 'image',
|
70
|
+
type: null,
|
71
|
+
modalTitle: 'Test Inline Plugin (With Auth)',
|
72
|
+
onRetry: noop,
|
73
|
+
useResource: () => ({ data: null, error: null, isLoading: false }),
|
74
|
+
source: {
|
75
|
+
id: 'inline-source-with-auth',
|
76
|
+
type: 'dummy',
|
77
|
+
configuration: {
|
78
|
+
authUrl: 'https://example.com/auth',
|
79
|
+
redirectUrl: 'https://example.com/redirect',
|
80
|
+
clientId: 'client123',
|
81
|
+
scope: 'scope1 scope2',
|
82
|
+
},
|
83
|
+
},
|
84
|
+
};
|
85
|
+
|
86
|
+
export const NotRendered = Template.bind({});
|
87
|
+
NotRendered.args = {
|
88
|
+
render: false,
|
89
|
+
inline: false,
|
90
|
+
type: null,
|
91
|
+
modalTitle: 'Should Not Render',
|
92
|
+
onRetry: noop,
|
93
|
+
source: null,
|
94
|
+
};
|
package/src/Plugin/Plugin.tsx
CHANGED
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
|
3
3
|
import { ResourceBrowserInlineButton } from '../ResourceBrowserInlineButton/ResourceBrowserInlineButton';
|
4
4
|
import { AuthProvider } from '../ResourceBrowserContext/AuthProvider';
|
5
|
-
import { ResourceBrowserPluginType, InlineType } from '../types';
|
5
|
+
import { ResourceBrowserPluginType, InlineType, ResourceBrowserSourceWithConfig } from '../types';
|
6
6
|
|
7
7
|
/**
|
8
8
|
* This plugin component exsits to deal with React rules of Hooks stupidity.
|
@@ -19,17 +19,12 @@ export type PluginRenderType = ResourceBrowserInputProps & {
|
|
19
19
|
onRetry: () => void;
|
20
20
|
};
|
21
21
|
export const PluginRender = ({ render, inline, inlineType, ...props }: PluginRenderType) => {
|
22
|
-
if (render)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
</AuthProvider>
|
31
|
-
);
|
32
|
-
} else {
|
33
|
-
return <></>;
|
34
|
-
}
|
22
|
+
if (!render) return <></>;
|
23
|
+
|
24
|
+
const requiresAuth = Boolean((props.source as ResourceBrowserSourceWithConfig)?.configuration?.authUrl);
|
25
|
+
|
26
|
+
const content =
|
27
|
+
inline && inlineType ? <ResourceBrowserInlineButton inlineType={inlineType} {...props} /> : <ResourceBrowserInput {...props} />;
|
28
|
+
|
29
|
+
return requiresAuth ? <AuthProvider authConfig={props.source}>{content}</AuthProvider> : content;
|
35
30
|
};
|
package/src/index.spec.tsx
CHANGED
@@ -10,6 +10,9 @@ jest.spyOn(RBI, 'ResourceBrowserInput');
|
|
10
10
|
|
11
11
|
import * as Plugin from './Plugin/Plugin';
|
12
12
|
|
13
|
+
import * as useSources from './Hooks/useSources';
|
14
|
+
jest.spyOn(useSources, 'useSources');
|
15
|
+
|
13
16
|
var useSourceReloadMock = jest.fn();
|
14
17
|
jest.mock('./Hooks/useSources', () => {
|
15
18
|
const actual = jest.requireActual('./Hooks/useSources');
|
@@ -57,12 +60,14 @@ describe('Resource browser input', () => {
|
|
57
60
|
renderResourceLauncher: jest.fn(),
|
58
61
|
} as ResourceBrowserPlugin;
|
59
62
|
|
63
|
+
const plugins = [mockMatrixPlugin, mockDamPlugin];
|
64
|
+
|
60
65
|
const renderComponent = (props: Partial<ResourceBrowserProps> = {}, searchEnabled?: boolean) => {
|
61
66
|
return renderWithContext(
|
62
67
|
<ResourceBrowser modalTitle="Asset picker" value={null} onChange={mockChange} onClear={mockOnClear} {...props} />,
|
63
68
|
{
|
64
69
|
onRequestSources: mockRequestSources,
|
65
|
-
plugins
|
70
|
+
plugins,
|
66
71
|
searchEnabled: !!searchEnabled,
|
67
72
|
},
|
68
73
|
);
|
@@ -76,6 +81,32 @@ describe('Resource browser input', () => {
|
|
76
81
|
};
|
77
82
|
};
|
78
83
|
|
84
|
+
it('allowedPlugins will restrict which plugins are used', async () => {
|
85
|
+
// Works as expected with no restriction
|
86
|
+
(useSources.useSources as jest.Mock).mockClear();
|
87
|
+
renderComponent();
|
88
|
+
let expectedPlugins = plugins;
|
89
|
+
await waitFor(() => {
|
90
|
+
expect(useSources.useSources).toHaveBeenLastCalledWith(
|
91
|
+
expect.objectContaining({
|
92
|
+
plugins: expectedPlugins,
|
93
|
+
}),
|
94
|
+
);
|
95
|
+
});
|
96
|
+
|
97
|
+
// Works as expected with restrictions
|
98
|
+
(useSources.useSources as jest.Mock).mockClear();
|
99
|
+
renderComponent({ allowedPlugins: ['matrix'] });
|
100
|
+
expectedPlugins = plugins.filter((plugin) => plugin.type === 'matrix');
|
101
|
+
await waitFor(() => {
|
102
|
+
expect(useSources.useSources).toHaveBeenLastCalledWith(
|
103
|
+
expect.objectContaining({
|
104
|
+
plugins: expectedPlugins,
|
105
|
+
}),
|
106
|
+
);
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
79
110
|
it('If only one valid source is provided will default to its Source and Plugin', async () => {
|
80
111
|
const source = mockSource({ type: 'dam' });
|
81
112
|
mockRequestSources.mockResolvedValueOnce([source]);
|
package/src/index.stories.tsx
CHANGED
@@ -86,6 +86,13 @@ SingleSource.args = {
|
|
86
86
|
singleSource: true,
|
87
87
|
};
|
88
88
|
|
89
|
+
export const RestrictedSource = Template.bind({});
|
90
|
+
RestrictedSource.args = {
|
91
|
+
...Primary.args,
|
92
|
+
allowedPlugins: ['matrix'],
|
93
|
+
searchEnabled: true,
|
94
|
+
};
|
95
|
+
|
89
96
|
export const SearchEnabled = Template.bind({});
|
90
97
|
SearchEnabled.args = {
|
91
98
|
...Primary.args,
|
package/src/index.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useState, useContext, useEffect, useCallback } from 'react';
|
1
|
+
import React, { useState, useContext, useEffect, useCallback, useMemo } from 'react';
|
2
2
|
|
3
3
|
import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
|
4
4
|
import {
|
@@ -7,6 +7,7 @@ import {
|
|
7
7
|
ResourceBrowserUnresolvedResource,
|
8
8
|
ResourceBrowserResource,
|
9
9
|
ResourceBrowserSourceWithPlugin,
|
10
|
+
ResourceBrowserPluginType,
|
10
11
|
} from './types';
|
11
12
|
import { useSources } from './Hooks/useSources';
|
12
13
|
import { PluginRender } from './Plugin/Plugin';
|
@@ -31,6 +32,7 @@ export * from './types';
|
|
31
32
|
export type ResourceBrowserProps = {
|
32
33
|
modalTitle: string;
|
33
34
|
allowedTypes?: string[];
|
35
|
+
allowedPlugins?: ResourceBrowserPluginType[];
|
34
36
|
isDisabled?: boolean;
|
35
37
|
value: ResourceBrowserUnresolvedResource | null;
|
36
38
|
inline?: boolean; // Will render open button only, no input / showing of existing selection
|
@@ -40,12 +42,19 @@ export type ResourceBrowserProps = {
|
|
40
42
|
};
|
41
43
|
|
42
44
|
export const ResourceBrowser = (props: ResourceBrowserProps) => {
|
43
|
-
const { value, inline, inlineType } = props;
|
45
|
+
const { value, inline, inlineType, allowedPlugins } = props;
|
44
46
|
const [error, setError] = useState<Error | null>(null);
|
45
|
-
const { onRequestSources, searchEnabled, plugins } = useContext(ResourceBrowserContext);
|
47
|
+
const { onRequestSources, searchEnabled, plugins: allPlugins } = useContext(ResourceBrowserContext);
|
46
48
|
|
47
49
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
48
50
|
const [source, setSource] = useState<ResourceBrowserSourceWithPlugin | null>(null);
|
51
|
+
const plugins = useMemo(() => {
|
52
|
+
// Allow some usages e.g. plugin specific like MatrixAssetWidget restrict what plugins show
|
53
|
+
if (!allowedPlugins) return allPlugins; // No restrictions, return all
|
54
|
+
return allPlugins.filter((plugin) => {
|
55
|
+
return allowedPlugins.includes(plugin.type); // Restrict based on plugin type
|
56
|
+
});
|
57
|
+
}, [allowedPlugins, allPlugins]);
|
49
58
|
const [mode, setMode] = useState<PluginLaunchMode | null>(null);
|
50
59
|
const { data: sources, isLoading, error: sourcesError, reload: reloadSources } = useSources({ onRequestSources, plugins });
|
51
60
|
const plugin = source?.plugin || null;
|