mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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:
parent
cf152eae23
commit
4a44fd31bb
11 changed files with 105 additions and 31 deletions
|
@ -27,6 +27,7 @@ export const MyFilePicker: FunctionComponent<Props> = ({ onClose, onDone, onUplo
|
|||
onDone={onDone}
|
||||
onUpload={(n) => onUpload(n.map(({ id }) => id))}
|
||||
pageSize={50}
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
));
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue