diff --git a/examples/files_example/public/components/file_picker.tsx b/examples/files_example/public/components/file_picker.tsx index 72a2057755742..220ca917b78b6 100644 --- a/examples/files_example/public/components/file_picker.tsx +++ b/examples/files_example/public/components/file_picker.tsx @@ -27,6 +27,7 @@ export const MyFilePicker: FunctionComponent = ({ onClose, onDone, onUplo onDone={onDone} onUpload={(n) => onUpload(n.map(({ id }) => id))} pageSize={50} + multiple /> ); }; diff --git a/src/plugins/files/public/components/file_picker/components/upload_files.tsx b/src/plugins/files/public/components/file_picker/components/empty_prompt.tsx similarity index 80% rename from src/plugins/files/public/components/file_picker/components/upload_files.tsx rename to src/plugins/files/public/components/file_picker/components/empty_prompt.tsx index a33f458bdf9d2..84b52281805ad 100644 --- a/src/plugins/files/public/components/file_picker/components/upload_files.tsx +++ b/src/plugins/files/public/components/file_picker/components/empty_prompt.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { EuiEmptyPrompt } from '@elastic/eui'; import type { FunctionComponent } from 'react'; import { UploadFile } from '../../upload_file'; import { useFilePickerContext } from '../context'; @@ -15,25 +15,22 @@ import { i18nTexts } from '../i18n_texts'; interface Props { kind: string; + multiple: boolean; } -export const UploadFilesPrompt: FunctionComponent = ({ kind }) => { +export const EmptyPrompt: FunctionComponent = ({ kind, multiple }) => { const { state } = useFilePickerContext(); return ( {i18nTexts.emptyStatePrompt}} - body={ - -

{i18nTexts.emptyStatePromptSubtitle}

-
- } titleSize="s" actions={[ // TODO: We can remove this once the entire modal is an upload area { state.selectFile(file.map(({ id }) => id)); state.retry(); diff --git a/src/plugins/files/public/components/file_picker/components/modal_footer.tsx b/src/plugins/files/public/components/file_picker/components/modal_footer.tsx index 0a9ad3b3dcafa..d5d60fbd7cc8b 100644 --- a/src/plugins/files/public/components/file_picker/components/modal_footer.tsx +++ b/src/plugins/files/public/components/file_picker/components/modal_footer.tsx @@ -22,9 +22,10 @@ interface Props { kind: string; onDone: SelectButtonProps['onClick']; onUpload?: FilePickerProps['onUpload']; + multiple: boolean; } -export const ModalFooter: FunctionComponent = ({ kind, onDone, onUpload }) => { +export const ModalFooter: FunctionComponent = ({ kind, onDone, onUpload, multiple }) => { const { state } = useFilePickerContext(); const onUploadStart = useCallback(() => state.setIsUploading(true), [state]); const onUploadEnd = useCallback(() => state.setIsUploading(false), [state]); @@ -53,7 +54,7 @@ export const ModalFooter: FunctionComponent = ({ kind, onDone, onUpload } onUploadEnd={onUploadEnd} kind={kind} initialPromptText={i18nTexts.uploadFilePlaceholderText} - multiple + multiple={multiple} compressed /> diff --git a/src/plugins/files/public/components/file_picker/components/title.tsx b/src/plugins/files/public/components/file_picker/components/title.tsx index 2d4de8881fa76..98ce9b0bc93d1 100644 --- a/src/plugins/files/public/components/file_picker/components/title.tsx +++ b/src/plugins/files/public/components/file_picker/components/title.tsx @@ -11,8 +11,12 @@ import type { FunctionComponent } from 'react'; import { EuiTitle } from '@elastic/eui'; import { i18nTexts } from '../i18n_texts'; -export const Title: FunctionComponent = () => ( +interface Props { + multiple: boolean; +} + +export const Title: FunctionComponent = ({ multiple }) => ( -

{i18nTexts.title}

+

{multiple ? i18nTexts.titleMultiple : i18nTexts.title}

); diff --git a/src/plugins/files/public/components/file_picker/context.tsx b/src/plugins/files/public/components/file_picker/context.tsx index 48d0ea3986253..c17fe601e487a 100644 --- a/src/plugins/files/public/components/file_picker/context.tsx +++ b/src/plugins/files/public/components/file_picker/context.tsx @@ -23,17 +23,19 @@ const FilePickerCtx = createContext( interface FilePickerContextProps { kind: string; pageSize: number; + multiple: boolean; } export const FilePickerContext: FunctionComponent = ({ kind, pageSize, + multiple, children, }) => { const filesContext = useFilesContext(); const { client } = filesContext; const state = useMemo( - () => createFilePickerState({ pageSize, client, kind }), - [pageSize, client, kind] + () => createFilePickerState({ pageSize, client, kind, selectMultiple: multiple }), + [pageSize, client, kind, multiple] ); useEffect(() => state.dispose, [state]); return ( diff --git a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx index 517663ebbbbbe..1d38a9893ed57 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.stories.tsx +++ b/src/plugins/files/public/components/file_picker/file_picker.stories.tsx @@ -27,6 +27,7 @@ const defaultProps: FilePickerProps = { kind, onDone: action('done!'), onClose: action('close!'), + multiple: true, }; export default { @@ -198,3 +199,25 @@ TryFilter.decorators = [ ); }, ]; + +export const SingleSelect = Template.bind({}); +SingleSelect.decorators = [ + (Story) => ( + `data:image/png;base64,${base64dLogo}`, + list: async (): Promise => ({ + files: [createFileJSON(), createFileJSON(), createFileJSON()], + total: 1, + }), + } as unknown as FilesClient + } + > + + + ), +]; +SingleSelect.args = { + multiple: undefined, +}; diff --git a/src/plugins/files/public/components/file_picker/file_picker.test.tsx b/src/plugins/files/public/components/file_picker/file_picker.test.tsx index 58c86739755a8..055eea0f4d5ab 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.test.tsx +++ b/src/plugins/files/public/components/file_picker/file_picker.test.tsx @@ -30,7 +30,7 @@ describe('FilePicker', () => { async function initTestBed(props?: Partial) { const createTestBed = registerTestBed((p: Props) => ( - + )); diff --git a/src/plugins/files/public/components/file_picker/file_picker.tsx b/src/plugins/files/public/components/file_picker/file_picker.tsx index 9472f87f7b912..94616a67a85cb 100644 --- a/src/plugins/files/public/components/file_picker/file_picker.tsx +++ b/src/plugins/files/public/components/file_picker/file_picker.tsx @@ -24,7 +24,7 @@ import { useFilePickerContext, FilePickerContext } from './context'; import { Title } from './components/title'; import { ErrorContent } from './components/error_content'; -import { UploadFilesPrompt } from './components/upload_files'; +import { EmptyPrompt } from './components/empty_prompt'; import { FileGrid } from './components/file_grid'; import { SearchField } from './components/search_field'; import { ModalFooter } from './components/modal_footer'; @@ -53,9 +53,17 @@ export interface Props { * The number of results to show per page. */ pageSize?: number; + /** + * Whether you can select one or more files + * + * @default false + */ + multiple?: boolean; } -const Component: FunctionComponent = ({ onClose, onDone, onUpload }) => { +type InnerProps = Required>; + +const Component: FunctionComponent = ({ onClose, onDone, onUpload, multiple }) => { const { state, kind } = useFilePickerContext(); const hasFiles = useBehaviorSubject(state.hasFiles$); @@ -65,7 +73,9 @@ const Component: FunctionComponent = ({ onClose, onDone, onUpload }) => { useObservable(state.files$); - const renderFooter = () => ; + const renderFooter = () => ( + + ); return ( = ({ onClose, onDone, onUpload }) => { onClose={onClose} > - + <Title multiple={multiple} /> <SearchField /> </EuiModalHeader> {isLoading ? ( @@ -93,7 +103,7 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => { </EuiModalBody> ) : !hasFiles && !hasQuery ? ( <EuiModalBody> - <UploadFilesPrompt kind={kind} /> + <EmptyPrompt multiple={multiple} kind={kind} /> </EuiModalBody> ) : ( <> @@ -109,9 +119,15 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => { ); }; -export const FilePicker: FunctionComponent<Props> = (props) => ( - <FilePickerContext pageSize={props.pageSize ?? 20} kind={props.kind}> - <Component {...props} /> +export const FilePicker: FunctionComponent<Props> = ({ + pageSize = 20, + kind, + multiple = false, + onUpload = () => {}, + ...rest +}) => ( + <FilePickerContext pageSize={pageSize} kind={kind} multiple={multiple}> + <Component {...rest} {...{ pageSize, kind, multiple, onUpload }} /> </FilePickerContext> ); diff --git a/src/plugins/files/public/components/file_picker/file_picker_state.test.ts b/src/plugins/files/public/components/file_picker/file_picker_state.test.ts index 0e7971e60bdcc..62881fa042a84 100644 --- a/src/plugins/files/public/components/file_picker/file_picker_state.test.ts +++ b/src/plugins/files/public/components/file_picker/file_picker_state.test.ts @@ -32,6 +32,7 @@ describe('FilePickerState', () => { client: filesClient, pageSize: 20, kind: 'test', + selectMultiple: true, }); }); it('starts off empty', () => { @@ -181,4 +182,20 @@ describe('FilePickerState', () => { expectObservable(filePickerState.files$).toBe('a------', { a: [] }); }); }); + describe('single selection', () => { + beforeEach(() => { + filePickerState = createFilePickerState({ + client: filesClient, + pageSize: 20, + kind: 'test', + selectMultiple: false, + }); + }); + it('allows only one file to be selected', () => { + filePickerState.selectFile('a'); + expect(filePickerState.getSelectedFileIds()).toEqual(['a']); + filePickerState.selectFile(['b', 'a', 'c']); + expect(filePickerState.getSelectedFileIds()).toEqual(['b']); + }); + }); }); diff --git a/src/plugins/files/public/components/file_picker/file_picker_state.ts b/src/plugins/files/public/components/file_picker/file_picker_state.ts index 42214f77c9cf2..a47268585a420 100644 --- a/src/plugins/files/public/components/file_picker/file_picker_state.ts +++ b/src/plugins/files/public/components/file_picker/file_picker_state.ts @@ -56,7 +56,8 @@ export class FilePickerState { constructor( private readonly client: FilesClient, private readonly kind: string, - public readonly pageSize: number + public readonly pageSize: number, + private selectMultiple: boolean ) { this.subscriptions = [ this.query$ @@ -105,8 +106,18 @@ export class FilePickerState { this.internalIsLoading$.next(value); } + /** + * If multiple selection is not configured, this will take the first file id + * if an array of file ids was provided. + */ public selectFile = (fileId: string | string[]): void => { - (Array.isArray(fileId) ? fileId : [fileId]).forEach((id) => this.fileSet.add(id)); + const fileIds = Array.isArray(fileId) ? fileId : [fileId]; + if (!this.selectMultiple) { + this.fileSet.clear(); + this.fileSet.add(fileIds[0]); + } else { + for (const id of fileIds) this.fileSet.add(id); + } this.sendNextSelectedFiles(); }; @@ -216,11 +227,13 @@ interface CreateFilePickerArgs { client: FilesClient; kind: string; pageSize: number; + selectMultiple: boolean; } export const createFilePickerState = ({ pageSize, client, kind, + selectMultiple, }: CreateFilePickerArgs): FilePickerState => { - return new FilePickerState(client, kind, pageSize); + return new FilePickerState(client, kind, pageSize, selectMultiple); }; diff --git a/src/plugins/files/public/components/file_picker/i18n_texts.ts b/src/plugins/files/public/components/file_picker/i18n_texts.ts index 0958e99cdffe1..9bc4b4642cd68 100644 --- a/src/plugins/files/public/components/file_picker/i18n_texts.ts +++ b/src/plugins/files/public/components/file_picker/i18n_texts.ts @@ -12,17 +12,17 @@ export const i18nTexts = { title: i18n.translate('files.filePicker.title', { defaultMessage: 'Select a file', }), + titleMultiple: i18n.translate('files.filePicker.titleMultiple', { + defaultMessage: 'Select files', + }), loadingFilesErrorTitle: i18n.translate('files.filePicker.error.loadingTitle', { defaultMessage: 'Could not load files', }), retryButtonLabel: i18n.translate('files.filePicker.error.retryButtonLabel', { defaultMessage: 'Retry', }), - emptyStatePrompt: i18n.translate('files.filePicker.emptyStatePrompt', { - defaultMessage: 'No files found', - }), - emptyStatePromptSubtitle: i18n.translate('files.filePicker.emptyStatePromptSubtitle', { - defaultMessage: 'Upload your first file.', + emptyStatePrompt: i18n.translate('files.filePicker.emptyStatePromptTitle', { + defaultMessage: 'Upload your first file', }), selectFileLabel: i18n.translate('files.filePicker.selectFileButtonLable', { defaultMessage: 'Select file', @@ -36,7 +36,7 @@ export const i18nTexts = { defaultMessage: 'my-file-*', }), emptyFileGridPrompt: i18n.translate('files.filePicker.emptyGridPrompt', { - defaultMessage: 'No files matched filter', + defaultMessage: 'No files match your filter', }), loadMoreButtonLabel: i18n.translate('files.filePicker.loadMoreButtonLabel', { defaultMessage: 'Load more',