Skip to content

Commit

Permalink
PLU-361: warning when deleting connections (#794)
Browse files Browse the repository at this point in the history
## Problem
There is no warning/prompt when user is deleting a connection.
Other delete actions on Apps/Pipes/Tiles have a warning/prompt when user
is attempting to delete.

Closes [PLU-361:
warning-when-deleting-connections](https://linear.app/ogp/issue/PLU-361/warning-when-deleting-connections)

## Solution
Add warning/prompt modal when user is attempting to delete a connection.


**Improvements**:
- Refactor Tiles to use MenuAlertDialog instead of inline AlertDialog
- Add toast on Tile delete success


## Before & After Screenshots

**BEFORE**:
-  No warning/modal, connection is deleted immediately.


https://github.com/user-attachments/assets/1d6ccd95-76bf-4e00-ad43-615902f3aa22

- No notification on Tile delete success

**AFTER**:
- Delete Connection


https://github.com/user-attachments/assets/c5b89692-26b1-47b3-80fe-20b64918f622

- Delete Tile


https://github.com/user-attachments/assets/c68e63df-9e30-4071-91f0-968f000524fe


## Tests
- [X] Modal appears when user attempts to delete a connection.
- [X] Connection is deleted successfully upon confirmation on modal.
- [X] Able to delete Tiles
- [X] Able to duplicate Pipes
- [X] Able to delete Pipes

**New scripts**:
- `packages/frontend/src/components/MenuAlertDialog/index.tsx` : generic
MenuAlertDialog for reuse across delete actions (updated from
`plumber/packages/frontend/src/components/FlowRow/MenuAlertDialog.tsx`)
  • Loading branch information
kevinkim-ogp authored Nov 19, 2024
1 parent 4c8e637 commit 25eeaae
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 76 deletions.
68 changes: 45 additions & 23 deletions packages/frontend/src/components/AppConnectionRow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IConnection } from '@plumber/types'
import * as React from 'react'
import { useCallback, useRef, useState } from 'react'
import { useLazyQuery, useMutation } from '@apollo/client'
import { Card } from '@chakra-ui/react'
import { Card, useDisclosure } from '@chakra-ui/react'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import ErrorIcon from '@mui/icons-material/Error'
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'
Expand All @@ -15,6 +15,7 @@ import { useToast } from '@opengovsg/design-system-react'
import { DateTime } from 'luxon'

import ConnectionContextMenu from '@/components/AppConnectionContextMenu'
import MenuAlertDialog from '@/components/MenuAlertDialog'
import { DELETE_CONNECTION } from '@/graphql/mutations/delete-connection'
import { TEST_CONNECTION } from '@/graphql/queries/test-connection'
import useFormatMessage from '@/hooks/useFormatMessage'
Expand Down Expand Up @@ -46,46 +47,58 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
setTimeout(() => setVerificationVisible(false), 3000)
},
})
const [deleteConnection] = useMutation(DELETE_CONNECTION)
const [deleteConnection, { loading: isDeletingConnection }] =
useMutation(DELETE_CONNECTION)

const formatMessage = useFormatMessage()
const { id, key, formattedData, createdAt, flowCount } = props.connection

const contextButtonRef = useRef<SVGSVGElement | null>(null)
const [anchorEl, setAnchorEl] = useState<SVGSVGElement | null>(null)
const cancelRef = useRef<HTMLButtonElement>(null)
const {
isOpen: isDialogOpen,
onOpen: onDialogOpen,
onClose: onDialogClose,
} = useDisclosure()

const handleClose = () => {
setAnchorEl(null)
}

