Skip to content

Commit

Permalink
[Files] Copy updates and file picker single select (#144398)
Browse files Browse the repository at this point in the history
* implement copy updates

* move rename file and update empty state copy

* update title to respect multi select vs single select

* added test for single select behaviour

* implement single select behaviour

* introduce and use multiple prop, default true

* pass multiple flag to state

* pass multiple upload flag to UI

* added single select story

* update files example to still support multiple select

* uploadMultiple -> selectMultiple

* remove use of non-existent i18n

* update filepicker react component tests
  • Loading branch information
jloleysens authored Nov 3, 2022
1 parent cf152ea commit 4a44fd3
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 31 deletions.
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

0 comments on commit 4a44fd3

Please sign in to comment.