-
Notifications
You must be signed in to change notification settings - Fork 470
feat(ui): Add Destructive block and wire the delete account section #9555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
33bce32
feat(ui): Add Destructive block and wire the delete account section
alexcarpenter 11bab1e
fix(swingset): Remount the danger zone story after a delete
alexcarpenter 2a47dfe
refactor(ui): Narrow Destructive copy props to string
alexcarpenter 7f7a515
fix(swingset): Collapse the Blocks sidebar group by default
alexcarpenter ec10bf4
wrap pretty
alexcarpenter 624bf92
refactor(ui): Rename the delete-account machine to a controller
alexcarpenter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import * as Stories from './destructive.stories'; | ||
|
|
||
| # Destructive | ||
|
|
||
| A type-to-confirm dialog for an action that cannot be undone. The action stays inert until the user types the confirmation phrase back. | ||
|
|
||
| ## Example | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={Stories} | ||
| composition={[ | ||
| { name: 'Dialog', href: '/components/dialog', layer: 'Components' }, | ||
| { name: 'Card', href: '/components/card', layer: 'Components' }, | ||
| { name: 'Field', href: '/components/field', layer: 'Components' }, | ||
| { name: 'Button', href: '/components/button', layer: 'Components' }, | ||
| ]} | ||
| /> | ||
|
|
||
| ## Usage | ||
|
|
||
| The block holds one thing: the phrase the user types. Nothing outside the dialog can use a half-typed string, so keeping it inside removes the keystroke plumbing a caller would otherwise write. | ||
|
|
||
| Everything that decides what the dialog does next belongs to the caller. `open` closes it, `isDeleting` marks it busy, `errorMessage` explains a failure. | ||
|
|
||
| ```tsx | ||
| import { Destructive } from '@clerk/ui/mosaic/blocks/destructive'; | ||
| import { Button } from '@clerk/ui/mosaic/components/button'; | ||
|
|
||
| const [open, setOpen] = useState(false); | ||
| const [isDeleting, setIsDeleting] = useState(false); | ||
|
|
||
| const handleDelete = async () => { | ||
| setIsDeleting(true); | ||
| await deleteAccount(); | ||
| setIsDeleting(false); | ||
| setOpen(false); | ||
| }; | ||
|
|
||
| <Destructive | ||
| open={open} | ||
| onOpenChange={setOpen} | ||
| trigger={<Button color='negative' variant='outline'>Delete account</Button>} | ||
| title='Delete account?' | ||
| description='Are you sure you want to delete your account? All of your data will be permanently deleted.' | ||
| fieldLabel='Type “Delete account” below to continue' | ||
| confirmationValue='Delete account' | ||
| actionLabel='Delete account' | ||
| onDelete={() => void handleDelete()} | ||
| isDeleting={isDeleting} | ||
| />; | ||
|
alexcarpenter marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| ## Failure | ||
|
|
||
| A failed attempt leaves the dialog up. Pass the sentence the user should read as `errorMessage`, and clear it when the next attempt starts. The field is marked invalid for as long as a message is set. | ||
|
|
||
| <Story | ||
| name='WithError' | ||
| storyModule={Stories} | ||
| /> | ||
|
|
||
| ## Props | ||
|
|
||
| | Prop | Type | Description | | ||
| | ------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | | ||
| | `open` | `boolean` | Whether the confirmation is showing. Controlled, the way any dialog is. | | ||
| | `onOpenChange` | `(open: boolean) => void` | Asks to open or close. Fired by the trigger, Cancel, Escape, and the backdrop. | | ||
| | `trigger` | `ReactNode` | Optional. The button that asks to open the dialog. | | ||
| | `title` | `string` | Names what is about to be destroyed. | | ||
| | `description` | `string` | Spells out what is lost. Sits above the confirmation field. | | ||
| | `fieldLabel` | `string` | Labels the confirmation field. | | ||
| | `confirmationValue` | `string` | The phrase the user has to type back. Also the field's placeholder. | | ||
| | `actionLabel` | `string` | The destructive button's label. | | ||
| | `cancelLabel` | `string` | Optional. Defaults to `Cancel`. | | ||
| | `onDelete` | `() => void` | Asks the caller to run the action. Reached by the button or by Enter in the field, once the typed phrase matches. | | ||
| | `isDeleting` | `boolean` | Optional. Disables the field and renders the action pending. | | ||
| | `errorMessage` | `string` | Optional. Marks the field invalid and renders under it. | | ||
|
alexcarpenter marked this conversation as resolved.
|
||
|
|
||
| ## Driving it from a machine | ||
|
|
||
| `UserProfileDeleteSection` wires the same block to a state machine rather than to `useState`. The machine's state maps onto the same props: | ||
|
|
||
| ```tsx | ||
| <Destructive | ||
| open={snapshot.value === 'confirming' || snapshot.value === 'deleting'} | ||
| onOpenChange={open => send({ type: open ? 'OPEN' : 'CANCEL' })} | ||
| onDelete={() => send({ type: 'CONFIRM' })} | ||
| isDeleting={snapshot.value === 'deleting'} | ||
| errorMessage={snapshot.context.errorMessage} | ||
| {...copy} | ||
| /> | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import { Destructive } from '@clerk/ui/mosaic/blocks/destructive'; | ||
| import { Button } from '@clerk/ui/mosaic/components/button'; | ||
| import React from 'react'; | ||
|
|
||
| import type { StoryMeta } from '@/lib/types'; | ||
|
|
||
| // Exposes this file's own source (via the `?raw` webpack rule) so each `<Story>` example | ||
| // renders a code footer with its function's source. See `StoryModule.__source`. | ||
| export { default as __source } from './destructive.stories?raw'; | ||
|
|
||
| export const meta: StoryMeta = { | ||
| group: 'Blocks', | ||
| title: 'Destructive', | ||
| source: 'packages/ui/src/mosaic/blocks/destructive/destructive.tsx', | ||
| }; | ||
|
|
||
| // A real delete is a network round trip. Without one the action never renders its pending | ||
| // state, so both stories wait before they settle. | ||
| const settleAfter = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)); | ||
|
|
||
| const trigger = ( | ||
| <Button | ||
| color='negative' | ||
| variant='outline' | ||
| > | ||
| Delete account | ||
| </Button> | ||
| ); | ||
|
|
||
| /** | ||
| * The block holds the typed phrase and compares it to `confirmationValue`. Everything that | ||
| * decides what the dialog does next stays with the caller: `open` closes it, `isDeleting` | ||
| * marks it busy, `errorMessage` explains a failure. | ||
| */ | ||
| export function Default() { | ||
| const [open, setOpen] = React.useState(false); | ||
| const [isDeleting, setIsDeleting] = React.useState(false); | ||
|
|
||
| const handleDelete = async () => { | ||
| setIsDeleting(true); | ||
| await settleAfter(2000); | ||
| setIsDeleting(false); | ||
| setOpen(false); | ||
| }; | ||
|
alexcarpenter marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <Destructive | ||
| open={open} | ||
| onOpenChange={setOpen} | ||
| trigger={trigger} | ||
| title='Delete account?' | ||
| description='Are you sure you want to delete your account? All of your data will be permanently deleted.' | ||
| fieldLabel='Type “Delete account” below to continue' | ||
| confirmationValue='Delete account' | ||
| actionLabel='Delete account' | ||
| onDelete={() => void handleDelete()} | ||
| isDeleting={isDeleting} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * A failed attempt leaves the dialog up. Pass the sentence the user should read as | ||
| * `errorMessage`, and clear it when the next attempt starts. | ||
| */ | ||
| export function WithError() { | ||
| const [open, setOpen] = React.useState(false); | ||
| const [isDeleting, setIsDeleting] = React.useState(false); | ||
| const [errorMessage, setErrorMessage] = React.useState<string | undefined>(undefined); | ||
|
|
||
| const handleDelete = async () => { | ||
| setErrorMessage(undefined); | ||
| setIsDeleting(true); | ||
| await settleAfter(2000); | ||
| setIsDeleting(false); | ||
| setErrorMessage('Your subscription is still active. Cancel it before you delete your account.'); | ||
| }; | ||
|
|
||
| // The error belongs to the caller, so the caller drops it. Without this a reopened dialog | ||
| // still shows why the last attempt failed. | ||
| const handleOpenChange = (next: boolean) => { | ||
| setOpen(next); | ||
| if (!next) { | ||
| setErrorMessage(undefined); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Destructive | ||
| open={open} | ||
| onOpenChange={handleOpenChange} | ||
| trigger={trigger} | ||
| title='Delete account?' | ||
| description='Are you sure you want to delete your account? All of your data will be permanently deleted.' | ||
| fieldLabel='Type “Delete account” below to continue' | ||
| confirmationValue='Delete account' | ||
| actionLabel='Delete account' | ||
| onDelete={() => void handleDelete()} | ||
| isDeleting={isDeleting} | ||
| errorMessage={errorMessage} | ||
| /> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.