const onContextMenuClick = () => setAnchorEl(contextButtonRef.current)
const onContextMenuAction = useCallback(
async (
_event: React.MouseEvent<Element, MouseEvent>,
action: { [key: string]: string },
) => {
if (action.type === 'delete') {
await deleteConnection({
variables: { input: { id } },
update: (cache) => {
const connectionCacheId = cache.identify({
__typename: 'Connection',
id,
})

cache.evict({
id: connectionCacheId,
})
},
const onConnectionDelete = useCallback(async () => {
await deleteConnection({
variables: { input: { id } },
update: (cache) => {
const connectionCacheId = cache.identify({
__typename: 'Connection',
id,
})

cache.evict({
id: connectionCacheId,
})
},
onCompleted: () => {
onDialogClose()
toast({
title: 'The connection has been deleted.',
status: 'success',
duration: 3000,
isClosable: true,
position: 'bottom-right',
})
},
})
}, [deleteConnection, id, toast, onDialogClose])

const onContextMenuClick = () => setAnchorEl(contextButtonRef.current)
const onContextMenuAction = useCallback(
async (
_event: React.MouseEvent<Element, MouseEvent>,
action: { [key: string]: string },
) => {
if (action.type === 'delete') {
onDialogOpen()
} else if (action.type === 'test') {
setVerificationVisible(true)
const testResults = await testConnection({
Expand All @@ -101,7 +114,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
}
}
},
[deleteConnection, id, testConnection, toast],
[id, onDialogOpen, testConnection],
)

const relativeCreatedAt = DateTime.fromMillis(
Expand Down Expand Up @@ -195,6 +208,15 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
anchorEl={anchorEl}
/>
)}
<MenuAlertDialog
isDialogOpen={isDialogOpen}
cancelRef={cancelRef}
onDialogClose={onDialogClose}
dialogHeader="Connection"
dialogType="delete"
onClick={onConnectionDelete}
isLoading={isDeletingConnection}
/>
</>
)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/frontend/src/components/FlowRow/FlowContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ import {
useToast,
} from '@opengovsg/design-system-react'

import MenuAlertDialog, { AlertDialogType } from '@/components/MenuAlertDialog'
import * as URLS from '@/config/urls'
import { DELETE_FLOW } from '@/graphql/mutations/delete-flow'
import { DUPLICATE_FLOW } from '@/graphql/mutations/duplicate-flow'

import MenuAlertDialog, { AlertDialogType } from './MenuAlertDialog'

interface FlowContextMenuProps {
flow: IFlow
}
Expand Down Expand Up @@ -184,7 +183,8 @@ export default function FlowContextMenu(props: FlowContextMenuProps) {
isDialogOpen={isDialogOpen}
cancelRef={cancelRef}
onDialogClose={onDialogClose}
type={dialogType}
dialogHeader="Pipe"
dialogType={dialogType}
onClick={dialogType === 'delete' ? onFlowDelete : onFlowDuplicate}
isLoading={dialogType === 'delete' ? isDeletingFlow : isDuplicatingFlow}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
import { Button } from '@opengovsg/design-system-react'

export type AlertDialogType = 'delete' | 'duplicate'
export type AlertHeaderType = 'Connection' | 'Pipe' | 'Tile'

interface MenuAlertDialogProps {
isDialogOpen: boolean
cancelRef: React.RefObject<HTMLButtonElement>
onDialogClose: () => void
type: AlertDialogType
dialogType: AlertDialogType
dialogHeader: AlertHeaderType
onClick: () => void
isLoading: boolean
}
Expand All @@ -25,12 +27,15 @@ interface AlertDialogContent {
buttonText: string
}

function getAlertDialogContent(type: AlertDialogType): AlertDialogContent {
switch (type) {
function getAlertDialogContent(
dialogHeader: AlertHeaderType,
dialogType: AlertDialogType,
): AlertDialogContent {
switch (dialogType) {
case 'delete':
return {
header: 'Delete Pipe',
body: "Are you sure you want to delete this pipe? You can't undo this action afterwards.",
header: `Delete ${dialogHeader}`,
body: `Are you sure you want to delete this ${dialogHeader?.toLowerCase()}? You can't undo this action afterwards.`,
buttonText: 'Delete',
}
case 'duplicate':
Expand All @@ -43,9 +48,19 @@ function getAlertDialogContent(type: AlertDialogType): AlertDialogContent {
}

export default function MenuAlertDialog(props: MenuAlertDialogProps) {
const { isDialogOpen, cancelRef, onDialogClose, type, onClick, isLoading } =
props
const { header, body, buttonText } = getAlertDialogContent(type)
const {
isDialogOpen,
cancelRef,
onDialogClose,
dialogHeader,
dialogType,
onClick,
isLoading,
} = props
const { header, body, buttonText } = getAlertDialogContent(
dialogHeader,
dialogType,
)

return (
<AlertDialog
Expand Down
62 changes: 20 additions & 42 deletions packages/frontend/src/pages/Tiles/components/TileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ import { MdOutlineRemoveRedEye } from 'react-icons/md'
import { Link, useNavigate } from 'react-router-dom'
import { useMutation } from '@apollo/client'
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Box,
Divider,
Flex,
Expand All @@ -23,13 +17,14 @@ import {
VStack,
} from '@chakra-ui/react'
import {
Button,
IconButton,
Tag,
TagLabel,
TagLeftIcon,
useToast,
} from '@opengovsg/design-system-react'

import MenuAlertDialog from '@/components/MenuAlertDialog'
import * as URLS from '@/config/urls'
import type { TableMetadata } from '@/graphql/__generated__/graphql'
import { DELETE_TABLE } from '@/graphql/mutations/tiles/delete-table'
Expand All @@ -38,6 +33,7 @@ import { toPrettyDateString } from '@/helpers/dateTime'

const TileListItem = ({ table }: { table: TableMetadata }): JSX.Element => {
const navigate = useNavigate()
const toast = useToast()
const [deleteTable, { loading: isDeletingTable }] = useMutation(
DELETE_TABLE,
{
Expand Down Expand Up @@ -73,7 +69,14 @@ const TileListItem = ({ table }: { table: TableMetadata }): JSX.Element => {
const deleteTile = useCallback(async () => {
await deleteTable()
onDialogClose()
}, [deleteTable, onDialogClose])
toast({
title: 'The tile has been deleted.',
status: 'success',
duration: 3000,
isClosable: true,
position: 'top',
})
}, [deleteTable, onDialogClose, toast])

return (
<Link to={URLS.TILE(table.id)}>
Expand Down Expand Up @@ -145,40 +148,15 @@ const TileListItem = ({ table }: { table: TableMetadata }): JSX.Element => {
</Menu>
</Flex>
</Flex>
<AlertDialog
isOpen={isDialogOpen}
leastDestructiveRef={cancelRef}
onClose={onDialogClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader>Delete Tile</AlertDialogHeader>

<AlertDialogBody>
{"Are you sure? You can't undo this action afterwards."}
</AlertDialogBody>

<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={onDialogClose}
variant="clear"
colorScheme="secondary"
>
Cancel
</Button>
<Button
colorScheme="critical"
onClick={deleteTile}
ml={3}
isLoading={isDeletingTable}
>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
<MenuAlertDialog
isDialogOpen={isDialogOpen}
cancelRef={cancelRef}
onDialogClose={onDialogClose}
dialogHeader="Tile"
dialogType="delete"
onClick={deleteTile}
isLoading={isDeletingTable}
/>
</Link>
)
}
Expand Down

0 comments on commit 25eeaae

Please sign in to comment.