diff --git a/app/src/actions/Global.ts b/app/src/actions/Global.ts index c575828..a1b3a67 100644 --- a/app/src/actions/Global.ts +++ b/app/src/actions/Global.ts @@ -1,4 +1,4 @@ -import { ActionTypes } from '../reducers/Global' +import { ActionTypes, ConfirmationRequest } from '../reducers/Global' import { Dispatch } from 'redux' export const showError = (error?: string) => ({ @@ -20,3 +20,30 @@ export const toggleSettingsVisibility = () => (dispatch: Dispatch) => { type: ActionTypes.toggleSettingsVisibility, }) } + +export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch) => { + return new Promise(resolve => { + const confirmationRequest = { + title, + inquiry, + callback: (confirmed: boolean) => { + resolve(confirmed) + dispatch(removeConfirmationRequest(confirmationRequest)) + }, + } + + dispatch({ + confirmationRequest, + type: ActionTypes.requestConfirmation, + }) + }) +} + +export const removeConfirmationRequest = (confirmationRequest: ConfirmationRequest) => (dispatch: Dispatch) => { + return new Promise((resolve, reject) => { + dispatch({ + confirmationRequest, + type: ActionTypes.removeConfirmationRequest, + }) + }) +} diff --git a/app/src/actions/clearTopic.ts b/app/src/actions/clearTopic.ts index afdcf20..5bc4233 100644 --- a/app/src/actions/clearTopic.ts +++ b/app/src/actions/clearTopic.ts @@ -3,11 +3,28 @@ import { AppState } from '../reducers' import { Dispatch } from 'redux' import { makePublishEvent, rendererEvents } from '../../../events' import { moveSelectionUpOrDownwards } from './visibleTreeTraversal' +import { globalActions } from '.' -export const clearTopic = (topic: q.TreeNode, recursive: boolean, subtopicClearLimit = 50) => ( +export const clearTopic = (topic: q.TreeNode, recursive: boolean, subtopicClearLimit = 50) => async ( dispatch: Dispatch, getState: () => AppState ) => { + if (recursive) { + const topicCount = topic.childTopicCount() + const deleteLimitMessage = + topicCount > subtopicClearLimit ? ` You can only delete ${subtopicClearLimit} child topics at once.` : '' + const childTopicsMessage = topicCount > 0 ? ` and ${topicCount} child ${topicCount === 1 ? 'topic' : 'topics'}` : '' + const confirmed = await dispatch( + globalActions.requestConfirmation( + 'Confirm delete', + `Do you want to delete "${topic.path()}"${childTopicsMessage}?${deleteLimitMessage}` + ) + ) + if (!confirmed) { + return + } + } + dispatch(moveSelectionUpOrDownwards('next')) const { connectionId } = getState().connection diff --git a/app/src/components/App.tsx b/app/src/components/App.tsx index e4c5931..2bb533b 100644 --- a/app/src/components/App.tsx +++ b/app/src/components/App.tsx @@ -1,12 +1,14 @@ -import * as React from 'react' +import ConfirmationDialog from './ConfirmationDialog' import ConnectionSetup from './ConnectionSetup/ConnectionSetup' import CssBaseline from '@material-ui/core/CssBaseline' import ErrorBoundary from './ErrorBoundary' import Notification from './Layout/Notification' +import React from 'react' import TitleBar from './Layout/TitleBar' import UpdateNotifier from './UpdateNotifier' import { AppState } from '../reducers' import { bindActionCreators } from 'redux' +import { ConfirmationRequest } from '../reducers/Global' import { connect } from 'react-redux' import { globalActions, settingsActions } from '../actions' import { Theme, withStyles } from '@material-ui/core/styles' @@ -23,6 +25,7 @@ interface Props { actions: typeof globalActions settingsActions: typeof settingsActions launching: boolean + confirmationRequests: Array } class App extends React.PureComponent { @@ -67,6 +70,7 @@ class App extends React.PureComponent {
+ {this.renderNotification()}
}> @@ -149,6 +153,7 @@ const mapStateToProps = (state: AppState) => { notification: state.globalState.get('notification'), highlightTopicUpdates: state.settings.get('highlightTopicUpdates'), launching: state.globalState.get('launching'), + confirmationRequests: state.globalState.get('confirmationRequests'), } } diff --git a/app/src/components/ConfirmationDialog.tsx b/app/src/components/ConfirmationDialog.tsx new file mode 100644 index 0000000..d1972a3 --- /dev/null +++ b/app/src/components/ConfirmationDialog.tsx @@ -0,0 +1,59 @@ +import React, { useRef, useCallback, memo } from 'react' +import { ConfirmationRequest } from '../reducers/Global' +import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core' +import { KeyCodes } from '../utils/KeyCodes' + +function ConfirmationDialog(props: { confirmationRequests: Array }) { + const request = props.confirmationRequests[0] + const yesRef = useRef() + const noRef = useRef() + const arrowKeyHandler = useCallback((event: KeyboardEvent) => { + const isArrowKey = event.keyCode === KeyCodes.arrow_left || event.keyCode === KeyCodes.arrow_right + if (!isArrowKey) { + return + } + + event.stopPropagation() + if (document.activeElement === noRef.current) { + yesRef.current && yesRef.current.focus() + } else { + noRef.current && noRef.current.focus() + } + }, []) + + const confirm = React.useCallback(() => { + request && request.callback(true) + }, [request]) + const reject = React.useCallback(() => { + request && request.callback(false) + }, [request]) + + if (!request) { + return null + } + + return ( + + {request.title} + + {request.inquiry} + + + + + + + ) +} + +export default memo(ConfirmationDialog) diff --git a/app/src/components/Sidebar/TopicPanel/RecursiveTopicDeleteButton.tsx b/app/src/components/Sidebar/TopicPanel/RecursiveTopicDeleteButton.tsx index dc3e130..2edcfb8 100644 --- a/app/src/components/Sidebar/TopicPanel/RecursiveTopicDeleteButton.tsx +++ b/app/src/components/Sidebar/TopicPanel/RecursiveTopicDeleteButton.tsx @@ -8,11 +8,16 @@ export const RecursiveTopicDeleteButton = (props: { node?: q.TreeNode deleteTopicAction: (node: q.TreeNode, a: boolean, limit: number) => void }) => { - const onClick = useCallback(() => { - if (props.node) { - props.deleteTopicAction(props.node, true, deleteLimit) - } - }, [props.node]) + const onClick = useCallback( + (event: React.MouseEvent) => { + if (props.node) { + event.stopPropagation() + event.preventDefault() + props.deleteTopicAction(props.node, true, deleteLimit) + } + }, + [props.node] + ) if (!props.node) { return null } diff --git a/app/src/reducers/Global.ts b/app/src/reducers/Global.ts index c61808b..079b07b 100644 --- a/app/src/reducers/Global.ts +++ b/app/src/reducers/Global.ts @@ -9,6 +9,14 @@ export enum ActionTypes { showNotification = 'SHOW_NOTIFICATION', didLaunch = 'DID_LAUNCH', toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', + requestConfirmation = 'REQUEST_CONFIRMATION', + removeConfirmationRequest = 'REMOVE_CONFIRMATION_REQUEST', +} + +export interface ConfirmationRequest { + inquiry: string + title: string + callback: (confirmed: boolean) => void } export interface GlobalAction extends Action { @@ -17,6 +25,7 @@ export interface GlobalAction extends Action { showUpdateDetails?: boolean error?: string notification?: string + confirmationRequest?: ConfirmationRequest } interface GlobalStateInterface { @@ -26,6 +35,7 @@ interface GlobalStateInterface { notification?: string launching: boolean settingsVisible: boolean + confirmationRequests: Array } export type GlobalState = Record @@ -37,6 +47,7 @@ const initialStateFactory = Record({ notification: undefined, launching: true, settingsVisible: false, + confirmationRequests: [], }) export const globalState: Reducer, GlobalAction> = ( @@ -67,6 +78,21 @@ export const globalState: Reducer, GlobalAction> = } return state.set('showUpdateDetails', action.showUpdateDetails) + case ActionTypes.requestConfirmation: + if (action.confirmationRequest === undefined) { + return state + } + return state.set('confirmationRequests', [...state.get('confirmationRequests'), action.confirmationRequest]) + + case ActionTypes.removeConfirmationRequest: + if (action.confirmationRequest === undefined) { + return state + } + return state.set( + 'confirmationRequests', + state.get('confirmationRequests').filter(a => a !== action.confirmationRequest) + ) + default: return state }