Skip to content

Commit 4bfadc0

Browse files
authored
Abstract confirm-delete into more general confirm-action (#1927)
* abstract confirm-delete into more general confirm-action * fix double error toast on instance delete
1 parent 695d367 commit 4bfadc0

File tree

7 files changed

+73
-50
lines changed

7 files changed

+73
-50
lines changed
Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,45 @@
88
import { useState } from 'react'
99

1010
import { type ApiError } from '@oxide/api'
11-
import { Message, Modal } from '@oxide/ui'
12-
import { classed } from '@oxide/util'
11+
import { Modal } from '@oxide/ui'
1312

14-
import { clearConfirmDelete, useConfirmDelete } from 'app/stores/confirm-delete'
13+
import { clearConfirmAction, useConfirmAction } from 'app/stores/confirm-action'
1514
import { addToast } from 'app/stores/toast'
1615

17-
export const HL = classed.span`text-sans-semi-md text-default`
18-
19-
export function ConfirmDeleteModal() {
20-
const deleteConfig = useConfirmDelete((state) => state.deleteConfig)
16+
export function ConfirmActionModal() {
17+
const actionConfig = useConfirmAction((state) => state.actionConfig)
2118

2219
// this is a bit sad -- ideally we would be able to use the loading state
2320
// from the mutation directly, but that would require a lot of line changes
2421
// and would require us to hook this up in a way that re-renders whenever the
2522
// loading state changes
2623
const [loading, setLoading] = useState(false)
2724

28-
if (!deleteConfig) return null
29-
30-
const { doDelete, warning, label } = deleteConfig
25+
if (!actionConfig) return null
3126

32-
const displayLabel = typeof label === 'string' ? <HL>{label}</HL> : label
27+
const { doAction, modalContent, errorTitle, modalTitle } = actionConfig
3328

3429
return (
35-
<Modal isOpen onDismiss={clearConfirmDelete} title="Confirm delete">
36-
<Modal.Section>
37-
<p>Are you sure you want to delete {displayLabel}?</p>
38-
{warning && <Message variant="error" content={warning} />}
39-
</Modal.Section>
30+
<Modal isOpen onDismiss={clearConfirmAction} title={modalTitle}>
31+
<Modal.Section>{modalContent}</Modal.Section>
4032
<Modal.Footer
41-
onDismiss={clearConfirmDelete}
33+
onDismiss={clearConfirmAction}
4234
onAction={async () => {
4335
setLoading(true)
4436
try {
45-
await doDelete()
37+
await doAction()
4638
} catch (error) {
4739
addToast({
4840
variant: 'error',
49-
title: 'Could not delete resource',
41+
title: errorTitle,
5042
content: (error as ApiError).message,
5143
})
5244
}
5345

5446
setLoading(false) // do this regardless of success or error
5547

5648
// TODO: generic success toast?
57-
clearConfirmDelete()
49+
clearConfirmAction()
5850
}}
5951
cancelText="Cancel"
6052
actionText="Confirm"

app/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
1414
import { queryClient } from '@oxide/api'
1515
import { SkipLink } from '@oxide/ui'
1616

17-
import { ConfirmDeleteModal } from './components/ConfirmDeleteModal'
17+
import { ConfirmActionModal } from './components/ConfirmActionModal'
1818
import { ErrorBoundary } from './components/ErrorBoundary'
1919
import { ReduceMotion } from './hooks'
2020
// stripped out by rollup in production
@@ -45,7 +45,7 @@ function render() {
4545
<StrictMode>
4646
<QueryClientProvider client={queryClient}>
4747
<ErrorBoundary>
48-
<ConfirmDeleteModal />
48+
<ConfirmActionModal />
4949
<SkipLink id="skip-nav" />
5050
<ReduceMotion />
5151
<RouterProvider router={router} />

app/pages/SiloAccessPage.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@ import {
3232
import { groupBy, isTruthy } from '@oxide/util'
3333

3434
import { AccessNameCell } from 'app/components/AccessNameCell'
35-
import { HL } from 'app/components/ConfirmDeleteModal'
3635
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
3736
import {
3837
SiloAccessAddUserSideModal,
3938
SiloAccessEditUserSideModal,
4039
} from 'app/forms/silo-access'
41-
import { confirmDelete } from 'app/stores/confirm-delete'
40+
import { confirmDelete, HL } from 'app/stores/confirm-delete'
4241

4342
const EmptyState = ({ onClick }: { onClick: () => void }) => (
4443
<TableEmptyBox>

app/pages/project/access/ProjectAccessPage.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ import {
3535
import { groupBy, isTruthy } from '@oxide/util'
3636

3737
import { AccessNameCell } from 'app/components/AccessNameCell'
38-
import { HL } from 'app/components/ConfirmDeleteModal'
3938
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
4039
import {
4140
ProjectAccessAddUserSideModal,
4241
ProjectAccessEditUserSideModal,
4342
} from 'app/forms/project-access'
4443
import { getProjectSelector, useProjectSelector } from 'app/hooks'
45-
import { confirmDelete } from 'app/stores/confirm-delete'
44+
import { confirmDelete, HL } from 'app/stores/confirm-delete'
4645

4746
const EmptyState = ({ onClick }: { onClick: () => void }) => (
4847
<TableEmptyBox>

app/pages/project/instances/actions.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,6 @@ export const useMakeInstanceActions = (
111111
options.onDelete?.()
112112
addToast({ title: `Deleting instance '${instance.name}'` })
113113
},
114-
onError: (error) =>
115-
addToast({
116-
variant: 'error',
117-
title: `Error deleting instance '${instance.name}'`,
118-
content: error.message,
119-
}),
120114
}),
121115
label: instance.name,
122116
}),
Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,22 @@
88
import type { ReactNode } from 'react'
99
import { create } from 'zustand'
1010

11-
type DeleteConfig = {
11+
type ActionConfig = {
1212
/** Must be `mutateAsync`, otherwise we can't catch the error generically */
13-
doDelete: () => Promise<unknown>
14-
warning?: string
15-
/**
16-
* Label identifying the resource. Could be a name or something more elaborate
17-
* "the Admin role for user Harry Styles". If a string, the modal will
18-
* automatically give it a highlighted style. Otherwise it will be rendered
19-
* directly.
20-
*/
21-
label: ReactNode
13+
doAction: () => Promise<unknown>
14+
/** e.g., Confirm delete, Confirm unlink */
15+
modalTitle: string
16+
modalContent: ReactNode
17+
/** Title of error toast */
18+
errorTitle: string
2219
}
2320

24-
type ConfirmDeleteStore = {
25-
deleteConfig: DeleteConfig | null
21+
type ConfirmActionStore = {
22+
actionConfig: ActionConfig | null
2623
}
2724

28-
export const useConfirmDelete = create<ConfirmDeleteStore>(() => ({
29-
deleteConfig: null,
25+
export const useConfirmAction = create<ConfirmActionStore>(() => ({
26+
actionConfig: null,
3027
}))
3128

3229
// zustand docs say this pattern is equivalent to putting the actions on the
@@ -39,10 +36,10 @@ export const useConfirmDelete = create<ConfirmDeleteStore>(() => ({
3936
/**
4037
* Note that this returns a function so we can save a line in the calling code.
4138
*/
42-
export const confirmDelete = (deleteConfig: DeleteConfig) => () => {
43-
useConfirmDelete.setState({ deleteConfig })
39+
export const confirmAction = (actionConfig: ActionConfig) => () => {
40+
useConfirmAction.setState({ actionConfig })
4441
}
4542

46-
export function clearConfirmDelete() {
47-
useConfirmDelete.setState({ deleteConfig: null })
43+
export function clearConfirmAction() {
44+
useConfirmAction.setState({ actionConfig: null })
4845
}

app/stores/confirm-delete.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import { classed } from '@oxide/util'
10+
11+
import { useConfirmAction } from './confirm-action'
12+
13+
// confirmAction was originally abstracted from confirmDelete. this preserves
14+
// the existing confirmDelete API by constructing a confirmAction from it
15+
16+
type DeleteConfig = {
17+
/** Must be `mutateAsync`, otherwise we can't catch the error generically */
18+
doDelete: () => Promise<unknown>
19+
/**
20+
* Label identifying the resource. Could be a name or something more elaborate
21+
* "the Admin role for user Harry Styles". If a string, the modal will
22+
* automatically give it a highlighted style. Otherwise it will be rendered
23+
* directly.
24+
*/
25+
label: React.ReactNode
26+
}
27+
28+
export const HL = classed.span`text-sans-semi-md text-default`
29+
30+
export const confirmDelete =
31+
({ doDelete, label }: DeleteConfig) =>
32+
() => {
33+
const displayLabel = typeof label === 'string' ? <HL>{label}</HL> : label
34+
useConfirmAction.setState({
35+
actionConfig: {
36+
doAction: doDelete,
37+
modalContent: <p>Are you sure you want to delete {displayLabel}?</p>,
38+
errorTitle: 'Could not delete resource',
39+
modalTitle: 'Confirm delete',
40+
},
41+
})
42+
}

0 commit comments

Comments
 (0)