import React, { useState, useContext, useEffect, useCallback, useMemo } from 'react'; import { ResourceBrowserContext, ResourceBrowserContextProvider } from './ResourceBrowserContext/ResourceBrowserContext'; import { InlineType, PluginLaunchMode, ResourceBrowserUnresolvedResource, ResourceBrowserResource, ResourceBrowserSourceWithPlugin, ResourceBrowserPluginType, } from './types'; import { useSources } from './Hooks/useSources'; import { PluginRender } from './Plugin/Plugin'; import { AuthProvider, useAuthContext, AuthContext } from './ResourceBrowserContext/AuthProvider'; import BrowseToSource from './BrowseToSource/BrowseToSource'; import SourceDropdown from './SourceDropdown/SourceDropdown'; import SourceDropdownContainer from './SourceDropdownContainer/SourceDropdownContainer'; export { ResourceBrowserContext, ResourceBrowserContextProvider, useAuthContext, AuthProvider, AuthContext, BrowseToSource, SourceDropdown, SourceDropdownContainer, }; export * from './types'; export type ResourceBrowserProps = { modalTitle: string; allowedTypes?: string[]; allowedPlugins?: ResourceBrowserPluginType[]; isDisabled?: boolean; value: ResourceBrowserUnresolvedResource | null; inline?: boolean; // Will render open button only, no input / showing of existing selection inlineType?: InlineType; // Type of inline button to show onChange(resource: ResourceBrowserResource | null): void; onModalStateChange?(isOpen: boolean): void; onClear?(): void; }; export const ResourceBrowser = React.forwardRef((props, forwardRef) => { const { value, inline, inlineType, allowedPlugins, onModalStateChange: onModalStateChangeExternalNotification } = props; const [error, setError] = useState(null); const { onRequestSources, searchEnabled, plugins: allPlugins } = useContext(ResourceBrowserContext); const [isModalOpen, setIsModalOpen] = useState(false); const [source, setSource] = useState(null); const plugins = useMemo(() => { // Allow some usages e.g. plugin specific like MatrixAssetWidget restrict what plugins show if (!allowedPlugins) return allPlugins; // No restrictions, return all return allPlugins.filter((plugin) => { return allowedPlugins.includes(plugin.type); // Restrict based on plugin type }); }, [allowedPlugins, allPlugins]); const [mode, setMode] = useState(null); const { data: sources, isLoading, error: sourcesError, reload: reloadSources } = useSources({ onRequestSources, plugins }); const plugin = source?.plugin || null; // Find source by its id or alias const findSourceById = (value: ResourceBrowserUnresolvedResource, sources: ResourceBrowserSourceWithPlugin[]) => { let newSource = sources.find((source) => source.id === value?.sourceId) || null; if (!newSource) { newSource = sources.find((source) => { if (!source.aliases) return false; return source.aliases.includes(value.sourceId); }) || null; } return newSource; }; // Check if the value is for the source const isValueForSource = (value: ResourceBrowserUnresolvedResource, source: ResourceBrowserSourceWithPlugin) => { return value?.sourceId === source.id || (source.aliases && source.aliases.includes(value?.sourceId)); }; // 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 const handleSourceSelect = useCallback( (source: ResourceBrowserSourceWithPlugin, mode?: PluginLaunchMode) => { setSource(source); setMode(mode || null); }, [setSource, setMode], ); // If an existing resource is passed in auto select its source useEffect(() => { let newSource: ResourceBrowserSourceWithPlugin | null = null; setError(null); if (source !== null) { return; } // If there is a provided value try to use its source if (value) { // Search the sources for it matching against the value.source property newSource = findSourceById(value, sources); // If the source is null and we arent loading the sources if (newSource === null && !isLoading) { // Set an error as the passed in value's source wasnt returned by onRequestSources setError(new Error('Unable to find resource source.')); } } else if (sources?.length === 1 && !searchEnabled) { // If only one source is passed and search is not enabled select it automatically newSource = sources[0]; } setSource(newSource); setMode(null); // Passed in resource will always use the default mode }, [value, isLoading, sources, source]); // The modal has some control over it own open/closed state (for WCAG reasons) so keep this in sync with our state const handleModalStateChange = useCallback( (isOpen: boolean) => { setIsModalOpen(isOpen); onModalStateChangeExternalNotification?.(isOpen); }, [setIsModalOpen, onModalStateChangeExternalNotification], ); // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open useEffect(() => { // If modal is closed and we dont have a value if (!isModalOpen && !value && (sources?.length > 1 || searchEnabled)) { setSource(null); setMode(null); } else if (!isModalOpen) { // Otherwise reset the RB mode setMode(null); // If there is a value passed in, reset the source so the preselected asset preview renders correctly if (value && value.sourceId !== source?.id) { setSource(findSourceById(value, sources)); } } }, [sources, isModalOpen]); // Reset function to allow user manual reload if sources fail in inline usage const handleReset = useCallback(() => { reloadSources(); }, [reloadSources]); // Render a default "plugin" and one for each item in the plugins array. They are conditionally rendered based on what is selected return (
{ return { data: null, error: null, isLoading: false, }; }} isModalOpen={isModalOpen} onModalStateChange={handleModalStateChange} onRetry={handleReset} /> {plugins.map((thisPlugin) => { return ( ); })}
); });