Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0899548

Browse files
authored
feat: have user type name of thing to delete for extra safety (#4080)
* Add info and text field to delete dialog * Format * Use DeleteDialog for Users, nix info except for Workspaces * Format * Update storybook * Add and update tests * Fix the worst of the UsersPage test bugs * Fix users page tests * Fix workspace tests * Format
1 parent eb71053 commit 0899548

File tree

14 files changed

+243
-140
lines changed

14 files changed

+243
-140
lines changed

site/src/components/Dialogs/ConfirmDialog/ConfirmDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
8383
confirmLoading,
8484
confirmText,
8585
description,
86+
disabled = false,
8687
hideCancel,
8788
onClose,
8889
onConfirm,
@@ -122,6 +123,7 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
122123
confirmDialog
123124
confirmLoading={confirmLoading}
124125
confirmText={confirmText || defaults.confirmText}
126+
disabled={disabled}
125127
onCancel={!hideCancel ? onClose : undefined}
126128
onConfirm={onConfirm || onClose}
127129
type={type}

site/src/components/Dialogs/DeleteDialog/DeleteDialog.stories.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ export default {
1515
control: "boolean",
1616
defaultValue: true,
1717
},
18-
title: {
19-
defaultValue: "Delete Something",
18+
entity: {
19+
defaultValue: "foo",
2020
},
21-
description: {
22-
defaultValue:
23-
"This is irreversible. To confirm, type the name of the thing you want to delete.",
21+
name: {
22+
defaultValue: "MyFoo",
23+
},
24+
info: {
25+
defaultValue: "Here's some info about the foo so you know you're deleting the right one.",
2426
},
2527
},
2628
} as ComponentMeta<typeof DeleteDialog>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { screen } from "@testing-library/react"
2+
import userEvent from "@testing-library/user-event"
3+
import i18next from "i18next"
4+
import { render } from "testHelpers/renderHelpers"
5+
import { DeleteDialog } from "./DeleteDialog"
6+
7+
describe("DeleteDialog", () => {
8+
it("disables confirm button when the text field is empty", () => {
9+
render(
10+
<DeleteDialog
11+
isOpen
12+
onConfirm={jest.fn()}
13+
onCancel={jest.fn()}
14+
entity="template"
15+
name="MyTemplate"
16+
/>,
17+
)
18+
const confirmButton = screen.getByRole("button", { name: "Delete" })
19+
expect(confirmButton).toBeDisabled()
20+
})
21+
22+
it("disables confirm button when the text field is filled incorrectly", async () => {
23+
const { t } = i18next
24+
render(
25+
<DeleteDialog
26+
isOpen
27+
onConfirm={jest.fn()}
28+
onCancel={jest.fn()}
29+
entity="template"
30+
name="MyTemplate"
31+
/>,
32+
)
33+
const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" })
34+
const textField = screen.getByLabelText(labelText)
35+
await userEvent.type(textField, "MyTemplateWrong")
36+
const confirmButton = screen.getByRole("button", { name: "Delete" })
37+
expect(confirmButton).toBeDisabled()
38+
})
39+
40+
it("enables confirm button when the text field is filled correctly", async () => {
41+
const { t } = i18next
42+
render(
43+
<DeleteDialog
44+
isOpen
45+
onConfirm={jest.fn()}
46+
onCancel={jest.fn()}
47+
entity="template"
48+
name="MyTemplate"
49+
/>,
50+
)
51+
const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" })
52+
const textField = screen.getByLabelText(labelText)
53+
await userEvent.type(textField, "MyTemplate")
54+
const confirmButton = screen.getByRole("button", { name: "Delete" })
55+
expect(confirmButton).not.toBeDisabled()
56+
})
57+
})
Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,80 @@
1-
import React, { ReactNode } from "react"
1+
import FormHelperText from "@material-ui/core/FormHelperText"
2+
import makeStyles from "@material-ui/core/styles/makeStyles"
3+
import TextField from "@material-ui/core/TextField"
4+
import Typography from "@material-ui/core/Typography"
5+
import { Maybe } from "components/Conditionals/Maybe"
6+
import { Stack } from "components/Stack/Stack"
7+
import React, { ChangeEvent, useState } from "react"
8+
import { useTranslation } from "react-i18next"
29
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog"
310

411
export interface DeleteDialogProps {
512
isOpen: boolean
613
onConfirm: () => void
714
onCancel: () => void
8-
title: string
9-
description: string | ReactNode
15+
entity: string
16+
name: string
17+
info?: string
1018
confirmLoading?: boolean
1119
}
1220

1321
export const DeleteDialog: React.FC<React.PropsWithChildren<DeleteDialogProps>> = ({
1422
isOpen,
1523
onCancel,
1624
onConfirm,
17-
title,
18-
description,
25+
entity,
26+
info,
27+
name,
1928
confirmLoading,
20-
}) => (
21-
<ConfirmDialog
22-
type="delete"
23-
hideCancel={false}
24-
open={isOpen}
25-
title={title}
26-
onConfirm={onConfirm}
27-
onClose={onCancel}
28-
description={description}
29-
confirmLoading={confirmLoading}
30-
/>
31-
)
29+
}) => {
30+
const styles = useStyles()
31+
const { t } = useTranslation("common")
32+
const [nameValue, setNameValue] = useState("")
33+
const confirmed = name === nameValue
34+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
35+
setNameValue(event.target.value)
36+
}
37+
38+
const content = (
39+
<>
40+
<Typography>{t("deleteDialog.intro", { entity })}</Typography>
41+
<Maybe condition={info !== undefined}>
42+
<Typography className={styles.warning}>{info}</Typography>
43+
</Maybe>
44+
<Typography>{t("deleteDialog.confirm", { entity })}</Typography>
45+
<Stack spacing={1}>
46+
<TextField
47+
name="confirmation"
48+
id="confirmation"
49+
placeholder={name}
50+
value={nameValue}
51+
onChange={handleChange}
52+
label={t("deleteDialog.confirmLabel", { entity })}
53+
/>
54+
<Maybe condition={nameValue.length > 0 && !confirmed}>
55+
<FormHelperText error>{t("deleteDialog.incorrectName", { entity })}</FormHelperText>
56+
</Maybe>
57+
</Stack>
58+
</>
59+
)
60+
61+
return (
62+
<ConfirmDialog
63+
type="delete"
64+
hideCancel={false}
65+
open={isOpen}
66+
title={t("deleteDialog.title", { entity })}
67+
onConfirm={onConfirm}
68+
onClose={onCancel}
69+
description={content}
70+
confirmLoading={confirmLoading}
71+
disabled={!confirmed}
72+
/>
73+
)
74+
}
75+
76+
const useStyles = makeStyles((theme) => ({
77+
warning: {
78+
color: theme.palette.warning.light,
79+
},
80+
}))

