From e9785da2014677c6391ee7a834cae393ff9805d0 Mon Sep 17 00:00:00 2001 From: kadiryazici Date: Mon, 20 Jan 2025 17:41:07 +0300 Subject: [PATCH 01/10] feat: Add edit functionality to DeviceDetails --- assets/icons/edit.svg | 9 +- src/lib/icons/Edit.tsx | 25 +--- .../DeviceDetails/DeviceDetails.tsx | 7 +- .../DeviceDetails/LockDeviceDetails.tsx | 10 +- .../NoiseSensorDeviceDetails.tsx | 10 +- .../SeamEditableDeviceName.tsx | 132 ++++++++++++++++++ src/lib/ui/thermostat/ThermostatCard.tsx | 17 ++- src/styles/_main.scss | 4 +- src/styles/_seam-editable-device-name.scss | 63 +++++++++ 9 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx create mode 100644 src/styles/_seam-editable-device-name.scss diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg index bf4c34678..2964432ac 100644 --- a/assets/icons/edit.svg +++ b/assets/icons/edit.svg @@ -1,10 +1,3 @@ - - - - - - - - + diff --git a/src/lib/icons/Edit.tsx b/src/lib/icons/Edit.tsx index fdf0824ac..4cf999cd1 100644 --- a/src/lib/icons/Edit.tsx +++ b/src/lib/icons/Edit.tsx @@ -12,25 +12,12 @@ export function EditIcon(props: SVGProps): JSX.Element { fill='none' {...props} > - - - - - - + ) } diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index 29e4ef625..2f53c3f53 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -21,6 +21,7 @@ export interface NestedSpecificDeviceDetailsProps extends Required> { onBack: (() => void) | undefined className: string | undefined + onEditName?: (newName: string) => void } export function DeviceDetails({ @@ -60,15 +61,15 @@ export function DeviceDetails({ } if (isLockDevice(device)) { - return + return } if (isThermostatDevice(device)) { - return + return } if (isNoiseSensorDevice(device)) { - return + return } return null diff --git a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx index 5c729ead1..79666a893 100644 --- a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx @@ -6,6 +6,7 @@ import { NestedAccessCodeTable } from 'lib/seam/components/AccessCodeTable/Acces import type { NestedSpecificDeviceDetailsProps } from 'lib/seam/components/DeviceDetails/DeviceDetails.js' import { DeviceInfo } from 'lib/seam/components/DeviceDetails/DeviceInfo.js' import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js' +import { SeamEditableDeviceName } from 'lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.js' import { deviceErrorFilter, deviceWarningFilter } from 'lib/seam/filters.js' import type { LockDevice } from 'lib/seam/locks/lock-device.js' import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js' @@ -19,6 +20,7 @@ import { useToggle } from 'lib/ui/use-toggle.js' interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { device: LockDevice + onEditName?: (newName: string) => void } export function LockDeviceDetails({ @@ -33,6 +35,7 @@ export function LockDeviceDetails({ disableConnectedAccountInformation, onBack, className, + onEditName }: LockDeviceDetailsProps): JSX.Element | null { const [accessCodesOpen, toggleAccessCodesOpen] = useToggle() const toggleLock = useToggleLock() @@ -95,7 +98,12 @@ export function LockDeviceDetails({
{t.device} -

{device.properties.name}

+
{t.status}:{' '} diff --git a/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx index 8e1f298d4..1120e8331 100644 --- a/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import type { NestedSpecificDeviceDetailsProps } from 'lib/seam/components/DeviceDetails/DeviceDetails.js' import { DeviceInfo } from 'lib/seam/components/DeviceDetails/DeviceInfo.js' import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js' +import { SeamEditableDeviceName } from 'lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.js' import type { NoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js' import { DeviceImage } from 'lib/ui/device/DeviceImage.js' import { NoiseLevelStatus } from 'lib/ui/device/NoiseLevelStatus.js' @@ -15,9 +16,9 @@ import { TabSet } from 'lib/ui/TabSet.js' type TabType = 'details' | 'activity' -interface NoiseSensorDeviceDetailsProps - extends NestedSpecificDeviceDetailsProps { - device: NoiseSensorDevice +interface NoiseSensorDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { + device: NoiseSensorDevice, + onEditName?: (newName: string) => void, } export function NoiseSensorDeviceDetails({ @@ -26,6 +27,7 @@ export function NoiseSensorDeviceDetails({ disableResourceIds, onBack, className, + onEditName }: NoiseSensorDeviceDetailsProps): JSX.Element | null { const [tab, setTab] = useState('details') @@ -45,7 +47,7 @@ export function NoiseSensorDeviceDetails({
{t.noiseSensor} -

{device.properties.name}

+
{t.status}:{' '} diff --git a/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx new file mode 100644 index 000000000..401f9b7d0 --- /dev/null +++ b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx @@ -0,0 +1,132 @@ +/* eslint-disable import/no-duplicates */ + +import classNames from "classnames" +import type React from "react" +import { type KeyboardEvent, useCallback, useState } from "react" + +import { CheckIcon } from "lib/icons/Check.js" +import { CloseIcon } from "lib/icons/Close.js" +import { EditIcon } from "lib/icons/Edit.js" + +export type SeamDeviceNameProps = { + onEdit?: (newName: string) => void; + editable?: boolean; + tagName?: string; + value: string; +} & React.HTMLAttributes; + +function IconButton(props: React.PropsWithChildren>): JSX.Element { + return +} + +function fixName(name: string): string { + return name.replace(/\s+/g, " ").trim() +} + +function isValidName(name: string): { type: 'success' } | { type: 'error', message: string } { + if (name.length < 2) { + return { + type: 'error', + message: 'Name must be at least 2 characters long' + } + } + + if (name.length > 64) { + return { + type: 'error', + message: 'Name must be at most 64 characters long' + } + } + + return { + type: 'success', + } as const +} + +export function SeamEditableDeviceName({ onEdit, editable = true, tagName, value, ...props }: SeamDeviceNameProps): JSX.Element { + const [editing, setEditing] = useState(false); + const [errorText, setErrorText] = useState(null) + const [currentValue, setCurrentValue] = useState(value); + const Tag = (tagName ?? "span") as "div"; + + const handleCheck = useCallback(() => { + const fixedName = fixName(currentValue); + const valid = isValidName(fixedName); + + if (valid.type === 'error') { + setErrorText(valid.message); + return; + } + + setEditing(false); + setCurrentValue(fixedName) + onEdit?.(fixedName); + }, [currentValue, onEdit]) + + const handleChange = useCallback((event: React.ChangeEvent): void => { + setCurrentValue(event.target.value); + setErrorText(null); + }, []) + + const handleCancel = useCallback(() => { + setEditing(false); + setCurrentValue(value); + setErrorText(null); + }, [value]) + + const handleInputKeydown = useCallback((e: KeyboardEvent): void => { + if (e.repeat) return; + + if (e.key === "Enter") { + handleCheck(); + } else if (e.key === "Escape") { + handleCancel(); + } + }, [handleCheck, handleCancel]); + + return ( + + {editing + ? ( + + { + setTimeout(() => { + el?.focus(); + }, 0) + }} + /> + + {errorText != null && {errorText}} + + ) : ( + {value} + )} + + + {editable ? ( + editing + ? (<> + + + + + + + ) : ( + { setEditing(true) }}> + + + ) + ) : null} + + + ); + +} \ No newline at end of file diff --git a/src/lib/ui/thermostat/ThermostatCard.tsx b/src/lib/ui/thermostat/ThermostatCard.tsx index 0376d785b..a03f4ce3b 100644 --- a/src/lib/ui/thermostat/ThermostatCard.tsx +++ b/src/lib/ui/thermostat/ThermostatCard.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { FanIcon } from 'lib/icons/Fan.js' import { OffIcon } from 'lib/icons/Off.js' +import { SeamEditableDeviceName } from 'lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.js' import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js' import { DeviceImage } from 'lib/ui/device/DeviceImage.js' import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js' @@ -10,17 +11,18 @@ import { Temperature } from 'lib/ui/thermostat/Temperature.js' interface ThermostatCardProps { device: ThermostatDevice + onEditName?: (newName: string) => void } -export function ThermostatCard({ device }: ThermostatCardProps): JSX.Element { +export function ThermostatCard(props: ThermostatCardProps): JSX.Element { return (
- +
) } -function Content(props: { device: ThermostatDevice }): JSX.Element | null { +function Content(props: ThermostatCardProps): JSX.Element | null { const { device } = props const [temperatureUnit, setTemperatureUnit] = useState< @@ -50,9 +52,12 @@ function Content(props: { device: ThermostatDevice }): JSX.Element | null {
-

- {device.properties.name} -

+
{t.noiseSensor} - +
{t.status}:{' '} diff --git a/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx index 401f9b7d0..1793c51dc 100644 --- a/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx +++ b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx @@ -1,42 +1,54 @@ /* eslint-disable import/no-duplicates */ -import classNames from "classnames" -import type React from "react" -import { type KeyboardEvent, useCallback, useState } from "react" +import classNames from 'classnames' +import type React from 'react' +import { type KeyboardEvent, useCallback, useState } from 'react' -import { CheckIcon } from "lib/icons/Check.js" -import { CloseIcon } from "lib/icons/Close.js" -import { EditIcon } from "lib/icons/Edit.js" +import { CheckIcon } from 'lib/icons/Check.js' +import { CloseIcon } from 'lib/icons/Close.js' +import { EditIcon } from 'lib/icons/Edit.js' export type SeamDeviceNameProps = { - onEdit?: (newName: string) => void; - editable?: boolean; - tagName?: string; - value: string; -} & React.HTMLAttributes; - -function IconButton(props: React.PropsWithChildren>): JSX.Element { - return + onEdit?: (newName: string) => void + editable?: boolean + tagName?: string + value: string +} & React.HTMLAttributes + +function IconButton( + props: React.PropsWithChildren> +): JSX.Element { + return ( + + ) } function fixName(name: string): string { - return name.replace(/\s+/g, " ").trim() + return name.replace(/\s+/g, ' ').trim() } -function isValidName(name: string): { type: 'success' } | { type: 'error', message: string } { +function isValidName( + name: string +): { type: 'success' } | { type: 'error'; message: string } { if (name.length < 2) { return { type: 'error', - message: 'Name must be at least 2 characters long' + message: 'Name must be at least 2 characters long', } } if (name.length > 64) { return { type: 'error', - message: 'Name must be at most 64 characters long' + message: 'Name must be at most 64 characters long', } } @@ -45,88 +57,110 @@ function isValidName(name: string): { type: 'success' } | { type: 'error', messa } as const } -export function SeamEditableDeviceName({ onEdit, editable = true, tagName, value, ...props }: SeamDeviceNameProps): JSX.Element { - const [editing, setEditing] = useState(false); +export function SeamEditableDeviceName({ + onEdit, + editable = true, + tagName, + value, + ...props +}: SeamDeviceNameProps): JSX.Element { + const [editing, setEditing] = useState(false) const [errorText, setErrorText] = useState(null) - const [currentValue, setCurrentValue] = useState(value); - const Tag = (tagName ?? "span") as "div"; + const [currentValue, setCurrentValue] = useState(value) + const Tag = (tagName ?? 'span') as 'div' const handleCheck = useCallback(() => { - const fixedName = fixName(currentValue); - const valid = isValidName(fixedName); + const fixedName = fixName(currentValue) + const valid = isValidName(fixedName) if (valid.type === 'error') { - setErrorText(valid.message); - return; + setErrorText(valid.message) + return } - setEditing(false); + setEditing(false) setCurrentValue(fixedName) - onEdit?.(fixedName); + onEdit?.(fixedName) }, [currentValue, onEdit]) - const handleChange = useCallback((event: React.ChangeEvent): void => { - setCurrentValue(event.target.value); - setErrorText(null); - }, []) + const handleChange = useCallback( + (event: React.ChangeEvent): void => { + setCurrentValue(event.target.value) + setErrorText(null) + }, + [] + ) const handleCancel = useCallback(() => { - setEditing(false); - setCurrentValue(value); - setErrorText(null); + setEditing(false) + setCurrentValue(value) + setErrorText(null) }, [value]) - const handleInputKeydown = useCallback((e: KeyboardEvent): void => { - if (e.repeat) return; + const handleInputKeydown = useCallback( + (e: KeyboardEvent): void => { + if (e.repeat) return - if (e.key === "Enter") { - handleCheck(); - } else if (e.key === "Escape") { - handleCancel(); - } - }, [handleCheck, handleCancel]); + if (e.key === 'Enter') { + handleCheck() + } else if (e.key === 'Escape') { + handleCancel() + } + }, + [handleCheck, handleCancel] + ) return ( - - {editing - ? ( - - { - setTimeout(() => { - el?.focus(); - }, 0) - }} - /> - - {errorText != null && {errorText}} - - ) : ( - {value} - )} - - + + {editing ? ( + + { + setTimeout(() => { + el?.focus() + }, 0) + }} + /> + + {errorText != null && ( + + {errorText} + + )} + + ) : ( + {value} + )} + + {editable ? ( - editing - ? (<> + editing ? ( + <> - + - + - ) : ( - { setEditing(true) }}> - - - ) + + ) : ( + { + setEditing(true) + }} + > + + + ) ) : null} - ); - -} \ No newline at end of file + ) +} diff --git a/src/styles/_main.scss b/src/styles/_main.scss index 24927433c..ff634d820 100644 --- a/src/styles/_main.scss +++ b/src/styles/_main.scss @@ -68,4 +68,4 @@ @include thermostat.all; @include seam-table.all; @include noise-sensor.all; -} \ No newline at end of file +} diff --git a/src/styles/_seam-editable-device-name.scss b/src/styles/_seam-editable-device-name.scss index b74d367f9..96ca2debb 100644 --- a/src/styles/_seam-editable-device-name.scss +++ b/src/styles/_seam-editable-device-name.scss @@ -2,7 +2,6 @@ @mixin all { .seam-editable-device-name { - input { border: none; background-color: transparent; @@ -13,7 +12,7 @@ font-family: inherit; margin: 0; padding: 0; - border-bottom: 1px dashed currentColor; + border-bottom: 1px dashed currentcolor; outline: none; } @@ -47,12 +46,12 @@ border-radius: 2px; path { - fill: currentColor; + fill: currentcolor; } &:hover { - background-color: currentColor; - box-shadow: 0 0 0 2px currentColor; + background-color: currentcolor; + box-shadow: 0 0 0 2px currentcolor; path { fill: white; @@ -60,4 +59,4 @@ } } } -} \ No newline at end of file +} From 945e1e26e9f1d8d4512970a033011e11da80e1a9 Mon Sep 17 00:00:00 2001 From: kadiryazici Date: Mon, 20 Jan 2025 19:18:57 +0300 Subject: [PATCH 03/10] add update call --- .../DeviceDetails/DeviceDetails.tsx | 26 ++++++++++++++----- .../DeviceDetails/ThermostatDeviceDetails.tsx | 8 +++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index ee04b87c0..3054cc50b 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -1,3 +1,4 @@ + import { type CommonProps, withRequiredCommonProps, @@ -9,6 +10,7 @@ import { useDevice } from 'lib/seam/devices/use-device.js' import { isLockDevice } from 'lib/seam/locks/lock-device.js' import { isNoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js' import { isThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js' +import { useSeamClient } from 'lib/seam/use-seam-client.js' import { useComponentTelemetry } from 'lib/telemetry/index.js' export interface DeviceDetailsProps extends CommonProps { @@ -16,12 +18,10 @@ export interface DeviceDetailsProps extends CommonProps { } export const NestedDeviceDetails = withRequiredCommonProps(DeviceDetails) - export interface NestedSpecificDeviceDetailsProps extends Required> { onBack: (() => void) | undefined className: string | undefined - onEditName?: (newName: string) => void } export function DeviceDetails({ @@ -39,10 +39,24 @@ export function DeviceDetails({ }: DeviceDetailsProps): JSX.Element | null { useComponentTelemetry('DeviceDetails') - const { device } = useDevice({ + const { client } = useSeamClient(); + const { device, refetch: refetchDevice } = useDevice({ device_id: deviceId, }) + + const updateDeviceName = async (newName: string): Promise => { + if (client == null) return; + + client.devices.update({ + device_id: deviceId, + name: newName, + }) + .then(async () => await refetchDevice()) + .catch((error) => { console.error(error); }) + + } + if (device == null) { return null } @@ -64,7 +78,7 @@ export function DeviceDetails({ return ( ) @@ -74,7 +88,7 @@ export function DeviceDetails({ return ( ) @@ -84,7 +98,7 @@ export function DeviceDetails({ return ( ) diff --git a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx index 32458e129..f9e084bad 100644 --- a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx @@ -29,6 +29,7 @@ import { ThermostatCard } from 'lib/ui/thermostat/ThermostatCard.js' interface ThermostatDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { device: ThermostatDevice + onEditName?: (newName: string) => void } export function ThermostatDeviceDetails({ @@ -37,6 +38,7 @@ export function ThermostatDeviceDetails({ disableConnectedAccountInformation, onBack, className, + onEditName }: ThermostatDeviceDetailsProps): JSX.Element | null { if (device == null) { return null @@ -47,7 +49,7 @@ export function ThermostatDeviceDetails({
- +
@@ -233,7 +235,7 @@ function ClimateSettingRow({ } } - return () => {} + return () => { } }, [isHeatCoolSuccess, isHeatSuccess, isCoolSuccess, isSetOffSuccess]) return ( @@ -273,7 +275,7 @@ function ClimateSettingRow({ delta={ Number( 'min_heating_cooling_delta_fahrenheit' in device.properties && - device.properties.min_heating_cooling_delta_fahrenheit + device.properties.min_heating_cooling_delta_fahrenheit ) ?? 0 } /> From 7d09ce7f4ffc3836f2a3ad2b727ad27dba33f06e Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Mon, 20 Jan 2025 16:19:50 +0000 Subject: [PATCH 04/10] ci: Format code --- .../DeviceDetails/DeviceDetails.tsx | 20 +++++++++---------- .../DeviceDetails/ThermostatDeviceDetails.tsx | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index 3054cc50b..93ae62b9b 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -1,4 +1,3 @@ - import { type CommonProps, withRequiredCommonProps, @@ -39,22 +38,23 @@ export function DeviceDetails({ }: DeviceDetailsProps): JSX.Element | null { useComponentTelemetry('DeviceDetails') - const { client } = useSeamClient(); + const { client } = useSeamClient() const { device, refetch: refetchDevice } = useDevice({ device_id: deviceId, }) - const updateDeviceName = async (newName: string): Promise => { - if (client == null) return; + if (client == null) return - client.devices.update({ - device_id: deviceId, - name: newName, - }) + client.devices + .update({ + device_id: deviceId, + name: newName, + }) .then(async () => await refetchDevice()) - .catch((error) => { console.error(error); }) - + .catch((error) => { + console.error(error) + }) } if (device == null) { diff --git a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx index f9e084bad..5f66aa356 100644 --- a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx @@ -38,7 +38,7 @@ export function ThermostatDeviceDetails({ disableConnectedAccountInformation, onBack, className, - onEditName + onEditName, }: ThermostatDeviceDetailsProps): JSX.Element | null { if (device == null) { return null @@ -235,7 +235,7 @@ function ClimateSettingRow({ } } - return () => { } + return () => {} }, [isHeatCoolSuccess, isHeatSuccess, isCoolSuccess, isSetOffSuccess]) return ( @@ -275,7 +275,7 @@ function ClimateSettingRow({ delta={ Number( 'min_heating_cooling_delta_fahrenheit' in device.properties && - device.properties.min_heating_cooling_delta_fahrenheit + device.properties.min_heating_cooling_delta_fahrenheit ) ?? 0 } /> From 5b544d89bbb324d397bf50f408ef5f50ad5efed2 Mon Sep 17 00:00:00 2001 From: kadiryazici Date: Mon, 20 Jan 2025 22:05:06 +0300 Subject: [PATCH 05/10] Fix type error --- src/lib/seam/components/DeviceDetails/DeviceDetails.tsx | 1 + src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx | 2 +- .../components/DeviceDetails/NoiseSensorDeviceDetails.tsx | 2 +- .../components/DeviceDetails/ThermostatDeviceDetails.tsx | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index 93ae62b9b..d1c902a99 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -53,6 +53,7 @@ export function DeviceDetails({ }) .then(async () => await refetchDevice()) .catch((error) => { + // eslint-disable-next-line no-console console.error(error) }) } diff --git a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx index 8766c3bfb..fde7d961f 100644 --- a/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx @@ -20,7 +20,7 @@ import { useToggle } from 'lib/ui/use-toggle.js' interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { device: LockDevice - onEditName?: (newName: string) => void + onEditName?: (newName: string) => void | Promise } export function LockDeviceDetails({ diff --git a/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx index 5d4819083..cdf580776 100644 --- a/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx @@ -19,7 +19,7 @@ type TabType = 'details' | 'activity' interface NoiseSensorDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { device: NoiseSensorDevice - onEditName?: (newName: string) => void + onEditName?: (newName: string) => void | Promise } export function NoiseSensorDeviceDetails({ diff --git a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx index 5f66aa356..3a625fc13 100644 --- a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx @@ -29,7 +29,7 @@ import { ThermostatCard } from 'lib/ui/thermostat/ThermostatCard.js' interface ThermostatDeviceDetailsProps extends NestedSpecificDeviceDetailsProps { device: ThermostatDevice - onEditName?: (newName: string) => void + onEditName?: (newName: string) => void | Promise } export function ThermostatDeviceDetails({ @@ -235,7 +235,7 @@ function ClimateSettingRow({ } } - return () => {} + return () => { } }, [isHeatCoolSuccess, isHeatSuccess, isCoolSuccess, isSetOffSuccess]) return ( @@ -275,7 +275,7 @@ function ClimateSettingRow({ delta={ Number( 'min_heating_cooling_delta_fahrenheit' in device.properties && - device.properties.min_heating_cooling_delta_fahrenheit + device.properties.min_heating_cooling_delta_fahrenheit ) ?? 0 } /> From c3bcf26b270e180091d50ff7dec6e3f514761b0b Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Mon, 20 Jan 2025 19:06:03 +0000 Subject: [PATCH 06/10] ci: Format code --- .../seam/components/DeviceDetails/ThermostatDeviceDetails.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx index 3a625fc13..3379bcd1e 100644 --- a/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx @@ -235,7 +235,7 @@ function ClimateSettingRow({ } } - return () => { } + return () => {} }, [isHeatCoolSuccess, isHeatSuccess, isCoolSuccess, isSetOffSuccess]) return ( @@ -275,7 +275,7 @@ function ClimateSettingRow({ delta={ Number( 'min_heating_cooling_delta_fahrenheit' in device.properties && - device.properties.min_heating_cooling_delta_fahrenheit + device.properties.min_heating_cooling_delta_fahrenheit ) ?? 0 } /> From 49a93b64a94511e3168ffc143111c1ae48575055 Mon Sep 17 00:00:00 2001 From: kadiryazici Date: Mon, 20 Jan 2025 23:26:14 +0300 Subject: [PATCH 07/10] Use mutation --- .../DeviceDetails/DeviceDetails.tsx | 23 +++-- src/lib/seam/devices/use-set-device-name.ts | 83 +++++++++++++++++++ 2 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 src/lib/seam/devices/use-set-device-name.ts diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index d1c902a99..314469710 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -6,10 +6,10 @@ import { LockDeviceDetails } from 'lib/seam/components/DeviceDetails/LockDeviceD import { NoiseSensorDeviceDetails } from 'lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js' import { ThermostatDeviceDetails } from 'lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js' import { useDevice } from 'lib/seam/devices/use-device.js' +import { useSetDeviceName } from 'lib/seam/devices/use-set-device-name.js' import { isLockDevice } from 'lib/seam/locks/lock-device.js' import { isNoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js' import { isThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js' -import { useSeamClient } from 'lib/seam/use-seam-client.js' import { useComponentTelemetry } from 'lib/telemetry/index.js' export interface DeviceDetailsProps extends CommonProps { @@ -38,24 +38,21 @@ export function DeviceDetails({ }: DeviceDetailsProps): JSX.Element | null { useComponentTelemetry('DeviceDetails') - const { client } = useSeamClient() - const { device, refetch: refetchDevice } = useDevice({ + const { device } = useDevice({ device_id: deviceId, }) - const updateDeviceName = async (newName: string): Promise => { - if (client == null) return + const { mutate: setDeviceName } = useSetDeviceName({ + device_id: deviceId, + }) - client.devices - .update({ - device_id: deviceId, + const updateDeviceName = (newName: string): void => { + if (device != null) { + setDeviceName({ + device_id: device.device_id, name: newName, }) - .then(async () => await refetchDevice()) - .catch((error) => { - // eslint-disable-next-line no-console - console.error(error) - }) + } } if (device == null) { diff --git a/src/lib/seam/devices/use-set-device-name.ts b/src/lib/seam/devices/use-set-device-name.ts new file mode 100644 index 000000000..e34b99bb0 --- /dev/null +++ b/src/lib/seam/devices/use-set-device-name.ts @@ -0,0 +1,83 @@ +import type { + DevicesGetParams, + DevicesUpdateBody, + SeamHttpApiError, +} from '@seamapi/http/connect' +import type { Device } from '@seamapi/types/connect' +import { + useMutation, + type UseMutationResult, + useQueryClient, +} from '@tanstack/react-query' + +import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' + +export type UseSetDeviceNameParams = never + +export type UseSetDeviceNameData = undefined + +export type UseSetDeviceNameMutationVariables = Pick + +type MutationError = SeamHttpApiError + +export function useSetDeviceName(params: DevicesGetParams): UseMutationResult< + UseSetDeviceNameData, + MutationError, + UseSetDeviceNameMutationVariables +> { + const { client } = useSeamClient() + const queryClient = useQueryClient() + + return useMutation< + UseSetDeviceNameData, + MutationError, + UseSetDeviceNameMutationVariables + >({ + mutationFn: async (variables) => { + if (client === null) throw new NullSeamClientError() + await client.devices.update(variables) + }, + onSuccess: (_data, variables) => { + + queryClient.setQueryData( + ['devices', 'get', params], + (device) => { + if (device == null) { + return + } + + return getUpdatedDevice(device, variables.name ?? device.properties.name) + } + ) + + queryClient.setQueryData( + ['devices', 'list', { device_id: variables.device_id }], + (devices): Device[] => { + if (devices == null) { + return [] + } + + return devices.map((device) => { + if (device.device_id === variables.device_id) { + return getUpdatedDevice(device, variables.name ?? device.properties.name) + } + + return device + }) + } + ) + }, + }) +} + +const getUpdatedDevice = (device: Device, name: string): Device => { + const { properties } = device + + return { + ...device, + properties: { + ...properties, + name, + }, + } +} \ No newline at end of file From c6723fb9947ac8067a3f946e23894a5f8872f543 Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Mon, 20 Jan 2025 20:27:14 +0000 Subject: [PATCH 08/10] ci: Format code --- src/lib/seam/devices/use-set-device-name.ts | 22 +++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/lib/seam/devices/use-set-device-name.ts b/src/lib/seam/devices/use-set-device-name.ts index e34b99bb0..1774cf3ee 100644 --- a/src/lib/seam/devices/use-set-device-name.ts +++ b/src/lib/seam/devices/use-set-device-name.ts @@ -16,11 +16,16 @@ export type UseSetDeviceNameParams = never export type UseSetDeviceNameData = undefined -export type UseSetDeviceNameMutationVariables = Pick +export type UseSetDeviceNameMutationVariables = Pick< + DevicesUpdateBody, + 'device_id' | 'name' +> type MutationError = SeamHttpApiError -export function useSetDeviceName(params: DevicesGetParams): UseMutationResult< +export function useSetDeviceName( + params: DevicesGetParams +): UseMutationResult< UseSetDeviceNameData, MutationError, UseSetDeviceNameMutationVariables @@ -38,7 +43,6 @@ export function useSetDeviceName(params: DevicesGetParams): UseMutationResult< await client.devices.update(variables) }, onSuccess: (_data, variables) => { - queryClient.setQueryData( ['devices', 'get', params], (device) => { @@ -46,7 +50,10 @@ export function useSetDeviceName(params: DevicesGetParams): UseMutationResult< return } - return getUpdatedDevice(device, variables.name ?? device.properties.name) + return getUpdatedDevice( + device, + variables.name ?? device.properties.name + ) } ) @@ -59,7 +66,10 @@ export function useSetDeviceName(params: DevicesGetParams): UseMutationResult< return devices.map((device) => { if (device.device_id === variables.device_id) { - return getUpdatedDevice(device, variables.name ?? device.properties.name) + return getUpdatedDevice( + device, + variables.name ?? device.properties.name + ) } return device @@ -80,4 +90,4 @@ const getUpdatedDevice = (device: Device, name: string): Device => { name, }, } -} \ No newline at end of file +} From b88a34aa209415b76518c85c7393b69dbe29c7d9 Mon Sep 17 00:00:00 2001 From: kadiryazici Date: Wed, 22 Jan 2025 23:11:25 +0300 Subject: [PATCH 09/10] Update according to reviews --- .../DeviceDetails/DeviceDetails.tsx | 4 +- .../SeamEditableDeviceName.tsx | 144 +++++++++++------- ...vice-name.ts => use-update-device-name.ts} | 16 +- 3 files changed, 100 insertions(+), 64 deletions(-) rename src/lib/seam/devices/{use-set-device-name.ts => use-update-device-name.ts} (85%) diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx index 314469710..eb8d75b6a 100644 --- a/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx @@ -6,7 +6,7 @@ import { LockDeviceDetails } from 'lib/seam/components/DeviceDetails/LockDeviceD import { NoiseSensorDeviceDetails } from 'lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js' import { ThermostatDeviceDetails } from 'lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js' import { useDevice } from 'lib/seam/devices/use-device.js' -import { useSetDeviceName } from 'lib/seam/devices/use-set-device-name.js' +import { useUpdateDeviceName } from 'lib/seam/devices/use-update-device-name.js' import { isLockDevice } from 'lib/seam/locks/lock-device.js' import { isNoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js' import { isThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js' @@ -42,7 +42,7 @@ export function DeviceDetails({ device_id: deviceId, }) - const { mutate: setDeviceName } = useSetDeviceName({ + const { mutate: setDeviceName } = useUpdateDeviceName({ device_id: deviceId, }) diff --git a/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx index 1793c51dc..350516ca1 100644 --- a/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx +++ b/src/lib/seam/components/SeamEditableDeviceName/SeamEditableDeviceName.tsx @@ -1,8 +1,5 @@ -/* eslint-disable import/no-duplicates */ - import classNames from 'classnames' -import type React from 'react' -import { type KeyboardEvent, useCallback, useState } from 'react' +import { type ChangeEvent, type HTMLAttributes, type KeyboardEvent, type PropsWithChildren, useCallback, useState } from 'react'; import { CheckIcon } from 'lib/icons/Check.js' import { CloseIcon } from 'lib/icons/Close.js' @@ -13,10 +10,10 @@ export type SeamDeviceNameProps = { editable?: boolean tagName?: string value: string -} & React.HTMLAttributes +} & HTMLAttributes function IconButton( - props: React.PropsWithChildren> + props: PropsWithChildren> ): JSX.Element { return (