@squiz/resource-browser 2.4.12 → 3.0.1-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 +12 -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 +12 -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 +35 -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
@@ -0,0 +1,16 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { ResourceBrowserSource } from '../types';
3
+ interface AuthContextProps {
4
+ authToken: string | null;
5
+ isAuthenticated: boolean;
6
+ login: () => void;
7
+ refreshAccessToken: () => Promise<any>;
8
+ }
9
+ export declare const AuthContext: React.Context<AuthContextProps | undefined>;
10
+ export declare const useAuthContext: () => AuthContextProps;
11
+ interface AuthProviderProps {
12
+ children: ReactNode;
13
+ authConfig?: ResourceBrowserSource | null;
14
+ }
15
+ export declare const AuthProvider: ({ children, authConfig }: AuthProviderProps) => React.JSX.Element;
16
+ export {};
@@ -0,0 +1,18 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { useAuth } from '../Hooks/useAuth';
3
+ export const AuthContext = createContext(undefined);
4
+ export const useAuthContext = () => {
5
+ const context = useContext(AuthContext);
6
+ if (!context) {
7
+ throw new Error('useAuthContext must be used within an AuthProvider');
8
+ }
9
+ return context;
10
+ };
11
+ export const AuthProvider = ({ children, authConfig }) => {
12
+ const authConfiguration = authConfig;
13
+ const auth = useAuth(authConfiguration?.configuration);
14
+ if (!authConfiguration?.configuration) {
15
+ return React.createElement(React.Fragment, null, children);
16
+ }
17
+ return React.createElement(AuthContext.Provider, { value: auth }, children);
18
+ };
@@ -0,0 +1,15 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ import { OnRequestSources, ResourceBrowserPlugin } from '../types';
3
+ export type ResourceBrowserContextProps = {
4
+ onRequestSources: OnRequestSources;
5
+ searchEnabled: boolean;
6
+ plugins: Array<ResourceBrowserPlugin>;
7
+ };
8
+ /**
9
+ * @internal Direct usage of this object is discouraged. It will be privated in a future major version.
10
+ * Please use ResourceBrowserContextProvider instead.
11
+ */
12
+ export declare const ResourceBrowserContext: React.Context<ResourceBrowserContextProps>;
13
+ export declare const ResourceBrowserContextProvider: (props: PropsWithChildren<{
14
+ value: ResourceBrowserContextProps;
15
+ }>) => React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ import React, { useMemo } from 'react';
2
+ import pMemoize from 'p-memoize';
3
+ import ExpiryMap from 'expiry-map';
4
+ /**
5
+ * @internal Direct usage of this object is discouraged. It will be privated in a future major version.
6
+ * Please use ResourceBrowserContextProvider instead.
7
+ */
8
+ export const ResourceBrowserContext = React.createContext({
9
+ onRequestSources: () => {
10
+ throw new Error('onRequestSources has not been configured.');
11
+ },
12
+ searchEnabled: false,
13
+ plugins: [],
14
+ });
15
+ export const ResourceBrowserContextProvider = (props) => {
16
+ const CACHE_DURATION = 30000; // 30 seconds
17
+ const { value: { onRequestSources, ...other }, children, } = props;
18
+ const cache = new ExpiryMap(CACHE_DURATION);
19
+ const memoized = useMemo(() => ({
20
+ onRequestSources: pMemoize(onRequestSources, {
21
+ cache,
22
+ cacheKey: () => 'onRequestSources',
23
+ }),
24
+ }), [onRequestSources]);
25
+ return React.createElement(ResourceBrowserContext.Provider, { value: { ...memoized, ...other } }, children);
26
+ };
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserPlugin, ResourceBrowserSource, ResourceBrowserUnresolvedResource, ResourceBrowserResource, ResourceBrowserSourceWithPlugin, PluginLaunchMode } from '../types';
3
+ export type ResourceBrowserInputProps = {
4
+ modalTitle: string;
5
+ allowedTypes?: string[];
6
+ isDisabled?: boolean;
7
+ value: ResourceBrowserUnresolvedResource | null;
8
+ useResource(referenceId: string | null, source: ResourceBrowserSource | null): {
9
+ data: ResourceBrowserResource | null;
10
+ error: Error | null;
11
+ isLoading: boolean;
12
+ };
13
+ onChange(resource: ResourceBrowserResource | null): void;
14
+ onClear?(): void;
15
+ plugin: ResourceBrowserPlugin | null;
16
+ pluginMode: PluginLaunchMode | null;
17
+ searchEnabled: boolean;
18
+ source: ResourceBrowserSource | null;
19
+ sources: ResourceBrowserSourceWithPlugin[];
20
+ isLoading: boolean;
21
+ error: Error | null;
22
+ setSource(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
23
+ isModalOpen: boolean;
24
+ onModalStateChange(isOpen: boolean): void;
25
+ };
26
+ export declare const ResourceBrowserInput: ({ modalTitle, allowedTypes, onChange, value, useResource, isDisabled, onClear, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }: ResourceBrowserInputProps) => React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import MainContainer from '../MainContainer/MainContainer';
3
+ import { ResourcePicker } from '../ResourcePicker/ResourcePicker';
4
+ export const ResourceBrowserInput = ({ modalTitle, allowedTypes, onChange, value, useResource, isDisabled, onClear, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }) => {
5
+ const { data: resource, error: resourceError, isLoading: isResourceLoading } = useResource(value?.resourceId || null, source);
6
+ const defaultOnClear = () => onChange(null);
7
+ const onClearFunction = onClear ?? defaultOnClear;
8
+ return (React.createElement(ResourcePicker, { resource: resource, plugin: plugin, allowedTypes: allowedTypes, error: resourceError || error, isLoading: isResourceLoading || isLoading, isDisabled: isDisabled, onClear: onClearFunction, isModalOpen: isModalOpen, onModalStateChange: onModalStateChange }, (onClose, titleProps) => (React.createElement(MainContainer, { selectedSource: source, sources: sources, preselectedResource: resource, plugin: plugin, pluginMode: pluginMode, searchEnabled: searchEnabled, title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onSourceSelect: setSource, onClose: onClose, onChange: onChange }))));
9
+ };
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserSource, ResourceBrowserSourceWithPlugin, PluginLaunchMode } from '../types';
3
+ interface ResourceLauncherProps {
4
+ sources: ResourceBrowserSourceWithPlugin[];
5
+ onSourceSelect(source: ResourceBrowserSource, mode: PluginLaunchMode): void;
6
+ }
7
+ declare function ResourceLauncher({ sources, onSourceSelect }: ResourceLauncherProps): React.JSX.Element;
8
+ export default ResourceLauncher;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { PluginLaunchModeType } from '../types';
3
+ function ResourceLauncher({ sources, onSourceSelect }) {
4
+ return (React.createElement("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" },
5
+ React.createElement("ul", { tabIndex: -1, "aria-label": `sources list`, className: "flex flex-col bg-gray-100 min-h-full focus-visible:outline-0" }, sources.map((source, index) => {
6
+ const SourceLauncher = source.plugin.renderResourceLauncher();
7
+ return (React.createElement("li", { key: index, className: "flex items-stretch relative" },
8
+ React.createElement("div", { className: `squiz-rb-plugin squiz-rb-plugin--${source.plugin?.type} w-full` },
9
+ React.createElement(SourceLauncher, { source: source, onSearch: (query) => onSourceSelect(source, { type: PluginLaunchModeType.Search, args: { query } }), onBrowse: (browseTo) => onSourceSelect(source, { type: PluginLaunchModeType.Browse, args: { browseTo } }) }))));
10
+ }))));
11
+ }
12
+ export default ResourceLauncher;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { DOMAttributes } from '@react-types/shared';
3
+ import { ResourceBrowserResource, ResourceBrowserPlugin } from '../types';
4
+ export type ResourcePickerProps = {
5
+ resource: ResourceBrowserResource | null;
6
+ plugin: ResourceBrowserPlugin | null;
7
+ allowedTypes: string[] | undefined;
8
+ error: Error | null;
9
+ isLoading: boolean;
10
+ isDisabled?: boolean;
11
+ children: (onClose: () => void, titleProps: DOMAttributes) => React.ReactElement;
12
+ onClear: () => void;
13
+ isModalOpen: boolean;
14
+ onModalStateChange(isOpen: boolean): void;
15
+ };
16
+ export declare const ResourcePicker: ({ resource, plugin, allowedTypes, error: externalError, isLoading: isExternalLoading, isDisabled, children, onClear, isModalOpen, onModalStateChange, }: ResourcePickerProps) => React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import AdsClickRoundedIcon from '@mui/icons-material/AdsClickRounded';
3
+ import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
4
+ import PhotoLibraryRoundedIcon from '@mui/icons-material/PhotoLibraryRounded';
5
+ import { ModalTrigger } from '@squiz/resource-browser-ui-lib';
6
+ import { ErrorState } from './States/Error';
7
+ import { LoadingState } from './States/Loading';
8
+ import { SelectedState } from './States/Selected';
9
+ import { useSelectedState } from '../Hooks/useSelectedState';
10
+ import clsx from 'clsx';
11
+ export const ResourcePicker = ({ resource, plugin, allowedTypes, error: externalError, isLoading: isExternalLoading, isDisabled, children, onClear, isModalOpen, onModalStateChange, }) => {
12
+ const { data: selectedState, error, isLoading } = useSelectedState({ resource, plugin });
13
+ const isImagePicker = allowedTypes && allowedTypes.length === 1 && allowedTypes.includes('image');
14
+ const isEmpty = resource === null && !isExternalLoading && !externalError;
15
+ return (React.createElement("div", { className: clsx('resource-picker', isDisabled && 'resource-picker--disabled') },
16
+ isImagePicker ? (React.createElement(PhotoLibraryRoundedIcon, { "aria-hidden": true, className: "w-6 h-6" })) : (React.createElement(AdsClickRoundedIcon, { "aria-hidden": true, className: "w-6 h-6" })),
17
+ isEmpty ? (React.createElement(ModalTrigger, { overlayTriggerState: {
18
+ isOpen: isModalOpen,
19
+ onOpenChange: onModalStateChange,
20
+ }, showLabel: true, label: isImagePicker ? `Choose image` : `Choose asset`, icon: React.createElement(AddCircleOutlineRoundedIcon, { "aria-hidden": true, className: `!w-4 !h-4 text-blue-300 ${isDisabled ? 'text-gray-600' : ''}` }), isDisabled: isDisabled, scope: "squiz-rb-scope" }, children)) : (React.createElement("div", { className: "resource-picker-info" },
21
+ React.createElement("div", { className: "resource-picker-info__layout" },
22
+ (isExternalLoading || isLoading) && React.createElement(LoadingState, null),
23
+ (externalError || error) && (React.createElement(ErrorState, { error: externalError || error, isDisabled: isDisabled, onClear: onClear })),
24
+ !isExternalLoading && resource && selectedState && (React.createElement(SelectedState, { isModalOpen: isModalOpen, onModalStateChange: onModalStateChange, resource: resource, showThumbnail: selectedState?.showThumbnail || false, icon: selectedState?.icon, label: selectedState?.label, description: selectedState?.description, isDisabled: isDisabled, onClear: onClear, resourcePickerContainer: children })))))));
25
+ };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export type ErrorStateProps = {
3
+ error: Error;
4
+ isDisabled?: boolean;
5
+ onClear: () => void;
6
+ };
7
+ export declare const ErrorState: ({ error, isDisabled, onClear }: ErrorStateProps) => React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Icon, ResetButton } from '@squiz/generic-browser-lib';
3
+ export const ErrorState = ({ error, isDisabled, onClear }) => (React.createElement(React.Fragment, null,
4
+ React.createElement(Icon, { icon: 'error', "aria-hidden": true, className: "w-6 h-6 text-red-300" }),
5
+ React.createElement("div", { className: "text-red-300 w-full" }, error.message),
6
+ React.createElement(ResetButton, { isDisabled: isDisabled, onClick: onClear })));
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const LoadingState: () => React.JSX.Element;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { Spinner } from '@squiz/generic-browser-lib';
3
+ export const LoadingState = () => (React.createElement("div", { className: "col-start-2 col-end-2" },
4
+ React.createElement(Spinner, { label: "Loading selection", className: "m-2" })));
@@ -0,0 +1,15 @@
1
+ import React, { ReactElement } from 'react';
2
+ import { ResourceBrowserResource } from '../../types';
3
+ export type SelectedStateProps = {
4
+ resource: ResourceBrowserResource;
5
+ showThumbnail: boolean;
6
+ icon: ReactElement | undefined;
7
+ label: string;
8
+ description: Array<ReactElement>;
9
+ isDisabled?: boolean;
10
+ onClear: () => void;
11
+ resourcePickerContainer: any;
12
+ isModalOpen?: boolean;
13
+ onModalStateChange?(isOpen: boolean): void;
14
+ };
15
+ export declare const SelectedState: ({ resource, showThumbnail, icon, label, description, isDisabled, onClear, resourcePickerContainer, isModalOpen, onModalStateChange, }: SelectedStateProps) => React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { ResetButton } from '@squiz/generic-browser-lib';
3
+ import { ModalTrigger } from '@squiz/resource-browser-ui-lib';
4
+ import { CircledLoopIcon } from '../../Icons/CircledLoopIcon';
5
+ export const SelectedState = ({ resource, showThumbnail, icon, label, description, isDisabled, onClear, resourcePickerContainer, isModalOpen, onModalStateChange, }) => {
6
+ const modalController = {
7
+ isOpen: isModalOpen,
8
+ onOpenChange: onModalStateChange,
9
+ };
10
+ const replaceAsset = (React.createElement(ModalTrigger, { overlayTriggerState: isModalOpen && onModalStateChange ? modalController : undefined, showLabel: false, label: "Replace selection", containerClasses: "text-gray-500 hover:text-gray-800 focus:text-gray-800 disabled:text-gray-500 disabled:cursor-not-allowed", icon: React.createElement(CircledLoopIcon, { "aria-hidden": true, className: "m-1" }), isDisabled: isDisabled, scope: "squiz-rb-scope" }, resourcePickerContainer));
11
+ return (React.createElement(React.Fragment, null,
12
+ showThumbnail && resource.squizImage ? (React.createElement("div", { className: "checkered-bg w-[56px] h-[56px] overflow-hidden flex justify-center items-center rounded" },
13
+ React.createElement("img", { src: resource.squizImage.imageVariations.original.url || resource.url, className: "w-full h-full object-cover object-center", alt: resource.squizImage.name || resource.name }))) : (React.createElement("div", { className: "w-4 h-4 mt-1 flex self-start overflow-hidden" }, icon)),
14
+ React.createElement("div", { className: "justify-self-start self-center w-full overflow-hidden break-words" },
15
+ label,
16
+ description),
17
+ React.createElement("div", { className: "flex" },
18
+ replaceAsset,
19
+ React.createElement(ResetButton, { isDisabled: isDisabled, onClick: onClear }))));
20
+ };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserSource, PluginLaunchMode } from '../types';
3
+ export default function SourceDropdown({ sources, selectedSource, onSourceSelect, }: {
4
+ sources: ResourceBrowserSource[];
5
+ selectedSource: ResourceBrowserSource;
6
+ onSourceSelect(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
7
+ }): React.JSX.Element;
@@ -0,0 +1,46 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import { Icon, uuid } from '@squiz/generic-browser-lib';
3
+ import { useFocusWithin, useKeyboard } from 'react-aria';
4
+ import { ArrowDownIcon } from '../Icons/ArrowDownIcon';
5
+ export default function SourceDropdown({ sources, selectedSource, onSourceSelect, }) {
6
+ const [uniqueId] = useState(uuid());
7
+ const buttonRef = useRef(null);
8
+ const [isOpen, setIsOpen] = useState(false);
9
+ // Watch the focus and blur on the menu and close if focus leaves the control
10
+ const { focusWithinProps } = useFocusWithin({
11
+ onBlurWithin: () => {
12
+ setIsOpen(false);
13
+ },
14
+ });
15
+ // Listen for Esc key within this element
16
+ const { keyboardProps } = useKeyboard({
17
+ onKeyDown: (e) => {
18
+ if (isOpen && e.key === 'Escape') {
19
+ setIsOpen(false);
20
+ buttonRef.current?.focus(); // Restore focus to the element which opened the menu
21
+ }
22
+ },
23
+ });
24
+ const handleSourceClick = (source) => {
25
+ setIsOpen(false);
26
+ buttonRef.current?.focus();
27
+ onSourceSelect(source);
28
+ };
29
+ if (!sources.length) {
30
+ return React.createElement(React.Fragment, null);
31
+ }
32
+ return (React.createElement("div", { ...focusWithinProps, ...keyboardProps, className: "relative w-full " },
33
+ React.createElement("button", { ref: buttonRef, type: "button", "aria-label": "Source quick select", "aria-expanded": isOpen, "aria-controls": `${uniqueId}-button-menu`, onClick: () => setIsOpen(!isOpen), className: "relative flex items-center p-2 w-full rounded bg-blue-100 hover:bg-blue-150" },
34
+ React.createElement("span", { className: "sr-only" }, "current source "),
35
+ React.createElement("div", { className: "truncate max-w-[200px] text-md font-semibold text-blue-400" }, selectedSource.name),
36
+ React.createElement(ArrowDownIcon, { "aria-hidden": true, className: "absolute right-2 fill-blue-300" })),
37
+ React.createElement("ul", { id: `${uniqueId}-button-menu`, "aria-hidden": !isOpen, 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 ${!isOpen ? 'hidden' : ''}` }, sources.map((source) => {
38
+ const { id, name, type } = source;
39
+ const isSelectedSource = id === selectedSource.id;
40
+ return (React.createElement("li", { key: id, className: "flex items-center text-sm font-semibold mb-2 bg-white rounded" },
41
+ React.createElement("button", { type: "button", onClick: () => handleSourceClick(source), className: `relative grow flex items-center p-2 border-1 border-white rounded hover:bg-gray-50 hover:border-gray-300 focus:bg-gray-100` },
42
+ React.createElement(Icon, { icon: type, "aria-label": type, className: "shrink-0 mr-2.5" }),
43
+ React.createElement("span", { className: "text-left mr-7" }, name),
44
+ isSelectedSource && (React.createElement(Icon, { icon: 'selected', "aria-label": "selected", className: "absolute right-4" })))));
45
+ }))));
46
+ }
@@ -0,0 +1,5 @@
1
+ import React, { PropsWithChildren } from 'react';
2
+ export default function SourceDropdownContainer({ children, isCollapsed, onExpand, }: PropsWithChildren<{
3
+ isCollapsed: boolean;
4
+ onExpand: () => void;
5
+ }>): React.JSX.Element;
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { AdsClickIcon } from '../Icons/AdsClickIcon';
3
+ export default function SourceDropdownContainer({ children, isCollapsed, onExpand, }) {
4
+ return (React.createElement("div", { className: "inline-flex rounded-lg p-1 bg-blue-100 min-h-[44px]" },
5
+ isCollapsed && (React.createElement("button", { "aria-label": "Expand browse options", onClick: onExpand, className: "flex items-center" },
6
+ React.createElement(AdsClickIcon, { "aria-hidden": true, className: "mx-2 fill-blue-300" }))),
7
+ !isCollapsed && (React.createElement(React.Fragment, null,
8
+ React.createElement("div", { className: "flex items-center pr-1" },
9
+ React.createElement(AdsClickIcon, { "aria-hidden": true, className: "mx-2 fill-blue-300" }),
10
+ React.createElement("span", { className: "sr-only" }, "Browse")),
11
+ children))));
12
+ }
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserSource } from '../types';
3
+ interface SourceListProps {
4
+ sources: ResourceBrowserSource[];
5
+ onSourceSelect(source: ResourceBrowserSource): void;
6
+ }
7
+ declare function SourceList({ sources, onSourceSelect }: SourceListProps): React.JSX.Element;
8
+ export default SourceList;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { Icon } from '@squiz/generic-browser-lib';
3
+ function SourceList({ sources, onSourceSelect }) {
4
+ return (React.createElement("div", { className: "overflow-y-scroll w-screen max-w-[400px] flex-1 grow-[3] border-r border-gray-300 bg-gray-100 pl-4.5 pr-4.5 pb-4.5 pt-3" },
5
+ React.createElement("div", { className: "text-md font-semibold" }, "Select an environment to use"),
6
+ React.createElement("ul", { tabIndex: -1, "aria-label": `environment list`, className: "flex flex-col bg-gray-100 min-h-full focus-visible:outline-0" }, sources.map((source, index) => {
7
+ return (React.createElement("li", { key: index, className: "flex items-stretch relative" },
8
+ React.createElement("button", { onClick: () => {
9
+ onSourceSelect(source);
10
+ }, className: "w-full p-1 mt-3 bg-white border-1 border-grey-200 min-h-[64px] rounded-lg flex items-center text-md font-semibold" },
11
+ React.createElement(Icon, { icon: source.type, className: "ml-4" }),
12
+ React.createElement("span", { className: "line-clamp-2 text-left break-word ml-4" }, source.name || source.id),
13
+ React.createElement(Icon, { icon: 'arrow-right', className: "absolute ml-1 right-4" }))));
14
+ }))));
15
+ }
16
+ export default SourceList;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
3
+ import { ResourceBrowserUnresolvedResource, ResourceBrowserResource } from './types';
4
+ import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
5
+ import BrowseToSource from './BrowseToSource/BrowseToSource';
6
+ import SourceDropdown from './SourceDropdown/SourceDropdown';
7
+ import SourceDropdownContainer from './SourceDropdownContainer/SourceDropdownContainer';
8
+ export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext, BrowseToSource, SourceDropdown, SourceDropdownContainer, };
9
+ export * from './types';
10
+ export type ResourceBrowserProps = {
11
+ modalTitle: string;
12
+ allowedTypes?: string[];
13
+ isDisabled?: boolean;
14
+ value: ResourceBrowserUnresolvedResource | null;
15
+ onChange(resource: ResourceBrowserResource | null): void;
16
+ onClear?(): void;
17
+ };
18
+ export declare const ResourceBrowser: (props: ResourceBrowserProps) => React.JSX.Element;
@@ -0,0 +1,79 @@
1
+ import React, { useState, useContext, useEffect, useCallback } from 'react';
2
+ import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext';
3
+ import { useSources } from './Hooks/useSources';
4
+ import { PluginRender } from './Plugin/Plugin';
5
+ import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider';
6
+ import BrowseToSource from './BrowseToSource/BrowseToSource';
7
+ import SourceDropdown from './SourceDropdown/SourceDropdown';
8
+ import SourceDropdownContainer from './SourceDropdownContainer/SourceDropdownContainer';
9
+ export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext, BrowseToSource, SourceDropdown, SourceDropdownContainer, };
10
+ export * from './types';
11
+ export const ResourceBrowser = (props) => {
12
+ const { value } = props;
13
+ const [error, setError] = useState(null);
14
+ const { onRequestSources, searchEnabled, plugins } = useContext(ResourceBrowserContext);
15
+ const [isModalOpen, setIsModalOpen] = useState(false);
16
+ const [source, setSource] = useState(null);
17
+ const [mode, setMode] = useState(null);
18
+ const { data: sources, isLoading, error: sourcesError } = useSources({ onRequestSources, plugins });
19
+ const [plugin, setPlugin] = useState(null);
20
+ // 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
21
+ const handleSourceSelect = useCallback((source, mode) => {
22
+ setSource(source);
23
+ setMode(mode || null);
24
+ }, [setSource, setMode]);
25
+ // If an existing resource is passed in auto select its source
26
+ useEffect(() => {
27
+ let source = null;
28
+ setError(null);
29
+ // If there is a provided value try to use its source
30
+ if (value) {
31
+ // Search the sources for it matching against the value.source property
32
+ source = sources.find((source) => source.id === value?.sourceId) || null;
33
+ // If the source is null and we arent loading the sources
34
+ if (source === null && !isLoading) {
35
+ // Set an error as the passed in value's source wasnt returned by onRequestSources
36
+ setError(new Error('Unable to find resource source.'));
37
+ }
38
+ }
39
+ else if (sources?.length === 1 && !searchEnabled) {
40
+ // If only one source is passed and search is not enabled select it automatically
41
+ source = sources[0];
42
+ }
43
+ setSource(source);
44
+ setMode(null); // Passed in resource will always use the default mode
45
+ }, [value, isLoading, sources, setSource, setError]);
46
+ // When a source is selected update our plugin reference to match (legacy support)
47
+ // the plugin is now attached to the source directly when fetched from the context so use that instead when possible
48
+ useEffect(() => {
49
+ if (source?.plugin) {
50
+ setPlugin(source.plugin);
51
+ }
52
+ else {
53
+ setPlugin(null);
54
+ }
55
+ }, [plugins, source]);
56
+ // The modal has some control over it own open/closed state (for WCAG reasons) so keep this in sync with our state
57
+ const handleModalStateChange = useCallback((isOpen) => {
58
+ setIsModalOpen(isOpen);
59
+ }, [setIsModalOpen]);
60
+ // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
61
+ useEffect(() => {
62
+ if (!isModalOpen && !value && (sources?.length > 1 || searchEnabled)) {
63
+ setSource(null);
64
+ setMode(null);
65
+ }
66
+ }, [sources, isModalOpen]);
67
+ // Render a default "plugin" and one for each item in the plugins array. They are conditionally rendered based on what is selected
68
+ return (React.createElement("div", { className: "squiz-rb-scope" },
69
+ React.createElement(PluginRender, { key: "default", render: plugin === null, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
70
+ return {
71
+ data: null,
72
+ error: null,
73
+ isLoading: false,
74
+ };
75
+ }, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange }),
76
+ plugins.map((thisPlugin) => {
77
+ return (React.createElement(PluginRender, { key: thisPlugin.type, render: thisPlugin === plugin, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange }));
78
+ })));
79
+ };
@@ -0,0 +1,97 @@
1
+ import React, { ReactElement } from 'react';
2
+ import { SquizImageType } from '@squiz/dx-json-schema-lib';
3
+ export type OnRequestSources = () => Promise<ResourceBrowserSource[]>;
4
+ export type ResourceBrowserPluginType = 'dam' | 'matrix';
5
+ export type AuthenticationConfiguration = {
6
+ authUrl: string;
7
+ redirectUrl: string;
8
+ clientId: string;
9
+ scope: string;
10
+ };
11
+ export interface ResourceBrowserSource {
12
+ name?: string;
13
+ id: string;
14
+ type: ResourceBrowserPluginType;
15
+ }
16
+ export interface ResourceBrowserSourceWithPlugin extends ResourceBrowserSource {
17
+ plugin: ResourceBrowserPlugin;
18
+ }
19
+ export interface ResourceBrowserSourceWithConfig extends ResourceBrowserSource {
20
+ configuration?: AuthenticationConfiguration;
21
+ }
22
+ export declare enum PluginLaunchModeType {
23
+ 'Browse' = "browse",
24
+ 'Search' = "search"
25
+ }
26
+ export type PluginLaunchMode = {
27
+ type: PluginLaunchModeType;
28
+ args?: ResourceBrowserSearchUIArgs | ResourceBrowserUIArgs;
29
+ };
30
+ export type ResourceBrowserUnresolvedResource = {
31
+ sourceId: string;
32
+ resourceId: string;
33
+ };
34
+ export type ResourceBrowserResource = {
35
+ id: string;
36
+ name: string;
37
+ url: string;
38
+ source: ResourceBrowserSource;
39
+ type: {
40
+ code: string;
41
+ name: string;
42
+ };
43
+ squizImage?: SquizImageType['__shape__'];
44
+ };
45
+ export type ResourceBrowserSelectedState = {
46
+ showThumbnail?: boolean;
47
+ icon?: ReactElement;
48
+ label: string;
49
+ description: Array<ReactElement>;
50
+ };
51
+ type ResourceBrowserProps = {
52
+ source: ResourceBrowserSource;
53
+ onSourceSelect(source: ResourceBrowserSource, mode?: PluginLaunchMode): void;
54
+ sources: ResourceBrowserSourceWithPlugin[];
55
+ allowedTypes?: string[];
56
+ headerPortal?: Element;
57
+ onSelected: (resource: ResourceBrowserResource) => void;
58
+ searchEnabled: boolean;
59
+ };
60
+ export type ResourceBrowserUIArgs = {
61
+ preselectedResource?: ResourceBrowserResource;
62
+ browseTo?: ResourceBrowserUnresolvedResource;
63
+ };
64
+ export type ResourceBrowserUIProps = ResourceBrowserProps & ResourceBrowserUIArgs;
65
+ export type ResourceBrowserSearchUIArgs = {
66
+ query?: string;
67
+ };
68
+ export type ResourceBrowserSearchUIProps = ResourceBrowserProps & ResourceBrowserSearchUIArgs;
69
+ export type ResourceBrowserLauncherProps = {
70
+ source: ResourceBrowserSource;
71
+ onSearch: (query: string) => void;
72
+ onBrowse: (browseTo?: ResourceBrowserUnresolvedResource) => void;
73
+ };
74
+ export type useResolveResourceResponse = {
75
+ data: ResourceBrowserResource | null;
76
+ error: Error | null;
77
+ isLoading: boolean;
78
+ };
79
+ /**
80
+ * If you change this interface please update the example here: src/__mocks__/PluginExample.tsx
81
+ */
82
+ export interface ResourceBrowserPlugin {
83
+ /** Datasource type this plugin should be used for */
84
+ type: ResourceBrowserPluginType;
85
+ createHeaderPortal?: boolean;
86
+ /** React Functional Component to provde the UI to render to allow a user to browse for resource to use */
87
+ sourceBrowserComponent: () => React.FunctionComponent<ResourceBrowserUIProps>;
88
+ /** React Functional Component to provde the UI to render to allow a user to search for resource to use */
89
+ sourceSearchComponent: () => React.FunctionComponent<ResourceBrowserSearchUIProps>;
90
+ /** React Functional Component to provde the sources launcher view */
91
+ renderResourceLauncher: () => React.FunctionComponent<ResourceBrowserLauncherProps>;
92
+ /** Function to provde the the summary information to show what resource is currently selected */
93
+ renderSelectedResource: (resource: ResourceBrowserResource) => Promise<ResourceBrowserSelectedState>;
94
+ /** Function to resolve a resource and source id into a fully resolved output reference */
95
+ useResolveResource: (resourceId: string | null, source: ResourceBrowserSource | null) => useResolveResourceResponse;
96
+ }
97
+ export {};
@@ -0,0 +1,5 @@
1
+ export var PluginLaunchModeType;
2
+ (function (PluginLaunchModeType) {
3
+ PluginLaunchModeType["Browse"] = "browse";
4
+ PluginLaunchModeType["Search"] = "search";
5
+ })(PluginLaunchModeType || (PluginLaunchModeType = {}));
@@ -0,0 +1,5 @@
1
+ import { AuthenticationConfiguration } from '../types';
2
+ export declare const getCookieValue: (name: string) => string | null;
3
+ export declare const setCookieValue: (name: string, value: string) => void;
4
+ export declare const logout: () => void;
5
+ export declare const refreshAccessToken: (authConfig?: AuthenticationConfiguration) => Promise<string>;
@@ -0,0 +1,31 @@
1
+ export const getCookieValue = (name) => {
2
+ const match = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
3
+ return match ? match.pop() : null;
4
+ };
5
+ export const setCookieValue = (name, value) => {
6
+ document.cookie = `${name}=${value}; Path=/;`;
7
+ };
8
+ export const logout = () => {
9
+ setCookieValue('authToken', '');
10
+ setCookieValue('refreshToken', '');
11
+ };
12
+ export const refreshAccessToken = async (authConfig) => {
13
+ if (!authConfig) {
14
+ throw new Error('No auth configuration available');
15
+ }
16
+ const refreshToken = getCookieValue('refreshToken');
17
+ if (!refreshToken) {
18
+ throw new Error('You are not logged in');
19
+ }
20
+ const response = await fetch(`${authConfig.redirectUrl}?grant_type=refresh_token&refresh_token=${refreshToken}`, {
21
+ method: 'GET',
22
+ credentials: 'include',
23
+ });
24
+ if (!response.ok) {
25
+ logout();
26
+ throw new Error('Failed to refresh token');
27
+ }
28
+ const data = await response.json();
29
+ setCookieValue('authToken', data.access_token);
30
+ return data.access_token;
31
+ };