diff --git a/src/lib/seam/access-codes/use-access-codes.ts b/src/lib/seam/access-codes/use-access-codes.ts index 82b9560de..5cdaff790 100644 --- a/src/lib/seam/access-codes/use-access-codes.ts +++ b/src/lib/seam/access-codes/use-access-codes.ts @@ -6,7 +6,6 @@ import type { SeamError, } from 'seamapi' -import { compareByCreatedAtDesc } from 'lib/dates.js' import { useSeamClient } from 'lib/seam/use-seam-client.js' import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' @@ -28,8 +27,7 @@ export function useAccessCodes( queryKey: ['access_codes', 'list', normalizedParams], queryFn: async () => { if (client == null) return [] - const accessCodes = await client?.accessCodes.list(normalizedParams) - return accessCodes.sort(compareByCreatedAtDesc) + return await client?.accessCodes.list(normalizedParams) }, }) diff --git a/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts index d195bd590..4dfeade89 100644 --- a/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts +++ b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts @@ -6,6 +6,8 @@ export const name = 'seam-access-code-table' export const props: ElementProps = { deviceId: 'string', + accessCodeFilter: 'function', + accessCodeComparator: 'function', onAccessCodeClick: 'function', preventDefaultOnAccessCodeClick: 'boolean', onBack: 'function', diff --git a/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx index 1daca9151..23c924938 100644 --- a/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +++ b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' +import { compareByCreatedAtDesc } from 'lib/dates.js' import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js' import { CopyIcon } from 'lib/icons/Copy.js' import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js' @@ -26,19 +27,40 @@ import { Title } from 'lib/ui/typography/Title.js' export interface AccessCodeTableProps { deviceId: string + accessCodeFilter?: ( + accessCode: AccessCode, + searchInputValue: string + ) => boolean + accessCodeComparator?: ( + accessCodeA: AccessCode, + accessCodeB: AccessCode + ) => number onAccessCodeClick?: (accessCodeId: string) => void preventDefaultOnAccessCodeClick?: boolean onBack?: () => void className?: string } +type AccessCode = UseAccessCodesData[number] + +const defaultAccessCodeFilter = ( + accessCode: AccessCode, + searchInputValue: string +) => { + const value = searchInputValue.trim() + if (value === '') return true + return new RegExp(value, 'i').test(accessCode.name ?? '') +} + export function AccessCodeTable({ deviceId, onAccessCodeClick = () => {}, preventDefaultOnAccessCodeClick = false, onBack, + accessCodeFilter = defaultAccessCodeFilter, + accessCodeComparator = compareByCreatedAtDesc, className, -}: AccessCodeTableProps): JSX.Element | null { +}: AccessCodeTableProps): JSX.Element { const { accessCodes } = useAccessCodes({ device_id: deviceId, }) @@ -47,7 +69,15 @@ export function AccessCodeTable({ string | null >(null) - const [searchTerm, setSearchTerm] = useState('') + const [searchInputValue, setSearchInputValue] = useState('') + + const filteredAccessCodes = useMemo( + () => + accessCodes + ?.filter((accessCode) => accessCodeFilter(accessCode, searchInputValue)) + ?.sort(accessCodeComparator) ?? [], + [accessCodes, searchInputValue, accessCodeFilter, accessCodeComparator] + ) const handleAccessCodeClick = useCallback( (accessCodeId: string): void => { @@ -74,36 +104,22 @@ export function AccessCodeTable({ ) } - if (accessCodes == null) { - return null - } - - const filteredCodes = accessCodes.filter((accessCode) => { - if (searchTerm === '') { - return true - } - - return new RegExp(searchTerm, 'i').test(accessCode.name ?? '') - }) - - const accessCodeCount = accessCodes.length - return (
- {t.accessCodes} ({accessCodeCount}) + {t.accessCodes} ({filteredAccessCodes.length}) diff --git a/src/lib/seam/components/DeviceTable/DeviceTable.element.ts b/src/lib/seam/components/DeviceTable/DeviceTable.element.ts index 0ccb5fcab..6efe052d6 100644 --- a/src/lib/seam/components/DeviceTable/DeviceTable.element.ts +++ b/src/lib/seam/components/DeviceTable/DeviceTable.element.ts @@ -6,6 +6,8 @@ export const name = 'seam-device-table' export const props: ElementProps = { deviceIds: 'json', + deviceFilter: 'function', + deviceComparator: 'function', onDeviceClick: 'function', preventDefaultOnDeviceClick: 'boolean', onBack: 'function', diff --git a/src/lib/seam/components/DeviceTable/DeviceTable.tsx b/src/lib/seam/components/DeviceTable/DeviceTable.tsx index e87ee0dbc..4a95eb887 100644 --- a/src/lib/seam/components/DeviceTable/DeviceTable.tsx +++ b/src/lib/seam/components/DeviceTable/DeviceTable.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' +import { compareByCreatedAtDesc } from 'lib/dates.js' import { DeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js' import { type DeviceFilter, @@ -19,28 +20,47 @@ import { TableTitle } from 'lib/ui/Table/TableTitle.js' import { SearchTextField } from 'lib/ui/TextField/SearchTextField.js' import { Caption } from 'lib/ui/typography/Caption.js' +type Device = UseDevicesData[number] + export interface DeviceTableProps { deviceIds?: string[] + deviceFilter?: (device: Device, searchInputValue: string) => boolean + deviceComparator?: (deviceA: Device, deviceB: Device) => number onDeviceClick?: (deviceId: string) => void preventDefaultOnDeviceClick?: boolean onBack?: () => void className?: string } +const defaultDeviceFilter = (device: Device, searchInputValue: string) => { + const value = searchInputValue.trim() + if (value === '') return true + return new RegExp(value, 'i').test(device.properties.name ?? '') +} + export function DeviceTable({ deviceIds, onDeviceClick = () => {}, preventDefaultOnDeviceClick = false, onBack, + deviceFilter = defaultDeviceFilter, + deviceComparator = compareByCreatedAtDesc, className, -}: DeviceTableProps = {}): JSX.Element | null { +}: DeviceTableProps = {}): JSX.Element { const { devices, isLoading, isError, error } = useDevices({ device_ids: deviceIds, }) const [selectedDeviceId, setSelectedDeviceId] = useState(null) - - const [searchTerm, setSearchTerm] = useState('') + const [searchInputValue, setSearchInputValue] = useState('') + + const filteredDevices = useMemo( + () => + devices + ?.filter((device) => deviceFilter(device, searchInputValue)) + ?.sort(deviceComparator) ?? [], + [devices, searchInputValue, deviceFilter, deviceComparator] + ) const handleDeviceClick = useCallback( (deviceId: string): void => { @@ -71,31 +91,17 @@ export function DeviceTable({ return

{error?.message}

} - if (devices == null) { - return null - } - - const deviceCount = devices.length - - const filteredDevices = devices.filter((device) => { - if (searchTerm === '') { - return true - } - - return new RegExp(searchTerm, 'i').test(device.properties.name) - }) - return (
- {t.devices} ({deviceCount}) + {t.devices} ({filteredDevices.length}) diff --git a/src/lib/seam/devices/use-devices.ts b/src/lib/seam/devices/use-devices.ts index 5b10e0678..5c88bf4db 100644 --- a/src/lib/seam/devices/use-devices.ts +++ b/src/lib/seam/devices/use-devices.ts @@ -7,7 +7,6 @@ import type { SeamError, } from 'seamapi' -import { compareByCreatedAtDesc } from 'lib/dates.js' import { useSeamClient } from 'lib/seam/use-seam-client.js' import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js' @@ -26,8 +25,7 @@ export function useDevices( queryKey: ['devices', 'list', params], queryFn: async () => { if (client == null) return [] - const devices = await client?.devices.list(params) - return devices.sort(compareByCreatedAtDesc) + return await client?.devices.list(params) }, onSuccess: (devices) => { // Prime cache for each device.