Add confirmation dialog

This commit is contained in:
Thomas Nordquist
2019-07-17 09:14:10 +02:00
parent 094831f037
commit 1cef529ac7
6 changed files with 147 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
import { ActionTypes } from '../reducers/Global' import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
export const showError = (error?: string) => ({ export const showError = (error?: string) => ({
@@ -20,3 +20,30 @@ export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>) => {
type: ActionTypes.toggleSettingsVisibility, type: ActionTypes.toggleSettingsVisibility,
}) })
} }
export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch<any>) => {
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<any>) => {
return new Promise((resolve, reject) => {
dispatch({
confirmationRequest,
type: ActionTypes.removeConfirmationRequest,
})
})
}

View File

@@ -3,11 +3,28 @@ import { AppState } from '../reducers'
import { Dispatch } from 'redux' import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events' import { makePublishEvent, rendererEvents } from '../../../events'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal' import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
import { globalActions } from '.'
export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => ( export const clearTopic = (topic: q.TreeNode<any>, recursive: boolean, subtopicClearLimit = 50) => async (
dispatch: Dispatch<any>, dispatch: Dispatch<any>,
getState: () => AppState 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')) dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection const { connectionId } = getState().connection

View File

@@ -1,12 +1,14 @@
import * as React from 'react' import ConfirmationDialog from './ConfirmationDialog'
import ConnectionSetup from './ConnectionSetup/ConnectionSetup' import ConnectionSetup from './ConnectionSetup/ConnectionSetup'
import CssBaseline from '@material-ui/core/CssBaseline' import CssBaseline from '@material-ui/core/CssBaseline'
import ErrorBoundary from './ErrorBoundary' import ErrorBoundary from './ErrorBoundary'
import Notification from './Layout/Notification' import Notification from './Layout/Notification'
import React from 'react'
import TitleBar from './Layout/TitleBar' import TitleBar from './Layout/TitleBar'
import UpdateNotifier from './UpdateNotifier' import UpdateNotifier from './UpdateNotifier'
import { AppState } from '../reducers' import { AppState } from '../reducers'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { ConfirmationRequest } from '../reducers/Global'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions' import { globalActions, settingsActions } from '../actions'
import { Theme, withStyles } from '@material-ui/core/styles' import { Theme, withStyles } from '@material-ui/core/styles'
@@ -23,6 +25,7 @@ interface Props {
actions: typeof globalActions actions: typeof globalActions
settingsActions: typeof settingsActions settingsActions: typeof settingsActions
launching: boolean launching: boolean
confirmationRequests: Array<ConfirmationRequest>
} }
class App extends React.PureComponent<Props, {}> { class App extends React.PureComponent<Props, {}> {
@@ -67,6 +70,7 @@ class App extends React.PureComponent<Props, {}> {
<div className={centerContent}> <div className={centerContent}>
<CssBaseline /> <CssBaseline />
<ErrorBoundary> <ErrorBoundary>
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()} {this.renderNotification()}
<React.Suspense fallback={<div></div>}> <React.Suspense fallback={<div></div>}>
<Settings /> <Settings />
@@ -149,6 +153,7 @@ const mapStateToProps = (state: AppState) => {
notification: state.globalState.get('notification'), notification: state.globalState.get('notification'),
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'), highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
launching: state.globalState.get('launching'), launching: state.globalState.get('launching'),
confirmationRequests: state.globalState.get('confirmationRequests'),
} }
} }

View File

@@ -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<ConfirmationRequest> }) {
const request = props.confirmationRequests[0]
const yesRef = useRef<HTMLButtonElement>()
const noRef = useRef<HTMLButtonElement>()
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 (
<Dialog
open={true}
onClose={reject}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={arrowKeyHandler}
>
<DialogTitle id="alert-dialog-title">{request.title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">{request.inquiry}</DialogContentText>
</DialogContent>
<DialogActions>
<Button ref={yesRef} variant="contained" onClick={confirm} color="primary" autoFocus>
Yes
</Button>
<Button ref={noRef} variant="contained" onClick={reject} color="secondary">
No
</Button>
</DialogActions>
</Dialog>
)
}
export default memo(ConfirmationDialog)

View File

@@ -8,11 +8,16 @@ export const RecursiveTopicDeleteButton = (props: {
node?: q.TreeNode<any> node?: q.TreeNode<any>
deleteTopicAction: (node: q.TreeNode<any>, a: boolean, limit: number) => void deleteTopicAction: (node: q.TreeNode<any>, a: boolean, limit: number) => void
}) => { }) => {
const onClick = useCallback(() => { const onClick = useCallback(
if (props.node) { (event: React.MouseEvent) => {
props.deleteTopicAction(props.node, true, deleteLimit) if (props.node) {
} event.stopPropagation()
}, [props.node]) event.preventDefault()
props.deleteTopicAction(props.node, true, deleteLimit)
}
},
[props.node]
)
if (!props.node) { if (!props.node) {
return null return null
} }

View File

@@ -9,6 +9,14 @@ export enum ActionTypes {
showNotification = 'SHOW_NOTIFICATION', showNotification = 'SHOW_NOTIFICATION',
didLaunch = 'DID_LAUNCH', didLaunch = 'DID_LAUNCH',
toggleSettingsVisibility = 'TOGGLE_SETTINGS_VISIBILITY', 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 { export interface GlobalAction extends Action {
@@ -17,6 +25,7 @@ export interface GlobalAction extends Action {
showUpdateDetails?: boolean showUpdateDetails?: boolean
error?: string error?: string
notification?: string notification?: string
confirmationRequest?: ConfirmationRequest
} }
interface GlobalStateInterface { interface GlobalStateInterface {
@@ -26,6 +35,7 @@ interface GlobalStateInterface {
notification?: string notification?: string
launching: boolean launching: boolean
settingsVisible: boolean settingsVisible: boolean
confirmationRequests: Array<ConfirmationRequest>
} }
export type GlobalState = Record<GlobalStateInterface> export type GlobalState = Record<GlobalStateInterface>
@@ -37,6 +47,7 @@ const initialStateFactory = Record<GlobalStateInterface>({
notification: undefined, notification: undefined,
launching: true, launching: true,
settingsVisible: false, settingsVisible: false,
confirmationRequests: [],
}) })
export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = ( export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> = (
@@ -67,6 +78,21 @@ export const globalState: Reducer<Record<GlobalStateInterface>, GlobalAction> =
} }
return state.set('showUpdateDetails', action.showUpdateDetails) 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: default:
return state return state
} }