Skip to content

Commit 178aabb

Browse files
mikewuurazor-xseambot
authored
feat(AccessCode): add ability to delete (#328)
* disable edit by default * add delete access code menu item * add delete code button * format * rename disableCloseOnClick -> preventDefaultOnClick * Update src/lib/seam/access-codes/use-delete-access-code.ts Co-authored-by: Evan Sosenko <[email protected]> * update to use t. * Turn hardcoded 'disableDeleteAccessCode' into a prop * ci: Format code * add new disableDeleteAccessCode prop to elements --------- Co-authored-by: Evan Sosenko <[email protected]> Co-authored-by: Seam Bot <[email protected]>
1 parent 4d6ea5c commit 178aabb

File tree

10 files changed

+302
-97
lines changed

10 files changed

+302
-97
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
useMutation,
3+
type UseMutationResult,
4+
useQueryClient,
5+
} from '@tanstack/react-query'
6+
import type {
7+
AccessCodeDeleteRequest,
8+
ActionAttempt,
9+
ActionType,
10+
SeamError,
11+
} from 'seamapi'
12+
13+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
14+
15+
export type UseDeleteAccessCodeParams = never
16+
export interface UseDeleteAccessCodeData {
17+
actionAttempt: ActionAttempt<ActionType>
18+
}
19+
export type UseDeleteAccessCodeMutationParams = AccessCodeDeleteRequest
20+
21+
export function useDeleteAccessCode(): UseMutationResult<
22+
UseDeleteAccessCodeData,
23+
SeamError,
24+
UseDeleteAccessCodeMutationParams
25+
> {
26+
const { client } = useSeamClient()
27+
const queryClient = useQueryClient()
28+
29+
return useMutation<
30+
UseDeleteAccessCodeData,
31+
SeamError,
32+
AccessCodeDeleteRequest
33+
>({
34+
mutationFn: async (mutationParams: UseDeleteAccessCodeMutationParams) => {
35+
if (client === null) throw new NullSeamClientError()
36+
return await client.accessCodes.delete(mutationParams)
37+
},
38+
onSuccess: (_data, variables) => {
39+
void queryClient.invalidateQueries([
40+
'access_codes',
41+
'get',
42+
{ access_code_id: variables.access_code_id },
43+
])
44+
void queryClient.invalidateQueries(['access_codes', 'list'])
45+
},
46+
})
47+
}

src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const name = 'seam-access-code-details'
77
export const props: ElementProps<AccessCodeDetailsProps> = {
88
accessCodeId: 'string',
99
disableLockUnlock: 'boolean',
10+
disableDeleteAccessCode: 'boolean',
1011
onBack: 'object',
1112
onEdit: 'object',
1213
className: 'string',

src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
} from 'seamapi'
1010

