Skip to content

Commit 242578b

Browse files
mikewuuseambot
andauthored
feat: support generic lock devices (#669)
* expand isLockDevice to check more properties * conditionally render UI based on availability: online, device property, access code type, etc. * add test assertion * ci: Format code --------- Co-authored-by: Seam Bot <[email protected]>
1 parent 102735e commit 242578b

File tree

8 files changed

+100
-35
lines changed

8 files changed

+100
-35
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export function AccessCodeDetails({
217217
</div>
218218
{(!disableEditAccessCode || !disableDeleteAccessCode) && (
219219
<div className='seam-actions'>
220-
{!disableEditAccessCode && (
220+
{!disableEditAccessCode && !accessCode.is_offline_access_code && (
221221
<Button
222222
size='small'
223223
onClick={handleEdit}
@@ -226,7 +226,7 @@ export function AccessCodeDetails({
226226
{t.editCode}
227227
</Button>
228228
)}
229-
{!disableDeleteAccessCode && (
229+
{!disableDeleteAccessCode && !accessCode.is_offline_access_code && (
230230
<Button
231231
size='small'
232232
onClick={handleDelete}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function Content(props: {
6868
{t.deviceDetails}
6969
</TextButton>
7070
</div>
71-
{!disableLockUnlock && (
71+
{!disableLockUnlock && device.properties.online && (
7272
<Button
7373
onClick={() => {
7474
toggleLock.mutate(device)

src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx

+19-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
44

55
import { compareByCreatedAtDesc } from 'lib/dates.js'
66
import { AddIcon } from 'lib/icons/Add.js'
7+
import { useDevice } from 'lib/index.js'
78
import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
89
import { NestedAccessCodeDetails } from 'lib/seam/components/AccessCodeDetails/AccessCodeDetails.js'
910
import {
@@ -73,6 +74,10 @@ export function AccessCodeTable({
7374
device_id: deviceId,
7475
})
7576

77+
const { device } = useDevice({
78+
device_id: deviceId,
79+
})
80+
7681
const [selectedViewAccessCodeId, setSelectedViewAccessCodeId] = useState<
7782
string | null
7883
>(null)
@@ -128,6 +133,10 @@ export function AccessCodeTable({
128133
}
129134
}, [accessCodeResult])
130135

136+
if (device == null) {
137+
return <></>
138+
}
139+
131140
if (selectedEditAccessCodeId != null) {
132141
return (
133142
<NestedEditAccessCodeForm
@@ -214,14 +223,16 @@ export function AccessCodeTable({
214223
) : (
215224
<div className='seam-fragment' />
216225
)}
217-
{!disableCreateAccessCode && (
218-
<IconButton
219-
onClick={toggleAddAccessCodeForm}
220-
className='seam-add-button'
221-
>
222-
<AddIcon />
223-
</IconButton>
224-
)}
226+
{!disableCreateAccessCode &&
227+
(device.properties.online_access_codes_enabled === true ||
228+
device.can_program_online_access_codes === true) && (
229+
<IconButton
230+
onClick={toggleAddAccessCodeForm}
231+
className='seam-add-button'
232+
>
233+
<AddIcon />
234+
</IconButton>
235+
)}
225236
</div>
226237
<div className='seam-table-header-loading-wrap'>
227238
<LoadingToast

src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx

+27-20
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,12 @@ export function LockDeviceDetails({
9999
<div className='seam-properties'>
100100
<span className='seam-label'>{t.status}:</span>{' '}
101101
<OnlineStatus device={device} />
102-
<span className='seam-label'>{t.power}:</span>{' '}
103-
<BatteryStatusIndicator device={device} />
102+
{device.properties.online && (
103+
<>
104+
<span className='seam-label'>{t.power}:</span>{' '}
105+
<BatteryStatusIndicator device={device} />
106+
</>
107+
)}
104108
<DeviceModel device={device} />
105109
</div>
106110
</div>
@@ -120,25 +124,28 @@ export function LockDeviceDetails({
120124
</div>
121125

122126
<div className='seam-box'>
123-
<div className='seam-content seam-lock-status'>
124-
<div>
125-
<span className='seam-label'>{t.lockStatus}</span>
126-
<span className='seam-value'>{lockStatus}</span>
127-
</div>
128-
<div className='seam-right'>
129-
{!disableLockUnlock &&
130-
device.capabilities_supported.includes('lock') && (
131-
<Button
132-
size='small'
133-
onClick={() => {
134-
toggleLock.mutate(device)
135-
}}
136-
>
137-
{toggleLockLabel}
138-
</Button>
139-
)}
127+
{device.properties.locked && device.properties.online && (
128+
<div className='seam-content seam-lock-status'>
129+
<div>
130+
<span className='seam-label'>{t.lockStatus}</span>
131+
<span className='seam-value'>{lockStatus}</span>
132+
</div>
133+
<div className='seam-right'>
134+
{!disableLockUnlock &&
135+
device.capabilities_supported.includes('lock') && (
136+
<Button
137+
size='small'
138+
onClick={() => {
139+
toggleLock.mutate(device)
140+
}}
141+
>
142+
{toggleLockLabel}
143+
</Button>
144+
)}
145+
</div>
140146
</div>
141-
</div>
147+
)}
148+
142149
<AccessCodeLength
143150
supportedCodeLengths={
144151
device.properties?.supported_code_lengths ?? []

src/lib/seam/components/DeviceTable/DeviceTable.test.tsx

+28-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,34 @@ import { render, screen } from 'fixtures/react.js'
55

66
import { DeviceTable } from './DeviceTable.js'
77

8-
test<ApiTestContext>('DeviceTable', async (ctx) => {
8+
test<ApiTestContext>('DeviceTable renders devices', async (ctx) => {
99
render(<DeviceTable />, ctx)
1010
await screen.findByText('Fake August Lock 1')
1111
})
12+
13+
test<ApiTestContext>('DeviceTable renders generic lock device', async (ctx) => {
14+
const existingDevice = ctx.database.devices[0]
15+
16+
ctx.database.addDevice({
17+
device_id: 'august_generic_lock_device',
18+
device_type: 'august_lock',
19+
name: 'Generic August Device',
20+
display_name: 'Generic August Device',
21+
connected_account_id: existingDevice?.connected_account_id,
22+
can_remotely_unlock: false,
23+
can_remotely_lock: false,
24+
can_program_online_access_codes: true,
25+
properties: {
26+
online: false,
27+
manufacturer: 'august',
28+
name: 'Generic August Device',
29+
},
30+
workspace_id: existingDevice?.workspace_id ?? '',
31+
errors: [],
32+
warnings: [],
33+
custom_metadata: {},
34+
})
35+
36+
render(<DeviceTable />, ctx)
37+
await screen.findByText('Generic August Device')
38+
})

src/lib/seam/locks/lock-device.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,14 @@ export type LockDevice = Omit<Device, 'properties'> & {
55
NonNullable<Required<Pick<Device['properties'], 'locked'>>>
66
}
77

8-
export const isLockDevice = (device: Device): device is LockDevice =>
9-
'locked' in device.properties
8+
export const isLockDevice = (device: Device): device is LockDevice => {
9+
return (
10+
'locked' in device.properties ||
11+
'can_remotely_lock' in device ||
12+
'can_remotely_unlock' in device ||
13+
'can_program_online_access_code' in device ||
14+
'can_program_offline_access_code' in device ||
15+
device.properties.online_access_codes_enabled === true ||
16+
device.properties.offline_access_codes_enabled === true
17+
)
18+
}

src/lib/ui/device/LockStatus.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export function LockStatus(props: LockStatusProps): JSX.Element | null {
1919
},
2020
} = props
2121

22+
if (locked === null) {
23+
return null
24+
}
25+
2226
return (
2327
<div className='seam-lock-status'>
2428
<Content isLocked={locked} />

test/fixtures/api.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import {
22
createFake as createFakeDevicedb,
33
type Fake as FakeDevicedb,
44
} from '@seamapi/fake-devicedb'
5-
import { createFake, type Fake, type Seed } from '@seamapi/fake-seam-connect'
5+
import {
6+
createFake,
7+
type Database,
8+
type Fake,
9+
type Seed,
10+
} from '@seamapi/fake-seam-connect'
611
import { beforeEach } from 'vitest'
712

813
export interface ApiTestContext {
914
endpoint: string
1015
seed: Seed
16+
database: Database
1117
}
1218

1319
beforeEach<ApiTestContext>(async (ctx) => {
@@ -22,6 +28,7 @@ beforeEach<ApiTestContext>(async (ctx) => {
2228

2329
ctx.endpoint = endpoint
2430
ctx.seed = seed
31+
ctx.database = fake.database
2532

2633
return () => {
2734
fake.server?.close()

0 commit comments

Comments
 (0)