[Files] Copy updates and file picker single select (#144398)

* 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
This commit is contained in:
Jean-Louis Leysens 2022-11-03 10:11:43 +01:00 committed by GitHub
parent cf152eae23
commit 4a44fd31bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 105 additions and 31 deletions

View file

@ -27,6 +27,7 @@ export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone, onUplo
onDone={onDone}
onUpload={(n) => onUpload(n.map(({ id }) => id))}
pageSize={50}
multiple
/>
);
};

View file

@ -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<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();

View file

@ -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]);
@ -53,7 +54,7 @@ export const ModalFooter: FunctionComponent<Props> = ({ kind, onDone, onUpload }
onUploadEnd={onUploadEnd}
kind={kind}
initialPromptText={i18nTexts.uploadFilePlaceholderText}
multiple
multiple={multiple}
compressed
/>
</div>

View file

@ -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>
);

View file

@ -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 (

View file

@ -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) => (
<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,
};

View file

@ -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>
));

View file

@ -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<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$);
@ -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
@ -75,7 +85,7 @@ const Component: FunctionComponent<Props> = ({ onClose, onDone, onUpload }) => {
onClose={onClose}
>
<EuiModalHeader>
<Title />
<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>
);

View file

@ -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']);
});
});
});

View file

@ -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);
};

View file

@ -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',