Skip to content

Commit

Permalink
Restore draft cancellation confirmation
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Feb 12, 2024
1 parent c708300 commit ef344e2
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 55 deletions.
55 changes: 29 additions & 26 deletions frontend/src/domain/shared_slices/SideWindow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'

import { FrontendError } from '../../libs/FrontendError'
import { SideWindowMenuKey, SideWindowStatus } from '../entities/sideWindow/constants'
import { getFullPathFromPath } from '../entities/sideWindow/utils'

Expand All @@ -26,12 +25,11 @@ const sideWindowSlice = createSlice({
name: 'sideWindow',
reducers: {
/**
* Show confirmation dialog when a draft is both in progress and dirty before going to menu + submenu
* Cancel the draft cancellation confirmation dialog.
*/
askForDraftCancellationConfirmationBeforeGoingTo(state, action: PayloadAction<SideWindow.FullPath>) {
state.isDraftCancellationConfirmationDialogOpen = true
state.nextPath = action.payload
state.status = SideWindowStatus.FOCUSED
cancelDraftCancellationConfirmationDialog(state) {
state.isDraftCancellationConfirmationDialogOpen = false
state.nextPath = undefined
},

/**
Expand All @@ -42,39 +40,44 @@ const sideWindowSlice = createSlice({
},

/**
* Toggle side window confirmation modal when a draft is both in progress and dirty
* Open the draft cancellation confirmation dialog.
*
* @description
* ⚠️ NEVER use this action directly, use `askForSideWindowDraftCancellationConfirmation()` use case instead.
*/
closeDraftCancellationConfirmationDialog(state) {
state.isDraftCancellationConfirmationDialogOpen = false
// We reset this prop that was set by `askForDraftCancellationConfirmationBeforeGoingToPath()`
state.nextPath = undefined
openDraftCancellationConfirmationDialog(state) {
state.isDraftCancellationConfirmationDialogOpen = true
state.status = SideWindowStatus.FOCUSED
},

/**
* Confirm cancellation of a draft that is both in progress and dirty
* Open side window and go to menu + submenu
*
* @description
* ⚠️ NEVER use this action directly, use `openSideWindowPath()` use case instead.
*/
hideDraftCancellationConfirmationDialogAndGoToNextPath(state) {
if (!state.nextPath) {
throw new FrontendError('`state.nextPath` is undefined.')
}

state.isDraftCancellationConfirmationDialogOpen = false
state.selectedPath = state.nextPath
state.status = SideWindowStatus.FOCUSED

openOrFocusAndGoTo(state, action: PayloadAction<SideWindow.FullPath>) {
state.nextPath = undefined
state.selectedPath = action.payload
state.status = SideWindowStatus.FOCUSED
},

/**
* Open side window and go to menu + submenu
* Set next path.
*
* @description
* ⚠️ You should only use this action when you are willingly cancelling or saving a current draft in progress.
* In all other cases, you should use `openPath()`.
* Used to store the next path while waiting for user confirmation before changing path.
*/
openOrFocusAndGoTo(state, action: PayloadAction<SideWindow.FullPath>) {
setNextPath(state, action: PayloadAction<SideWindow.FullPath>) {
state.nextPath = action.payload
},

/**
* Set current path.
*/
setSelectedPath(state, action: PayloadAction<SideWindow.FullPath>) {
state.nextPath = undefined
state.selectedPath = action.payload
state.status = SideWindowStatus.FOCUSED
},

/**
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/domain/use_cases/error/displayOrLogError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DisplayedError } from '../../../libs/DisplayedError'
import { INITIAL_STATE, type DisplayedErrorState, displayedErrorActions } from '../../shared_slices/DisplayedError'
import { setError } from '../../shared_slices/Global'

import type { RetryableUseCase } from '../../../libs/DisplayedError'
import type { RetryableUseCase } from '../../../types'

/**
* Dispatch:
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/domain/use_cases/error/retry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { displayedErrorActions, type DisplayedErrorState } from '../../shared_slices/DisplayedError'

import type { RetryableUseCase } from '../../../libs/DisplayedError'
import type { MainAppThunk } from '../../../store'
import type { RetryableUseCase } from '../../../types'

export const retry =
(errorKey: keyof DisplayedErrorState, retryableUseCase: RetryableUseCase | undefined): MainAppThunk =>
Expand Down
25 changes: 19 additions & 6 deletions frontend/src/features/Mission/useCases/addMission.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SideWindowMenuKey } from '../../../domain/entities/sideWindow/constants'
import { displayedErrorActions } from '../../../domain/shared_slices/DisplayedError'
import { askForSideWindowDraftCancellationConfirmation } from '../../SideWindow/useCases/askForSideWindowDraftCancellationConfirmation'
import { openSideWindowPath } from '../../SideWindow/useCases/openSideWindowPath'
import { missionFormActions } from '../components/MissionForm/slice'
import { getMissionDraftFromPartialMainFormValues } from '../components/MissionForm/utils/getMissionDraftFromPartialMainFormValues'
Expand All @@ -8,16 +9,28 @@ import type { MainAppThunk } from '../../../store'
import type { MissionMainFormValues } from '../../SideWindow/MissionForm/types'

export const addMission =
(initialMainFormValues: Partial<MissionMainFormValues> = {}): MainAppThunk =>
(dispatch, getState) => {
const { missionForm } = getState()
const path = { id: 'new', menu: SideWindowMenuKey.MISSION_FORM }

if (missionForm.isDraftDirty) {
dispatch(
askForSideWindowDraftCancellationConfirmation(path, () => addMissionWithoutConfirmation(initialMainFormValues))
)

return
}

dispatch(addMissionWithoutConfirmation(initialMainFormValues))
dispatch(openSideWindowPath(path, true))
}

const addMissionWithoutConfirmation =
(initialMainFormValues: Partial<MissionMainFormValues> = {}): MainAppThunk =>
dispatch => {
const newDraft = getMissionDraftFromPartialMainFormValues(initialMainFormValues)

dispatch(displayedErrorActions.unset('missionFormError'))
dispatch(missionFormActions.initializeDraft(newDraft))
dispatch(
openSideWindowPath({
id: 'new',
menu: SideWindowMenuKey.MISSION_FORM
})
)
}
26 changes: 18 additions & 8 deletions frontend/src/features/Mission/useCases/editMission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,33 @@ import { sideWindowActions } from '../../../domain/shared_slices/SideWindow'
import { displayOrLogError } from '../../../domain/use_cases/error/displayOrLogError'
import { FrontendApiError } from '../../../libs/FrontendApiError'
import { handleThunkError } from '../../../utils/handleThunkError'
import { askForSideWindowDraftCancellationConfirmation } from '../../SideWindow/useCases/askForSideWindowDraftCancellationConfirmation'
import { openSideWindowPath } from '../../SideWindow/useCases/openSideWindowPath'
import { missionFormActions } from '../components/MissionForm/slice'
import { getMissionDraftFromMissionWithActions } from '../components/MissionForm/utils/getMissionFormInitialValues'

import type { MainAppThunk } from '../../../store'

export const editMission =
(id: number): MainAppThunk =>
async (dispatch, getState) => {
const { missionForm } = getState()
const path = { id, isLoading: true, menu: SideWindowMenuKey.MISSION_FORM }

if (missionForm.isDraftDirty) {
dispatch(askForSideWindowDraftCancellationConfirmation(path, () => editMissionWithoutConfirmation(id)))

return
}

dispatch(editMissionWithoutConfirmation(id))
dispatch(openSideWindowPath(path, true))
}

export const editMissionWithoutConfirmation =
(id: number): MainAppThunk =>
async dispatch => {
dispatch(displayedErrorActions.unset('missionFormError'))
dispatch(
openSideWindowPath({
id,
isLoading: true,
menu: SideWindowMenuKey.MISSION_FORM
})
)

try {
const missionWithActions = await dispatch(getMissionWithActions(id))
Expand All @@ -32,7 +42,7 @@ export const editMission =
dispatch(sideWindowActions.setSelectedPathIsLoading(false))
} catch (err) {
if (err instanceof FrontendApiError) {
dispatch(displayOrLogError(err, () => editMission(id), true, 'missionFormError'))
dispatch(displayOrLogError(err, () => editMissionWithoutConfirmation(id), true, 'missionFormError'))

return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from 'styled-components'

import { sideWindowActions } from '../../../../domain/shared_slices/SideWindow'
import { useMainAppDispatch } from '../../../../hooks/useMainAppDispatch'
import { confirmSideWindowDraftCancellationAndProceed } from '../../useCases/confirmSideWindowDraftCancellationAndProceed'

type DraftCancellationConfirmationDialogProps = {
isAutoSaveEnabled: boolean
Expand All @@ -11,11 +12,11 @@ export function DraftCancellationConfirmationDialog({ isAutoSaveEnabled }: Draft
const dispatch = useMainAppDispatch()

const cancel = () => {
dispatch(sideWindowActions.closeDraftCancellationConfirmationDialog())
dispatch(sideWindowActions.cancelDraftCancellationConfirmationDialog())
}

const confirm = () => {
dispatch(sideWindowActions.hideDraftCancellationConfirmationDialogAndGoToNextPath())
dispatch(confirmSideWindowDraftCancellationAndProceed())
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getFullPathFromPath } from '../../../domain/entities/sideWindow/utils'
import { sideWindowActions } from '../../../domain/shared_slices/SideWindow'
import { PendingUseCaseKey, setPendingUseCase } from '../../../libs/PendingUseCase'

import type { SideWindow } from '../../../domain/entities/sideWindow/types'
import type { MainAppThunk } from '../../../store'
import type { RetryableUseCase } from '../../../types'

export const askForSideWindowDraftCancellationConfirmation =
(nextPath: SideWindow.Path, useCase?: RetryableUseCase): MainAppThunk =>
dispatch => {
if (useCase) {
setPendingUseCase(PendingUseCaseKey.DRAFT_CANCELLATION_CONFIRMATION, useCase)
}

const nextFullPath = getFullPathFromPath(nextPath)

dispatch(sideWindowActions.setNextPath(nextFullPath))
dispatch(sideWindowActions.openDraftCancellationConfirmationDialog())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { sideWindowActions } from '../../../domain/shared_slices/SideWindow'
import { getPendingUseCase, PendingUseCaseKey } from '../../../libs/PendingUseCase'
import { assert } from '../../../utils/assert'

import type { MainAppThunk } from '../../../store'

export const confirmSideWindowDraftCancellationAndProceed = (): MainAppThunk => (dispatch, getState) => {
const { sideWindow } = getState()
assert(sideWindow.nextPath, 'sideWindow.nextPath')
const pendingUseCase = getPendingUseCase(PendingUseCaseKey.DRAFT_CANCELLATION_CONFIRMATION)

if (pendingUseCase) {
dispatch(pendingUseCase())
}
dispatch(sideWindowActions.cancelDraftCancellationConfirmationDialog())
dispatch(sideWindowActions.openOrFocusAndGoTo(sideWindow.nextPath))
}
17 changes: 9 additions & 8 deletions frontend/src/features/SideWindow/useCases/openSideWindowPath.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
// import { SideWindowStatus } from '../../../domain/entities/sideWindow/constants'
import { askForSideWindowDraftCancellationConfirmation } from './askForSideWindowDraftCancellationConfirmation'
import { SideWindowStatus } from '../../../domain/entities/sideWindow/constants'
import { getFullPathFromPath } from '../../../domain/entities/sideWindow/utils'
import { sideWindowActions } from '../../../domain/shared_slices/SideWindow'

import type { SideWindow } from '../../../domain/entities/sideWindow/types'
import type { MainAppThunk } from '../../../store'

export const openSideWindowPath =
(path: SideWindow.Path): MainAppThunk =>
async dispatch => {
// const { missionForm, sideWindow } = getState()
(path: SideWindow.Path, withoutConfirmation: boolean = false): MainAppThunk =>
async (dispatch, getState) => {
const { missionForm, sideWindow } = getState()

const fullPath: SideWindow.FullPath = getFullPathFromPath(path)

// if (sideWindow.status !== SideWindowStatus.CLOSED && missionForm.isDraftDirty) {
// dispatch(sideWindowActions.askForDraftCancellationConfirmationBeforeGoingTo(fullPath))
if (!withoutConfirmation && sideWindow.status !== SideWindowStatus.CLOSED && missionForm.isDraftDirty) {
dispatch(askForSideWindowDraftCancellationConfirmation(path))

// return
// }
return
}

dispatch(sideWindowActions.openOrFocusAndGoTo(fullPath))
}
4 changes: 1 addition & 3 deletions frontend/src/libs/DisplayedError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import { captureException, captureMessage } from '@sentry/react'

import type { MainAppThunk } from '../store'

export type RetryableUseCase = () => MainAppThunk
import type { RetryableUseCase } from '../types'

export class DisplayedError extends Error {
originalError: any | undefined
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/libs/PendingUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { assert } from '../utils/assert'

import type { RetryableUseCase } from '../types'

export enum PendingUseCaseKey {
DRAFT_CANCELLATION_CONFIRMATION = 'DRAFT_CANCELLATION_CONFIRMATION',
RETRYABLE_ERROR = 'RETRYABLE_ERROR'
}

const PENDING_USE_CASE_STORE: Record<PendingUseCaseKey, RetryableUseCase | undefined> = {
DRAFT_CANCELLATION_CONFIRMATION: undefined,
RETRYABLE_ERROR: undefined
}

export function getPendingUseCase(key: PendingUseCaseKey): RetryableUseCase {
const pendingUseCase = PENDING_USE_CASE_STORE[key]
assert(pendingUseCase, 'pendingUseCase')

unsetPendingUseCase(key)

return pendingUseCase
}

export function setPendingUseCase(key: PendingUseCaseKey, useCase: RetryableUseCase) {
PENDING_USE_CASE_STORE[key] = useCase
}

export function unsetPendingUseCase(key: PendingUseCaseKey) {
PENDING_USE_CASE_STORE[key] = undefined
}
3 changes: 3 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MainAppThunk } from './store'
import type { ConditionalKeys, Exact } from 'type-fest'

export type CollectionItem = {
Expand Down Expand Up @@ -42,4 +43,6 @@ export type PickStringKeysWithNativeValues<T extends Record<any, any>> = Exact<
T
>

export type RetryableUseCase = () => MainAppThunk

export type StringKeyRecord<T> = PickStringKeys<Record<string, T>>

0 comments on commit ef344e2

Please sign in to comment.