site/src/components/Dialogs/Dialog.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export interface DialogActionButtonsProps {
7272
confirmLoading?: boolean
7373
/** Whether or not this is a confirm dialog */
7474
confirmDialog?: boolean
75+
/** Whether or not the submit button is disabled */
76+
disabled?: boolean
7577
/** Called when cancel is clicked */
7678
onCancel?: () => void
7779
/** Called when confirm is clicked */
@@ -94,6 +96,7 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
9496
confirmText = "Confirm",
9597
confirmLoading = false,
9698
confirmDialog,
99+
disabled = false,
97100
onCancel,
98101
onConfirm,
99102
type = "info",
@@ -122,6 +125,7 @@ export const DialogActionButtons: React.FC<DialogActionButtonsProps> = ({
122125
onClick={onConfirm}
123126
color={typeToColor(type)}
124127
loading={confirmLoading}
128+
disabled={disabled}
125129
type="submit"
126130
className={combineClasses({
127131
[styles.dialogButton]: true,

site/src/components/EnterpriseSnackbar/EnterpriseSnackbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const EnterpriseSnackbar: FC<React.PropsWithChildren<EnterpriseSnackbarPr
4545
<div className={styles.actionWrapper}>
4646
{action}
4747
<IconButton onClick={onClose} className={styles.iconButton}>
48-
<CloseIcon className={styles.closeIcon} />
48+
<CloseIcon className={styles.closeIcon} aria-label="close" />
4949
</IconButton>
5050
</div>
5151
}

site/src/i18n/en/common.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,12 @@
1111
"canceled": "Canceled action",
1212
"failed": "Failed",
1313
"queued": "Queued"
14+
},
15+
"deleteDialog": {
16+
"title": "Delete {{entity}}",
17+
"intro": "Deleting this {{entity}} is irreversible!",
18+
"confirm": "Are you sure you want to proceed? Type the name of this {{entity}} below to confirm.",
19+
"confirmLabel": "Name of {{entity}} to delete",
20+
"incorrectName": "Incorrect {{entity}} name."
1421
}
1522
}

site/src/i18n/en/templatePage.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
{
2-
"deleteDialog": {
3-
"title": "Delete template",
4-
"description": "Deleting a template is irreversible. Are you sure you want to proceed?"
5-
},
62
"deleteSuccess": "Template successfully deleted."
73
}

site/src/i18n/en/workspacePage.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"deleteDialog": {
3-
"title": "Delete workspace",
4-
"description": "Deleting a workspace is irreversible. Are you sure you want to proceed?"
3+
"info": "This workspace was created {{timeAgo}}."
54
},
65
"workspaceScheduleButton": {
76
"schedule": "Schedule",

site/src/pages/TemplatePage/TemplatePage.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useMachine, useSelector } from "@xstate/react"
22
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
33
import { FC, useContext } from "react"
44
import { Helmet } from "react-helmet-async"
5-
import { useTranslation } from "react-i18next"
65
import { Navigate, useParams } from "react-router-dom"
76
import { selectPermissions } from "xServices/auth/authSelectors"
87
import { XServiceContext } from "xServices/StateContext"
@@ -24,7 +23,6 @@ const useTemplateName = () => {
2423

2524
export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
2625
const organizationId = useOrganizationId()
27-
const { t } = useTranslation("templatePage")
2826
const templateName = useTemplateName()
2927
const [templateState, templateSend] = useMachine(templateMachine, {
3028
context: {
@@ -77,8 +75,8 @@ export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
7775
<DeleteDialog
7876
isOpen={templateState.matches("confirmingDelete")}
7977
confirmLoading={templateState.matches("deleting")}
80-
title={t("deleteDialog.title")}
81-
description={t("deleteDialog.description")}
78+
entity="template"
79+
name={template.name}
8280
onConfirm={() => {
8381
templateSend("CONFIRM_DELETE")
8482
}}

0 commit comments

Comments
 (0)