UNPKG

chonky

Version:

A File Browser component for React

146 lines (132 loc) 5.85 kB
import { MaybePromise, Undefinable } from 'tsdef'; import { FileActionData, FileActionState } from '../../types/action-handler.types'; import { FileAction } from '../../types/action.types'; import { ChonkyDispatch, ChonkyThunk } from '../../types/redux.types'; import { Logger } from '../../util/logger'; import { reduxActions } from '../reducers'; import { selectContextMenuTriggerFile, selectExternalFileActionHandler, selectFileActionMap, selectInstanceId, selectSelectedFiles } from '../selectors'; import { thunkActivateSortAction, thunkApplySelectionTransform } from './file-actions.thunks'; /** * Thunk that dispatches actions to the external (user-provided) action handler. */ export const thunkDispatchFileAction = (data: FileActionData<FileAction>): ChonkyThunk => (_dispatch, getState) => { Logger.debug(`FILE ACTION DISPATCH: [${data.id}]`, 'data:', data); const state = getState(); const action = selectFileActionMap(state)[data.id]; const externalFileActionHandler = selectExternalFileActionHandler(state); if (action) { if (externalFileActionHandler) { Promise.resolve(externalFileActionHandler(data)).catch(error => Logger.error(`User-defined file action handler threw an error: ${error.message}`) ); } } else { Logger.warn( `Internal components dispatched the "${data.id}" file action, but such ` + `action was not registered.` ); } }; /** * Thunk that is used by internal components (and potentially the user) to "request" * actions. When action is requested, Chonky "prepares" the action data by extracting it * from Redux state. Once action data is ready, Chonky executes some side effect and/or * dispatches the action to the external action handler. */ export const thunkRequestFileAction = <Action extends FileAction>( action: Action, payload: Action['__payloadType'] ): ChonkyThunk => (dispatch, getState) => { Logger.debug(`FILE ACTION REQUEST: [${action.id}]`, 'action:', action, 'payload:', payload); const state = getState(); const instanceId = selectInstanceId(state); if (!selectFileActionMap(state)[action.id]) { Logger.warn( `The action "${action.id}" was requested, but it is not registered. The ` + `action will still be dispatched, but this might indicate a bug in ` + `the code. Please register your actions by passing them to ` + `"fileActions" prop.` ); } // Determine files for the action if action requires selection const selectedFiles = selectSelectedFiles(state); const selectedFilesForAction = action.fileFilter ? selectedFiles.filter(action.fileFilter) : selectedFiles; if (action.requiresSelection && selectedFilesForAction.length === 0) { Logger.warn( `Internal components requested the "${action.id}" file ` + `action, but the selection for this action was empty. This ` + `might a bug in the code of the presentational components.` ); return; } const contextMenuTriggerFile = selectContextMenuTriggerFile(state); const actionState: FileActionState<{}> = { instanceId, selectedFiles, selectedFilesForAction, contextMenuTriggerFile, }; // === Update sort state if necessary const sortKeySelector = action.sortKeySelector; if (sortKeySelector) dispatch(thunkActivateSortAction(action.id)); // === Update file view state if necessary const fileViewConfig = action.fileViewConfig; if (fileViewConfig) dispatch(reduxActions.setFileViewConfig(fileViewConfig)); // === Update option state if necessary const option = action.option; if (option) dispatch(reduxActions.toggleOption(option.id)); // === Apply selection transform if necessary const selectionTransform = action.selectionTransform; if (selectionTransform) dispatch(thunkApplySelectionTransform(action)); // Apply the effect const effect = action.effect; let maybeEffectPromise: MaybePromise<boolean | undefined> = undefined; if (effect) { try { maybeEffectPromise = effect({ action, payload, state: actionState, reduxDispatch: dispatch, getReduxState: getState, }) as MaybePromise<boolean | undefined>; } catch (error) { Logger.error(`User-defined effect function for action ${action.id} threw an ` + `error: ${error.message}`); } } // Dispatch the action to user code. Deliberately call it after all other // operations are over. return Promise.resolve(maybeEffectPromise) .then(effectResult => { const data: FileActionData<Action> = { id: action.id, action, payload, state: actionState, }; triggerDispatchAfterEffect(dispatch, data, effectResult); }) .catch(error => { Logger.error( `User-defined effect function for action ${action.id} returned a ` + `promise that was rejected: ${error.message}` ); const data: FileActionData<Action> = { id: action.id, action, payload, state: actionState, }; triggerDispatchAfterEffect(dispatch, data, undefined); }); }; export const triggerDispatchAfterEffect = <Action extends FileAction>( dispatch: ChonkyDispatch, data: FileActionData<Action>, effectResult: Undefinable<boolean> ) => { const preventDispatch = effectResult === true; if (!preventDispatch) dispatch(thunkDispatchFileAction(data)); };