1111
import { useAccessCode } from 'lib/seam/access-codes/use-access-code.js'
12+
import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js'
1213
import { AccessCodeDevice } from 'lib/seam/components/AccessCodeDetails/AccessCodeDevice.js'
1314
import { DeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
1415
import { Alerts } from 'lib/ui/Alert/Alerts.js'
@@ -24,6 +25,7 @@ export interface AccessCodeDetailsProps {
2425
onBack?: () => void
2526
onEdit: () => void
2627
className?: string
28+
disableDeleteAccessCode?: boolean
2729
}
2830

2931
export function AccessCodeDetails({
@@ -32,9 +34,11 @@ export function AccessCodeDetails({
3234
onBack,
3335
onEdit,
3436
className,
37+
disableDeleteAccessCode = false,
3538
}: AccessCodeDetailsProps): JSX.Element | null {
3639
const { accessCode } = useAccessCode(accessCodeId)
3740
const [selectedDeviceId, selectDevice] = useState<string | null>(null)
41+
const { mutate: deleteCode, isLoading: isDeleting } = useDeleteAccessCode()
3842

3943
if (accessCode == null) {
4044
return null
@@ -92,11 +96,24 @@ export function AccessCodeDetails({
9296
onSelectDevice={selectDevice}
9397
/>
9498
</div>
95-
{!disableEditAccessCode && (
99+
{(!disableEditAccessCode || !disableDeleteAccessCode) && (
96100
<div className='seam-actions'>
97-
<Button size='small' onClick={onEdit}>
98-
{t.editCode}
99-
</Button>
101+
{!disableEditAccessCode && (
102+
<Button size='small' onClick={onEdit} disabled={isDeleting}>
103+
{t.editCode}
104+
</Button>
105+
)}
106+
{!disableDeleteAccessCode && (
107+
<Button
108+
size='small'
109+
onClick={() => {
110+
deleteCode({ access_code_id: accessCode.access_code_id })
111+
}}
112+
disabled={isDeleting}
113+
>
114+
{t.deleteCode}
115+
</Button>
116+
)}
100117
</div>
101118
)}
102119
<div className='seam-details'>
@@ -224,4 +241,5 @@ const t = {
224241
start: 'Start',
225242
end: 'End',
226243
editCode: 'Edit code',
244+
deleteCode: 'Delete code',
227245
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { CopyIcon } from 'lib/icons/Copy.js'
2+
import type { UseAccessCodeData } from 'lib/seam/access-codes/use-access-code.js'
3+
import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js'
4+
import { Button } from 'lib/ui/Button.js'
5+
import { copyToClipboard } from 'lib/ui/clipboard.js'
6+
import { MenuItem } from 'lib/ui/Menu/MenuItem.js'
7+
import { MoreActionsMenu } from 'lib/ui/Menu/MoreActionsMenu.js'
8+
import { useToggle } from 'lib/ui/use-toggle.js'
9+
10+
export interface AccessCodeMenuProps {
11+
accessCode: NonNullable<UseAccessCodeData>
12+
onEdit: () => void
13+
onViewDetails: () => void
14+
disableEditAccessCode: boolean
15+
disableDeleteAccessCode: boolean
16+
}
17+
18+
export function AccessCodeMenu(props: AccessCodeMenuProps): JSX.Element {
19+
return (
20+
<MoreActionsMenu
21+
menuProps={{
22+
backgroundProps: {
23+
className: 'seam-access-code-table-action-menu',
24+
},
25+
}}
26+
>
27+
<Content {...props} />
28+
</MoreActionsMenu>
29+
)
30+
}
31+
32+
function Content({
33+
accessCode,
34+
onViewDetails,
35+
disableEditAccessCode,
36+
disableDeleteAccessCode,
37+
onEdit,
38+
}: AccessCodeMenuProps): JSX.Element {
39+
const [deleteConfirmationVisible, toggleDeleteConfirmation] = useToggle()
40+
41+
const deleteAccessCode = useDeleteAccessCode()
42+
43+
if (deleteConfirmationVisible) {
44+
return (
45+
<div className='seam-delete-confirmation'>
46+
<span>{t.deleteCodeConfirmation}</span>
47+
<div className='seam-actions'>
48+
<Button
49+
onClick={toggleDeleteConfirmation}
50+
disabled={deleteAccessCode.isLoading}
51+
>
52+
{t.cancelDelete}
53+
</Button>
54+
<Button
55+
variant='solid'
56+
disabled={deleteAccessCode.isLoading}
57+
onClick={() => {
58+
deleteAccessCode.mutate({
59+
access_code_id: accessCode.access_code_id,
60+
})
61+
}}
62+
>
63+
{t.confirmDelete}
64+
</Button>
65+
</div>
66+
</div>
67+
)
68+
}
69+
70+
return (
71+
<>
72+
<MenuItem
73+
onClick={() => {
74+
void copyToClipboard(accessCode.code ?? '')
75+
}}
76+
>
77+
<div className='seam-menu-item-copy'>
78+
<span>
79+
{t.copyCode} - {accessCode.code}
80+
</span>
81+
<CopyIcon />
82+
</div>
83+
</MenuItem>
84+
<div className='seam-divider' />
85+
<MenuItem onClick={onViewDetails}>{t.viewCodeDetails}</MenuItem>
86+
{!disableEditAccessCode && (
87+
<MenuItem onClick={onEdit}>{t.editCode}</MenuItem>
88+
)}
89+
{!disableDeleteAccessCode && (
90+
<>
91+
<div className='seam-divider' />
92+
<MenuItem
93+
onClick={(event) => {
94+
event.stopPropagation() // Prevent hiding menu on outside click
95+
toggleDeleteConfirmation()
96+
}}
97+
preventDefaultOnClick
98+
className='seam-text-danger'
99+
>
100+
{t.deleteCode}
101+
</MenuItem>
102+
</>
103+
)}
104+
</>
105+
)
106+
}
107+
108+
const t = {
109+
copyCode: 'Copy code',
110+
codeIssue: 'code issue',
111+
codeIssues: 'code issues',
112+
editCode: 'Edit code',
113+
viewCodeDetails: 'View code details',
114+
deleteCode: 'Delete code',
115+
deleteCodeConfirmation: 'Delete this code and data?',
116+
cancelDelete: 'Cancel',
117+
confirmDelete: 'Delete',
118+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
2+
import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
3+
import { TriangleWarningOutlineIcon } from 'lib/icons/TriangleWarningOutline.js'
4+
import type { UseAccessCodesData } from 'lib/seam/access-codes/use-access-codes.js'
5+
import { AccessCodeMenu } from 'lib/seam/components/AccessCodeTable/AccessCodeMenu.js'
6+
import { CodeDetails } from 'lib/seam/components/AccessCodeTable/CodeDetails.js'
7+
import { TableCell } from 'lib/ui/Table/TableCell.js'
8+
import { TableRow } from 'lib/ui/Table/TableRow.js'
9+
import { Title } from 'lib/ui/typography/Title.js'
10+
11+
export interface AccessCodeRowProps {
12+
accessCode: UseAccessCodesData[number]
13+
onClick: () => void
14+
onEdit: () => void
15+
disableEditAccessCode: boolean
16+
disableDeleteAccessCode: boolean
17+
}
18+
19+
export function AccessCodeRow({
20+
onClick,
21+
accessCode,
22+
onEdit,
23+
disableEditAccessCode,
24+
disableDeleteAccessCode,
25+
}: AccessCodeRowProps): JSX.Element {
26+
const errorCount = accessCode.errors.length
27+
const warningCount = accessCode.warnings.length
28+
const isPlural = errorCount === 0 || errorCount > 1
29+
const errorIconTitle = isPlural
30+
? `${errorCount} ${t.codeIssues}`
31+
: `${errorCount} ${t.codeIssue}`
32+
const warningIconTitle = isPlural
33+
? `${warningCount} ${t.codeIssues}`
34+
: `${warningCount} ${t.codeIssue}`
35+
36+
return (
37+
<TableRow onClick={onClick}>
38+
<TableCell className='seam-icon-cell'>
39+
<div>
40+
<AccessCodeKeyIcon />
41+
</div>
42+
</TableCell>
43+
<TableCell className='seam-name-cell'>
44+
<Title className='seam-truncated-text'>{accessCode.name}</Title>
45+
<CodeDetails accessCode={accessCode} />
46+
</TableCell>
47+
<TableCell className='seam-action-cell'>
48+
{errorCount > 0 && (
49+
<div className='seam-code-issue-icon-wrap' title={errorIconTitle}>
50+
<ExclamationCircleOutlineIcon />
51+
</div>
52+
)}
53+
{errorCount === 0 && warningCount > 0 && (
54+
<div className='seam-code-issue-icon-wrap' title={warningIconTitle}>
55+
<TriangleWarningOutlineIcon />
56+
</div>
57+
)}
58+
<AccessCodeMenu
59+
accessCode={accessCode}
60+
onEdit={onEdit}
61+
onViewDetails={onClick}
62+
disableDeleteAccessCode={disableDeleteAccessCode}
63+
disableEditAccessCode={disableEditAccessCode}
64+
/>
65+
</TableCell>
66+
</TableRow>
67+
)
68+
}
69+
70+
const t = {
71+
codeIssue: 'code issue',
72+
codeIssues: 'code issues',
73+
}

src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const name = 'seam-access-code-table'
77
export const props: ElementProps<Omit<AccessCodeTableProps, 'title'>> = {
88
deviceId: 'string',
99
disableLockUnlock: 'boolean',
10+
disableDeleteAccessCode: 'boolean',
1011
disableSearch: 'boolean',
1112
accessCodeFilter: 'object',
1213
accessCodeComparator: 'object',

0 commit comments

Comments
 (0)