Skip to content

Commit 70b1515

Browse files
authored
Merge pull request #650 from seamapi/feat/449-delete-access-code
Display warning when access-code is being removed
2 parents ca18544 + 43217e1 commit 70b1515

File tree

10 files changed

+337
-146
lines changed

10 files changed

+337
-146
lines changed

.storybook/seed-fake.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,20 @@ export const seedFake = (db) => {
205205
is_managed: true,
206206
})
207207

208+
db.addAccessCode({
209+
device_id: device1.device_id,
210+
workspace_id: ws2.workspace_id,
211+
created_at: '2023-05-19T03:11:10.000',
212+
name: "Luc's Front Door Code",
213+
code: '4444',
214+
common_code_key: null,
215+
type: 'ongoing',
216+
status: 'removing',
217+
errors: [],
218+
warnings: [],
219+
is_managed: true,
220+
})
221+
208222
const device2 = db.addDevice({
209223
connected_account_id: ca.connected_account_id,
210224
device_type: 'august_lock',

src/lib/seam/access-codes/use-delete-access-code.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
AccessCodesDeleteParams,
33
SeamHttpApiError,
44
} from '@seamapi/http/connect'
5+
import type { AccessCode } from '@seamapi/types/connect'
56
import {
67
useMutation,
78
type UseMutationResult,
@@ -42,6 +43,20 @@ export function useDeleteAccessCode(): UseMutationResult<
4243
],
4344
})
4445
void queryClient.invalidateQueries({ queryKey: ['access_codes', 'list'] })
46+
47+
queryClient.setQueryData<AccessCode | null>(
48+
['access_codes', 'get', { access_code_id: variables.access_code_id }],
49+
(accessCode) => {
50+
if (accessCode == null) {
51+
return
52+
}
53+
54+
return {
55+
...accessCode,
56+
status: 'removing',
57+
}
58+
}
59+
)
4560
},
4661
})
4762
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export const name = 'seam-access-code-details'
77
export const props: ElementProps<AccessCodeDetailsProps> = {
88
accessCodeId: 'string',
99
onEdit: 'object',
10+
preventDefaultOnEdit: 'boolean',
11+
onDelete: 'object',
12+
preventDefaultOnDelete: 'boolean',
1013
}
1114

1215
export { AccessCodeDetails as Component } from './AccessCodeDetails.js'

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ export const DisableLockUnlock: Story = {
6565
/>
6666
),
6767
}
68+
69+
export const AccessCodeBeingRemoved: Story = {
70+
render: (props) => (
71+
<AccessCodeDetails
72+
{...props}
73+
accessCodeId={props.accessCodeId ?? 'access_code5'}
74+
/>
75+
),
76+
}

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

Lines changed: 169 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AccessCode } from '@seamapi/types/connect'
22
import classNames from 'classnames'
33
import { DateTime } from 'luxon'
4-
import { useState } from 'react'
4+
import { useCallback, useEffect, useState } from 'react'
55

