diff --git a/app/actions/api.ts b/app/actions/api.ts index 260ee2f2..6a09bd6a 100644 --- a/app/actions/api.ts +++ b/app/actions/api.ts @@ -29,7 +29,7 @@ import { selectWorkingDatasetBodyPageInfo, selectMutationsDataset } from '../selections' import { CALL_API, ApiAction, ApiActionThunk, chainSuccess } from '../store/api' -import { openToast } from './ui' +import { openToast, setBulkActionExecuting } from './ui' import { actionWithPagination } from '../utils/pagination' import { getActionType } from '../utils/actionType' import { datasetConvertStringToScriptBytes } from '../utils/datasetConvertStringToScriptBytes' @@ -641,52 +641,32 @@ export function removeDataset ( } } - let response: Action - try { - response = await dispatch(action) - dispatch(openToast('success', 'remove', `Removed ${username}/${name}`)) - } catch (action) { - dispatch(openToast('error', 'remove', action.payload.err.message)) - throw action - } - - return response - } -} - -// remove the specified dataset, then refresh the dataset list -export function removeDatasetAndFetch (username: string, name: string, isLinked: boolean, keepFiles: boolean): ApiActionThunk { - return async (dispatch, getState) => { - let response: Action - - try { - response = await removeDataset(username, name, isLinked, keepFiles)(dispatch, getState) - } catch (action) { - if (!action.payload.err.message.contains('directory not empty')) { - throw action - } - } - // reset pagination - dispatch(push(pathToCollection())) - response = await fetchMyDatasets(-1)(dispatch, getState) - return response + return dispatch(action) } } // remove the specified datasets, then refresh the dataset list export function removeDatasetsAndFetch (refs: VersionInfo[], keepFiles: boolean): ApiActionThunk { return async (dispatch, getState) => { + const datasetsString = refs.length === 1 ? `${refs[0].username}/${refs[0].name}` : `${refs.length} datasets` try { + dispatch(setBulkActionExecuting(true)) + dispatch(openToast('info', 'remove', `removing ${datasetsString}`)) await Promise.all( refs.map(async (ref) => removeDataset(ref.username, ref.name, !!ref.fsiPath, keepFiles)(dispatch, getState)) ) + dispatch(openToast('success', `remove-success`, `removed ${datasetsString}`)) // reset pagination - return fetchMyDatasets(-1)(dispatch, getState) + const res = await fetchMyDatasets(-1)(dispatch, getState) + dispatch(push(pathToCollection())) + dispatch(setBulkActionExecuting(false)) + return res } catch (action) { if (!action.payload.err.message.contains('directory not empty')) { throw action } - dispatch(openToast('error', 'removeDatasetsAndFetch', action.payload.err.message)) + dispatch(openToast('error', 'remove-error', `error removing ${datasetsString}: ${action.payload.err.message}`)) + dispatch(setBulkActionExecuting(false)) return Promise.reject(action.payload.err.message) } } diff --git a/app/actions/ui.ts b/app/actions/ui.ts index 47c2ac6e..94b7c86a 100644 --- a/app/actions/ui.ts +++ b/app/actions/ui.ts @@ -9,7 +9,8 @@ import { UI_SET_DATASET_DIR_PATH, UI_SET_EXPORT_PATH, UI_SET_DETAILS_BAR, - UI_SET_BOOTUP_COMPONENT + UI_SET_BOOTUP_COMPONENT, + UI_BULK_ACTION_EXECUTING } from '../reducers/ui' import { ToastType, BootupComponentType } from '../models/store' @@ -98,3 +99,10 @@ export const setBootupComponent = (component: BootupComponentType) => { component } } + +export const setBulkActionExecuting = (executing: boolean) => { + return { + type: UI_BULK_ACTION_EXECUTING, + executing + } +} diff --git a/app/components/collection/collectionHome/DatasetList.tsx b/app/components/collection/collectionHome/DatasetList.tsx index 4d011ace..bfaa8d9e 100644 --- a/app/components/collection/collectionHome/DatasetList.tsx +++ b/app/components/collection/collectionHome/DatasetList.tsx @@ -13,9 +13,9 @@ import { connectComponentToPropsWithRouter } from '../../../utils/connectCompone import { setFilter } from '../../../actions/myDatasets' import { pullDatasets, fetchMyDatasets, removeDatasetsAndFetch } from '../../../actions/api' import { setWorkingDataset } from '../../../actions/selections' -import { setModal, openToast } from '../../../actions/ui' +import { setModal, openToast, setBulkActionExecuting } from '../../../actions/ui' -import { selectSessionUsername, selectWorkingDataset } from '../../../selections' +import { selectSessionUsername, selectWorkingDataset, selectBulkActionExecuting } from '../../../selections' import DatasetsTable from './DatasetsTable' interface DatasetListProps extends RouteProps { @@ -24,25 +24,37 @@ interface DatasetListProps extends RouteProps { workingDataset: WorkingDataset sessionUsername: string showFSI: boolean + bulkActionExecuting: boolean setFilter: (filter: string) => Action setWorkingDataset: (username: string, name: string) => Action fetchMyDatasets: (page: number, pageSize: number) => Promise pullDatasets: (refs: VersionInfo[]) => Promise - removeDatasetsAndFetch: (refs: VersionInfo[], keepFiles: boolean) => Promise setModal: (modal: Modal) => void openToast: (type: ToastType, name: string, message: string) => Action + setBulkActionExecuting: (executing: boolean) => Action } type DatasetActionType = 'pull' | 'remove' export const DatasetListComponent: React.FC = (props) => { - const { showFSI, setFilter, myDatasets, history, sessionUsername, pullDatasets, removeDatasetsAndFetch, openToast, setModal } = props + const { + showFSI, + setFilter, + myDatasets, + history, + sessionUsername, + bulkActionExecuting, + pullDatasets, + openToast, + setModal, + setBulkActionExecuting + } = props + const { filter, value: datasets } = myDatasets const lowercasedFilterString = filter.toLowerCase() const [selected, setSelected] = useState([] as VersionInfo[]) const [onlySessionUserDatasets, setOnlySessionUserDatasets] = useState(false) - const [bulkActionExecuting, setBulkActionExecuting] = useState(false) const handleSetFilter = (value: string) => { setFilter(value) @@ -109,11 +121,6 @@ export const DatasetListComponent: React.FC = (props) => { return handleBulkActionForDatasets('pull', 'pulling', 'pulled', actionCallback, selected) } - const handleBulkRemove = async (keepFiles: boolean) => { - const actionCallback = async () => removeDatasetsAndFetch(selected, keepFiles) - return handleBulkActionForDatasets('remove', 'removing', 'removed', actionCallback, selected) - } - const handleBulkActionForDatasets = async (actionType: DatasetActionType, actionGerund: string, actionPastTense: string, actionCallback: () => Promise, refs: VersionInfo[]) => { setBulkActionExecuting(true) openToast('info', actionType, `${actionGerund} ${refs.length} ${refs.length === 1 ? 'dataset' : 'datasets'}`) @@ -134,8 +141,7 @@ export const DatasetListComponent: React.FC = (props) => { setModal( { type: ModalType.RemoveDataset, - datasets: selected, - onSubmit: handleBulkRemove + datasets: selected } ) } @@ -201,7 +207,8 @@ export default connectComponentToPropsWithRouter( ...ownProps, myDatasets: state.myDatasets, sessionUsername: selectSessionUsername(state), - setWorkingDataset: selectWorkingDataset(state) + setWorkingDataset: selectWorkingDataset(state), + bulkActionExecuting: selectBulkActionExecuting(state) }), { setFilter, @@ -210,6 +217,7 @@ export default connectComponentToPropsWithRouter( setModal, pullDatasets, removeDatasetsAndFetch, - openToast + openToast, + setBulkActionExecuting } ) diff --git a/app/components/collection/headerButtons/RemoveButton.tsx b/app/components/collection/headerButtons/RemoveButton.tsx index e01300af..12e9f80a 100644 --- a/app/components/collection/headerButtons/RemoveButton.tsx +++ b/app/components/collection/headerButtons/RemoveButton.tsx @@ -9,16 +9,13 @@ import { selectFsiPath } from '../../../selections' import { connectComponentToPropsWithRouter } from '../../../utils/connectComponentToProps' import HeaderColumnButton from '../../chrome/HeaderColumnButton' -import { removeDatasetsAndFetch } from '../../../actions/api' -import { ApiAction } from '../../../store/api' -import { VersionInfo } from '../../../models/store' interface RemoveButtonProps extends RouteProps { qriRef: QriRef fsiPath: string showIcon: boolean setModal: (modal: Modal) => void - removeDatasetsAndFetch: (datasets: VersionInfo[], keepfiles: boolean) => Promise + size: 'sm' | 'md' } /** @@ -31,8 +28,8 @@ export const RemoveButtonComponent: React.FunctionComponent = const { qriRef, fsiPath, + size = 'md', showIcon = true, - removeDatasetsAndFetch, setModal } = props @@ -42,15 +39,15 @@ export const RemoveButtonComponent: React.FunctionComponent = return null } return ( { setModal({ type: ModalType.RemoveDataset, - datasets: [{ ...qriRef, fsiPath }], - onSubmit: async (keepfiles: boolean) => removeDatasetsAndFetch([{ ...qriRef, fsiPath }], keepfiles) + datasets: [{ ...qriRef, fsiPath }] }) }} />) @@ -66,7 +63,6 @@ export default connectComponentToPropsWithRouter( fsiPath: selectFsiPath(state) } }, { - setModal, - removeDatasetsAndFetch + setModal } ) diff --git a/app/components/modals/RemoveDataset.tsx b/app/components/modals/RemoveDataset.tsx index 86534192..4a1f1231 100644 --- a/app/components/modals/RemoveDataset.tsx +++ b/app/components/modals/RemoveDataset.tsx @@ -4,20 +4,24 @@ import { RemoveDatasetModal } from '../../../app/models/modals' import { connectComponentToProps } from '../../utils/connectComponentToProps' import { dismissModal } from '../../actions/ui' import { selectModal, selectSessionUsername } from '../../selections' +import { ApiAction } from '../../store/api' +import { removeDatasetsAndFetch } from '../../actions/api' import CheckboxInput from '../form/CheckboxInput' import Modal from './Modal' import Error from './Error' import Buttons from './Buttons' +import { VersionInfo } from '../../models/store' interface RemoveDatasetProps { modal: RemoveDatasetModal sessionUsername: string onDismissed: () => void + onSubmit: (refs: VersionInfo[], keepfiles: boolean) => Promise } export const RemoveDatasetComponent: React.FC = (props: RemoveDatasetProps) => { - const { modal: { datasets, onSubmit }, sessionUsername, onDismissed } = props + const { modal: { datasets }, sessionUsername, onDismissed, onSubmit } = props const [keepFiles, setKeepFiles] = useState(true) const [dismissable, setDismissable] = useState(true) @@ -38,7 +42,7 @@ export const RemoveDatasetComponent: React.FC = (props: Remo if (!onSubmit) return try { - await onSubmit(keepFiles) + await onSubmit(datasets, keepFiles) setLoading(false) setDismissable(true) onDismissed() @@ -104,6 +108,7 @@ export default connectComponentToProps( } }, { - onDismissed: dismissModal + onDismissed: dismissModal, + onSubmit: removeDatasetsAndFetch } ) diff --git a/app/models/modals.ts b/app/models/modals.ts index 99359a7c..16a0bfd5 100644 --- a/app/models/modals.ts +++ b/app/models/modals.ts @@ -1,5 +1,3 @@ -import { AnyAction } from 'redux' - import { VersionInfo } from './store' export enum ModalType { @@ -55,7 +53,6 @@ export interface PublishDatasetModal { export interface RemoveDatasetModal { type: ModalType.RemoveDataset datasets: VersionInfo[] - onSubmit: (keepFiles: boolean) => Promise } export interface SearchModal { diff --git a/app/models/store.ts b/app/models/store.ts index 3d66df30..072d3281 100644 --- a/app/models/store.ts +++ b/app/models/store.ts @@ -82,6 +82,7 @@ export interface UI { importFileName: string importFileSize: number bootupComponent: BootupComponentType + bulkActionExecuting: boolean } export type BootupComponentType = diff --git a/app/reducers/ui.ts b/app/reducers/ui.ts index d3d83c67..d14240dc 100644 --- a/app/reducers/ui.ts +++ b/app/reducers/ui.ts @@ -23,6 +23,7 @@ export const UI_SET_DATASET_DIR_PATH = 'UI_SET_DATASET_DIR_PATH' export const UI_SET_EXPORT_PATH = 'UI_SET_EXPORT_PATH' export const UI_SET_DETAILS_BAR = 'UI_SET_DETAILS_BAR' export const UI_SET_BOOTUP_COMPONENT = 'UI_SET_BOOTUP_COMPONENT' +export const UI_BULK_ACTION_EXECUTING = 'UI_BULK_ACTION_EXECUTING' export const UNAUTHORIZED = 'UNAUTHORIZED' @@ -62,7 +63,8 @@ const initialState = { detailsBar: { type: DetailsType.NoDetails }, importFileName: '', importFileSize: 0, - bootupComponent: 'loading' + bootupComponent: 'loading', + bulkActionExecuting: false } // send an event to electron to block menus on first load @@ -203,6 +205,13 @@ export default (state = initialState, action: AnyAction) => { ...state, bootupComponent: component } + + case UI_BULK_ACTION_EXECUTING: + const { executing } = action + return { + ...state, + bulkActionExecuting: executing + } default: return state } diff --git a/app/selections.ts b/app/selections.ts index 4ae00a8b..e0755383 100644 --- a/app/selections.ts +++ b/app/selections.ts @@ -323,6 +323,10 @@ export function selectToast (state: Store): Toast { export function selectBootupComponent (state: Store): BootupComponentType { return state.ui.bootupComponent } + +export function selectBulkActionExecuting (state: Store): boolean { + return state.ui.bulkActionExecuting +} /** * * WORKINGDATASET STATE TREE