Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files_example/public/components/file_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone, onUplo
onDone={onDone}
onUpload={(n) => onUpload(n.map(({ id }) => id))}
pageSize={50}
multiple
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,30 @@
*/

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';
import { i18nTexts } from '../i18n_texts';

interface Props {
kind: string;
multiple: boolean;
}

export const UploadFilesPrompt: FunctionComponent<Props> = ({ kind }) => {
export const EmptyPrompt: FunctionComponent<Props> = ({ kind, multiple }) => {
const { state } = useFilePickerContext();
return (
<EuiEmptyPrompt
data-test-subj="emptyPrompt"
title={<h3>{i18nTexts.emptyStatePrompt}</h3>}
body={
<EuiText color="subdued" size="s">
<p>{i18nTexts.emptyStatePromptSubtitle}</p>
</EuiText>
}
titleSize="s"
actions={[
// TODO: We can remove this once the entire modal is an upload area
<UploadFile
kind={kind}
immediate
multiple={multiple}
onDone={(file) => {
state.selectFile(file.map(({ id }) => id));
state.retry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ interface Props {
kind: string;
onDone: SelectButtonProps['onClick'];
onUpload?: FilePickerProps['onUpload'];
multiple: boolean;
}

export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload }) => {
export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload, multiple }) => {
const { state } = useFilePickerContext();
const onUploadStart = useCallback(() => state.setIsUploading(true), [state]);
const onUploadEnd = useCallback(() => state.setIsUploading(false), [state]);
Expand Down Expand Up @@ -53,7 +54,7 @@ export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload }
onUploadEnd={onUploadEnd}
kind={kind}
initialPromptText={i18nTexts.uploadFilePlaceholderText}
multiple
multiple={multiple}
compressed
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ multiple }) => (
<EuiTitle>
<h2>{i18nTexts.title}</h2>
<h2>{multiple ? i18nTexts.titleMultiple : i18nTexts.title}</h2>
</EuiTitle>
);
6 changes: 4 additions & 2 deletions src/plugins/files/public/components/file_picker/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ const FilePickerCtx = createContext<FilePickerContextValue>(
interface FilePickerContextProps {
kind: string;
pageSize: number;
multiple: boolean;
}
export const FilePickerContext: FunctionComponent<FilePickerContextProps> = ({
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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const defaultProps: FilePickerProps = {
kind,
onDone: action('done!'),
onClose: action('close!'),
multiple: true,
};

export default {
Expand Down Expand Up @@ -198,3 +199,25 @@ TryFilter.decorators = [
);
},
];

export const SingleSelect = Template.bind({});
SingleSelect.decorators = [
(Story) => (
<FilesContext
client={
{
getDownloadHref: () => `data:image/png;base64,${base64dLogo}`,
list: async (): Promise<FilesClientResponses['list']> => ({
files: [createFileJSON(), createFileJSON(), createFileJSON()],
total: 1,
}),
} as unknown as FilesClient
}
>
<Story />
</FilesContext>
),
];
SingleSelect.args = {
multiple: undefined,
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('FilePicker', () => {
async function initTestBed(props?: Partial<Props>) {
const createTestBed = registerTestBed((p: Props) => (
<FilesContext client={client}>
<FilePicker {...p} />
<FilePicker multiple {...p} />
</FilesContext>
));

Expand Down
32 changes: 24 additions & 8 deletions src/plugins/files/public/components/file_picker/file_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -53,9 +53,17 @@ export interface Props<Kind extends string = string> {
* 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<Props> = ({ onClose, onDone, onUpload }) => {
type InnerProps = Required<Pick<Props, 'onClose' | 'onDone' | 'onUpload' | 'multiple'>>;

const Component: FunctionComponent<InnerProps> = ({ onClose, onDone, onUpload, multiple }) => {
const { state, kind } = useFilePickerContext();

const hasFiles = useBehaviorSubject(state.hasFiles$);
Expand All @@ -65,7 +73,9 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {

useObservable(state.files$);

const renderFooter = () => <ModalFooter kind={kind} onDone={onDone} onUpload={onUpload} />;
const renderFooter = () => (
<ModalFooter kind={kind} onDone={onDone} onUpload={onUpload} multiple={multiple} />
);

return (
<EuiModal
Expand All @@ -75,7 +85,7 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {
onClose={onClose}
>
<EuiModalHeader>
<Title />
<Title multiple={multiple} />
<SearchField />
</EuiModalHeader>
{isLoading ? (
Expand All @@ -93,7 +103,7 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {
</EuiModalBody>
) : !hasFiles && !hasQuery ? (
<EuiModalBody>
<UploadFilesPrompt kind={kind} />
<EmptyPrompt multiple={multiple} kind={kind} />
</EuiModalBody>
) : (
<>
Expand All @@ -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>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('FilePickerState', () => {
client: filesClient,
pageSize: 20,
kind: 'test',
selectMultiple: true,
});
});
it('starts off empty', () => {
Expand Down Expand Up @@ -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']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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$
Expand Down Expand Up @@ -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();
};

Expand Down Expand Up @@ -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);
};
12 changes: 6 additions & 6 deletions src/plugins/files/public/components/file_picker/i18n_texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down