66
import { CopyIcon } from 'lib/icons/Copy.js'
77
import { useAccessCode } from 'lib/seam/access-codes/use-access-code.js'
@@ -12,6 +12,7 @@ import {
1212
withRequiredCommonProps,
1313
} from 'lib/seam/components/common-props.js'
1414
import { NestedDeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
15+
import { NestedEditAccessCodeForm } from 'lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js'
1516
import {
1617
accessCodeErrorFilter,
1718
accessCodeWarningFilter,
@@ -22,11 +23,15 @@ import { Button } from 'lib/ui/Button.js'
2223
import { copyToClipboard } from 'lib/ui/clipboard.js'
2324
import { IconButton } from 'lib/ui/IconButton.js'
2425
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
26+
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
2527
import { useIsDateInPast } from 'lib/ui/use-is-date-in-past.js'
2628

2729
export interface AccessCodeDetailsProps extends CommonProps {
2830
accessCodeId: string
29-
onEdit: () => void
31+
onEdit?: () => void
32+
preventDefaultOnEdit?: boolean
33+
onDelete?: () => void
34+
preventDefaultOnDelete?: boolean
3035
}
3136

3237
export const NestedAccessCodeDetails =
@@ -35,6 +40,9 @@ export const NestedAccessCodeDetails =
3540
export function AccessCodeDetails({
3641
accessCodeId,
3742
onEdit,
43+
preventDefaultOnEdit = false,
44+
onDelete,
45+
preventDefaultOnDelete = false,
3846
errorFilter = () => true,
3947
warningFilter = () => true,
4048
disableCreateAccessCode = false,
@@ -52,12 +60,72 @@ export function AccessCodeDetails({
5260
const { accessCode } = useAccessCode({ access_code_id: accessCodeId })
5361
const [selectedDeviceId, selectDevice] = useState<string | null>(null)
5462
const { mutate: deleteCode, isPending: isDeleting } = useDeleteAccessCode()
63+
const [editFormOpen, setEditFormOpen] = useState<boolean>(false)
64+
65+
const [accessCodeResult, setAccessCodeResult] = useState<
66+
'updated' | 'deleted' | null
67+
>(null)
68+
const [snackbarMessage, setSnackbarMessage] = useState<string>('')
69+
70+
// Circumvent Snackbar bug that causes it to switch to default message
71+
// while the dismiss animation is playing
72+
useEffect(() => {
73+
if (accessCodeResult !== null) {
74+
setSnackbarMessage(accessCodeResultToMessage(accessCodeResult))
75+
}
76+
}, [accessCodeResult])
77+
78+
const handleEdit = useCallback((): void => {
79+
onEdit?.()
80+
if (preventDefaultOnEdit) return
81+
setEditFormOpen(true)
82+
}, [onEdit, preventDefaultOnEdit, setEditFormOpen])
83+
84+
const handleDelete = useCallback((): void => {
85+
onDelete?.()
86+
if (preventDefaultOnDelete) return
87+
if (accessCode == null) return
88+
deleteCode(
89+
{ access_code_id: accessCode.access_code_id },
90+
{
91+
onSuccess: () => {
92+
setAccessCodeResult('deleted')
93+
},
94+
}
95+
)
96+
}, [accessCode, deleteCode, onDelete, preventDefaultOnDelete])
5597

5698
if (accessCode == null) {
5799
return null
58100
}
59101

60102
const name = accessCode.name ?? t.fallbackName
103+
const isAccessCodeBeingRemoved = accessCode.status === 'removing'
104+
105+
if (editFormOpen) {
106+
return (
107+
<NestedEditAccessCodeForm
108+
accessCodeId={accessCode.access_code_id}
109+
errorFilter={errorFilter}
110+
warningFilter={warningFilter}
111+
disableLockUnlock={disableLockUnlock}
112+
disableCreateAccessCode={disableCreateAccessCode}
113+
disableEditAccessCode={disableEditAccessCode}
114+
disableDeleteAccessCode={disableDeleteAccessCode}
115+
disableResourceIds={disableResourceIds}
116+
disableConnectedAccountInformation={disableConnectedAccountInformation}
117+
disableClimateSettingSchedules={disableClimateSettingSchedules}
118+
onBack={() => {
119+
setEditFormOpen(false)
120+
}}
121+
onSuccess={() => {
122+
setAccessCodeResult('updated')
123+
setEditFormOpen(false)
124+
}}
125+
className={className}
126+
/>
127+
)
128+
}
61129

62130
if (selectedDeviceId != null) {
63131
return (
@@ -96,92 +164,114 @@ export function AccessCodeDetails({
96164
variant: 'warning' as const,
97165
message: warning.message,
98166
})),
167+
168+
...(isAccessCodeBeingRemoved
169+
? [
170+
{
171+
variant: 'warning' as const,
172+
message: t.warningRemoving,
173+
},
174+
]
175+
: []),
99176
]
100177

101178
return (
102-
<div className={classNames('seam-access-code-details', className)}>
103-
<ContentHeader title='Access code' onBack={onBack} />
104-
<div className='seam-summary'>
105-
<div
106-
className={classNames(
107-
'seam-top',
108-
alerts.length > 0 && 'seam-top-has-alerts'
109-
)}
110-
>
111-
<span className='seam-label'>{t.accessCode}</span>
112-
<h5 className='seam-access-code-name'>{name}</h5>
113-
<div className='seam-code'>
114-
<span>{accessCode.code}</span>
115-
<IconButton
116-
onClick={() => {
117-
void copyToClipboard(accessCode.code ?? '')
118-
}}
119-
>
120-
<CopyIcon />
121-
</IconButton>
122-
</div>
123-
<div className='seam-duration'>
124-
<Duration accessCode={accessCode} />
125-
</div>
126-
</div>
127-
<Alerts alerts={alerts} className='seam-alerts-padded' />
128-
<AccessCodeDevice
129-
deviceId={accessCode.device_id}
130-
disableLockUnlock={disableLockUnlock}
131-
onSelectDevice={selectDevice}
132-
/>
133-
</div>
134-
{(!disableEditAccessCode || !disableDeleteAccessCode) && (
135-
<div className='seam-actions'>
136-
{!disableEditAccessCode && (
137-
<Button size='small' onClick={onEdit} disabled={isDeleting}>
138-
{t.editCode}
139-
</Button>
140-
)}
141-
{!disableDeleteAccessCode && (
142-
<Button
143-
size='small'
144-
onClick={() => {
145-
deleteCode({ access_code_id: accessCode.access_code_id })
146-
}}
147-
disabled={isDeleting}
148-
>
149-
{t.deleteCode}
150-
</Button>
151-
)}
152-
</div>
153-
)}
154-
<div className='seam-details'>
155-
{!disableResourceIds && (
156-
<div className='seam-row'>
157-
<div className='seam-heading'>{t.id}:</div>
158-
<div className='seam-content seam-code-id'>
159-
<span>{accessCode.access_code_id}</span>
179+
<>
180+
<Snackbar
181+
variant='success'
182+
message={snackbarMessage}
183+
visible={accessCodeResult != null}
184+
autoDismiss
185+
onClose={() => {
186+
setAccessCodeResult(null)
187+
}}
188+
/>
189+
<div className={classNames('seam-access-code-details', className)}>
190+
<ContentHeader title='Access code' onBack={onBack} />
191+
<div className='seam-summary'>
192+
<div
193+
className={classNames(
194+
'seam-top',
195+
alerts.length > 0 && 'seam-top-has-alerts'
196+
)}
197+
>
198+
<span className='seam-label'>{t.accessCode}</span>
199+
<h5 className='seam-access-code-name'>{name}</h5>
200+
<div className='seam-code'>
201+
<span>{accessCode.code}</span>
160202
<IconButton
161203
onClick={() => {
162-
void copyToClipboard(accessCode.access_code_id)
204+
void copyToClipboard(accessCode.code ?? '')
163205
}}
164206
>
165207
<CopyIcon />
166208
</IconButton>
167209
</div>
210+
<div className='seam-duration'>
211+
<Duration accessCode={accessCode} />
212+
</div>
213+
</div>
214+
<Alerts alerts={alerts} className='seam-alerts-padded' />
215+
<AccessCodeDevice
216+
deviceId={accessCode.device_id}
217+
disableLockUnlock={disableLockUnlock}
218+
onSelectDevice={selectDevice}
219+
/>
220+
</div>
221+
{(!disableEditAccessCode || !disableDeleteAccessCode) && (
222+
<div className='seam-actions'>
223+
{!disableEditAccessCode && (
224+
<Button
225+
size='small'
226+
onClick={handleEdit}
227+
disabled={isAccessCodeBeingRemoved || isDeleting}
228+
>
229+
{t.editCode}
230+
</Button>
231+
)}
232+
{!disableDeleteAccessCode && (
233+
<Button
234+
size='small'
235+
onClick={handleDelete}
236+
disabled={isAccessCodeBeingRemoved || isDeleting}
237+
>
238+
{t.deleteCode}
239+
</Button>
240+
)}
168241
</div>
169242
)}
170-
<div className='seam-row'>
171-
<div className='seam-heading'>{t.created}:</div>
172-
<div className='seam-content'>
173-
{formatDate(accessCode.created_at)}
243+
<div className='seam-details'>
244+
{!disableResourceIds && (
245+
<div className='seam-row'>
246+
<div className='seam-heading'>{t.id}:</div>
247+
<div className='seam-content seam-code-id'>
248+
<span>{accessCode.access_code_id}</span>
249+
<IconButton
250+
onClick={() => {
251+
void copyToClipboard(accessCode.access_code_id)
252+
}}
253+
>
254+
<CopyIcon />
255+
</IconButton>
256+
</div>
257+
</div>
258+
)}
259+
<div className='seam-row'>
260+
<div className='seam-heading'>{t.created}:</div>
261+
<div className='seam-content'>
262+
{formatDate(accessCode.created_at)}
263+
</div>
174264
</div>
175-
</div>
176265

177-
<div className='seam-row seam-schedule'>
178-
<div className='seam-heading'>{t.timing}:</div>
179-
<div className='seam-content'>
180-
<ScheduleInfo accessCode={accessCode} />
266+
<div className='seam-row seam-schedule'>
267+
<div className='seam-heading'>{t.timing}:</div>
268+
<div className='seam-content'>
269+
<ScheduleInfo accessCode={accessCode} />
270+
</div>
181271
</div>
182272
</div>
183273
</div>
184-
</div>
274+
</>
185275
)
186276
}
187277

@@ -266,6 +356,11 @@ const formatDate = (date: string): string =>
266356
year: 'numeric',
267357
})
268358

359+
const accessCodeResultToMessage = (result: 'updated' | 'deleted'): string => {
360+
if (result === 'deleted') return t.accessCodeDeleted
361+
return t.accessCodeUpdated
362+
}
363+
269364
const t = {
270365
accessCode: 'Access code',
271366
fallbackName: 'Code',
@@ -282,4 +377,7 @@ const t = {
282377
at: 'at',
283378
editCode: 'Edit code',
284379
deleteCode: 'Delete code',
380+
warningRemoving: 'This access code is currently being removed.',
381+
accessCodeUpdated: 'Access code updated',
382+
accessCodeDeleted: 'Access code is being removed',
285383
}

0 commit comments

Comments
 (0)