// External modules import * as React from 'react'; // Types import {superdeskApi} from '../apis'; // UI import {Button, ButtonGroup} from 'superdesk-ui-framework/react'; import {PanelContent, PanelContentBlock, PanelContentBlockInner} from '../ui'; import {IModalSize, Modal, ModalHeader} from '../ui/modal'; import {GridList} from '../ui/grid/GridList'; import {PageLayout} from './PageLayout'; export interface IUploadItem { id: string; binary: File; uploadProgress: number; error: boolean; completed: boolean; } export interface IUploadFileListItemProps { item: IUploadItem; asset: T; selected: boolean; selectFile(): void; removeFile?(): void; } export interface IContentPanelProps { item: IUploadItem; submitting?: boolean; } interface IProps { dark?: boolean; modalSize?: IModalSize; multiple?: boolean; accept?: Array; initialFiles?: Array<{ id: string; file: File; }>; closeModal(): void; title: string; onFileAdded(id: string, file: File): void; onFileRemoved(id: string): void; uploadFile(item: IUploadItem, onProgress: (progressEvent: ProgressEvent) => void): Promise; assets: Dictionary; ListItemComponent: React.ComponentType>; RightPanelComponent: React.ComponentType; } interface IState { selectedIndex: number; submitting: boolean; items: Array; } export class FileUploadModal extends React.Component, IState> { fileInputNode: React.RefObject; constructor(props: IProps) { super(props); this.state = { selectedIndex: 0, submitting: false, items: this.getInitialItems(), }; this.showFileUploadDialog = this.showFileUploadDialog.bind(this); this.addFiles = this.addFiles.bind(this); this.removeFile = this.removeFile.bind(this); this.selectFile = this.selectFile.bind(this); this.onSubmit = this.onSubmit.bind(this); this.fileInputNode = React.createRef(); } componentDidMount() { if (this.state.items.length === 0) { this.showFileUploadDialog(); } } showFileUploadDialog() { if (this.fileInputNode.current != null) { this.fileInputNode.current.click(); } } getInitialItems(): Array { const items: Array = []; if (this.props.initialFiles != null && this.props.initialFiles?.length > 0) { this.props.initialFiles.forEach( (item) => { items.push({ id: item.id, binary: item.file, uploadProgress: 0, error: false, completed: false, }); }, ); } return items; } addFiles(event: React.ChangeEvent) { const newItems: Array = []; Array.from(event.target.files ?? []).forEach( (file: File) => { const id = Math.random().toString(36).substr(1); this.props.onFileAdded(id, file); newItems.push({ id: id, binary: file, uploadProgress: 0, error: false, completed: false, }); }, ); this.setState((state: IState) => ({ items: [ ...state.items, ...newItems, ], })); event.target.value = ''; // reset to allow selecting same file again } getItemIndexById(id: string) { return this.state.items.findIndex((item) => item.id === id); } removeFile(index: number) { this.props.onFileRemoved(this.state.items[index].id); this.setState((state: IState) => { const updates: IState = {...state}; updates.items.splice(index, 1); if (updates.items.length <= 1) { updates.selectedIndex = 0; } else if (index >= updates.items.length) { updates.selectedIndex = index - 1; } return updates; }); } selectFile(index: number) { this.setState({selectedIndex: index}); } updateAssetState(index: number, updates: Partial) { this.setState((state: IState) => { const items: IState['items'] = [...state.items]; items[index] = { ...items[index], ...updates, }; return {items: items}; }); } onSubmit() { this.setState({submitting: true}); let requestsCompleted = 0; let failed = false; this.state.items.forEach( (item, index) => { if (item.completed) { if (item.error === false) { requestsCompleted += 1; return; } else { this.updateAssetState(index, { error: false, completed: false, uploadProgress: 0, }); } } const onSuccess = () => { this.updateAssetState(index, { error: false, completed: true, uploadProgress: 100, }); }; const onFail = () => { failed = true; this.updateAssetState(index, { error: true, completed: true, uploadProgress: 100, }); }; const onCompleted = () => { const {notify} = superdeskApi.ui; const {gettext, gettextPlural} = superdeskApi.localization; requestsCompleted += 1; if (requestsCompleted === this.state.items.length) { if (failed === false) { setTimeout(this.props.closeModal, 500); notify.success( gettextPlural( requestsCompleted, 'File uploaded successfully', '{{count}} files uploaded successfully', {count: requestsCompleted}, ), ); } else { this.setState({submitting: false}); // TODO: Improve displaying error messages to the user notify.error(gettext('Failed to upload files')); } } }; const onProgress = (progressEvent: ProgressEvent) => { // limit progress to 90% and set 100 only after request is done let uploadProgress = Math.min( Math.round( progressEvent.loaded / progressEvent.total * 100.0, ), 90, ); this.updateAssetState(index, {uploadProgress: uploadProgress}); }; this.props.uploadFile(item, onProgress) .then(onSuccess, onFail) .finally(onCompleted); }, ); } render() { const {gettext} = superdeskApi.localization; const {ListItemComponent, RightPanelComponent} = this.props; const headerButtonTheme = this.props.dark ? 'dark' : 'light'; const currentItem = this.state.items[this.state.selectedIndex]; return (