@seamapi/react 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/elements.js +4656 -4472
- package/dist/elements.js.map +1 -1
- package/dist/index.css +49 -0
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/icons/Edit.js +2 -4
- package/lib/icons/Edit.js.map +1 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeMainIcon.js +6 -3
- package/lib/seam/components/AccessCodeTable/AccessCodeMainIcon.js.map +1 -1
- package/lib/seam/components/DeviceDetails/DeviceDetails.js +15 -3
- package/lib/seam/components/DeviceDetails/DeviceDetails.js.map +1 -1
- package/lib/seam/components/DeviceDetails/LockDeviceDetails.d.ts +2 -1
- package/lib/seam/components/DeviceDetails/LockDeviceDetails.js +3 -2
- package/lib/seam/components/DeviceDetails/LockDeviceDetails.js.map +1 -1
- package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.d.ts +2 -1
- package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js +3 -2
- package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js.map +1 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.d.ts +2 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +2 -2
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
- package/lib/seam/devices/use-update-device-name.d.ts +8 -0
- package/lib/seam/devices/use-update-device-name.js +43 -0
- package/lib/seam/devices/use-update-device-name.js.map +1 -0
- package/lib/ui/device/EditableDeviceName.d.ts +9 -0
- package/lib/ui/device/EditableDeviceName.js +85 -0
- package/lib/ui/device/EditableDeviceName.js.map +1 -0
- package/lib/ui/thermostat/ThermostatCard.d.ts +2 -1
- package/lib/ui/thermostat/ThermostatCard.js +4 -3
- package/lib/ui/thermostat/ThermostatCard.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/lib/icons/Edit.tsx +6 -19
- package/src/lib/seam/components/AccessCodeTable/AccessCodeMainIcon.tsx +7 -3
- package/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +35 -4
- package/src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx +9 -1
- package/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx +8 -1
- package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +3 -1
- package/src/lib/seam/devices/use-update-device-name.ts +93 -0
- package/src/lib/ui/device/EditableDeviceName.tsx +213 -0
- package/src/lib/ui/thermostat/ThermostatCard.tsx +11 -6
- package/src/lib/version.ts +1 -1
- package/src/styles/_main.scss +2 -0
- package/src/styles/_seam-editable-device-name.scss +62 -0
|
@@ -6,6 +6,7 @@ import { LockDeviceDetails } from 'lib/seam/components/DeviceDetails/LockDeviceD
|
|
|
6
6
|
import { NoiseSensorDeviceDetails } from 'lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js'
|
|
7
7
|
import { ThermostatDeviceDetails } from 'lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js'
|
|
8
8
|
import { useDevice } from 'lib/seam/devices/use-device.js'
|
|
9
|
+
import { useUpdateDeviceName } from 'lib/seam/devices/use-update-device-name.js'
|
|
9
10
|
import { isLockDevice } from 'lib/seam/locks/lock-device.js'
|
|
10
11
|
import { isNoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js'
|
|
11
12
|
import { isThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
|
|
@@ -16,7 +17,6 @@ export interface DeviceDetailsProps extends CommonProps {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const NestedDeviceDetails = withRequiredCommonProps(DeviceDetails)
|
|
19
|
-
|
|
20
20
|
export interface NestedSpecificDeviceDetailsProps
|
|
21
21
|
extends Required<Omit<CommonProps, 'onBack' | 'className'>> {
|
|
22
22
|
onBack: (() => void) | undefined
|
|
@@ -42,6 +42,19 @@ export function DeviceDetails({
|
|
|
42
42
|
device_id: deviceId,
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
const { mutate: setDeviceName } = useUpdateDeviceName({
|
|
46
|
+
device_id: deviceId,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const updateDeviceName = (newName: string): void => {
|
|
50
|
+
if (device != null) {
|
|
51
|
+
setDeviceName({
|
|
52
|
+
device_id: device.device_id,
|
|
53
|
+
name: newName,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
if (device == null) {
|
|
46
59
|
return null
|
|
47
60
|
}
|
|
@@ -60,15 +73,33 @@ export function DeviceDetails({
|
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
if (isLockDevice(device)) {
|
|
63
|
-
return
|
|
76
|
+
return (
|
|
77
|
+
<LockDeviceDetails
|
|
78
|
+
device={device}
|
|
79
|
+
onEditName={updateDeviceName}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
if (isThermostatDevice(device)) {
|
|
67
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
<ThermostatDeviceDetails
|
|
88
|
+
device={device}
|
|
89
|
+
onEditName={updateDeviceName}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
if (isNoiseSensorDevice(device)) {
|
|
71
|
-
return
|
|
96
|
+
return (
|
|
97
|
+
<NoiseSensorDeviceDetails
|
|
98
|
+
device={device}
|
|
99
|
+
onEditName={updateDeviceName}
|
|
100
|
+
{...props}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
return null
|
|
@@ -13,12 +13,14 @@ import { Alerts } from 'lib/ui/Alert/Alerts.js'
|
|
|
13
13
|
import { Button } from 'lib/ui/Button.js'
|
|
14
14
|
import { BatteryStatusIndicator } from 'lib/ui/device/BatteryStatusIndicator.js'
|
|
15
15
|
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
16
|
+
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
|
|
16
17
|
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
|
|
17
18
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
18
19
|
import { useToggle } from 'lib/ui/use-toggle.js'
|
|
19
20
|
|
|
20
21
|
interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps {
|
|
21
22
|
device: LockDevice
|
|
23
|
+
onEditName?: (newName: string) => void | Promise<void>
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function LockDeviceDetails({
|
|
@@ -33,6 +35,7 @@ export function LockDeviceDetails({
|
|
|
33
35
|
disableConnectedAccountInformation,
|
|
34
36
|
onBack,
|
|
35
37
|
className,
|
|
38
|
+
onEditName,
|
|
36
39
|
}: LockDeviceDetailsProps): JSX.Element | null {
|
|
37
40
|
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
|
|
38
41
|
const toggleLock = useToggleLock()
|
|
@@ -95,7 +98,12 @@ export function LockDeviceDetails({
|
|
|
95
98
|
</div>
|
|
96
99
|
<div className='seam-info'>
|
|
97
100
|
<span className='seam-label'>{t.device}</span>
|
|
98
|
-
<
|
|
101
|
+
<EditableDeviceName
|
|
102
|
+
tagName='h4'
|
|
103
|
+
value={device.properties.name}
|
|
104
|
+
className='seam-device-name'
|
|
105
|
+
onEdit={onEditName}
|
|
106
|
+
/>
|
|
99
107
|
<div className='seam-properties'>
|
|
100
108
|
<span className='seam-label'>{t.status}:</span>{' '}
|
|
101
109
|
<OnlineStatus device={device} />
|
|
@@ -6,6 +6,7 @@ import { DeviceInfo } from 'lib/seam/components/DeviceDetails/DeviceInfo.js'
|
|
|
6
6
|
import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js'
|
|
7
7
|
import type { NoiseSensorDevice } from 'lib/seam/noise-sensors/noise-sensor-device.js'
|
|
8
8
|
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
9
|
+
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
|
|
9
10
|
import { NoiseLevelStatus } from 'lib/ui/device/NoiseLevelStatus.js'
|
|
10
11
|
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
|
|
11
12
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
@@ -18,6 +19,7 @@ type TabType = 'details' | 'activity'
|
|
|
18
19
|
interface NoiseSensorDeviceDetailsProps
|
|
19
20
|
extends NestedSpecificDeviceDetailsProps {
|
|
20
21
|
device: NoiseSensorDevice
|
|
22
|
+
onEditName?: (newName: string) => void | Promise<void>
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export function NoiseSensorDeviceDetails({
|
|
@@ -26,6 +28,7 @@ export function NoiseSensorDeviceDetails({
|
|
|
26
28
|
disableResourceIds,
|
|
27
29
|
onBack,
|
|
28
30
|
className,
|
|
31
|
+
onEditName,
|
|
29
32
|
}: NoiseSensorDeviceDetailsProps): JSX.Element | null {
|
|
30
33
|
const [tab, setTab] = useState<TabType>('details')
|
|
31
34
|
|
|
@@ -45,7 +48,11 @@ export function NoiseSensorDeviceDetails({
|
|
|
45
48
|
</div>
|
|
46
49
|
<div className='seam-info'>
|
|
47
50
|
<span className='seam-label'>{t.noiseSensor}</span>
|
|
48
|
-
<
|
|
51
|
+
<EditableDeviceName
|
|
52
|
+
onEdit={onEditName}
|
|
53
|
+
tagName='h4'
|
|
54
|
+
value={device.properties.name}
|
|
55
|
+
/>
|
|
49
56
|
<div className='seam-properties'>
|
|
50
57
|
<span className='seam-label'>{t.status}:</span>{' '}
|
|
51
58
|
<OnlineStatus device={device} />
|
|
@@ -29,6 +29,7 @@ import { ThermostatCard } from 'lib/ui/thermostat/ThermostatCard.js'
|
|
|
29
29
|
interface ThermostatDeviceDetailsProps
|
|
30
30
|
extends NestedSpecificDeviceDetailsProps {
|
|
31
31
|
device: ThermostatDevice
|
|
32
|
+
onEditName?: (newName: string) => void | Promise<void>
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function ThermostatDeviceDetails({
|
|
@@ -37,6 +38,7 @@ export function ThermostatDeviceDetails({
|
|
|
37
38
|
disableConnectedAccountInformation,
|
|
38
39
|
onBack,
|
|
39
40
|
className,
|
|
41
|
+
onEditName,
|
|
40
42
|
}: ThermostatDeviceDetailsProps): JSX.Element | null {
|
|
41
43
|
if (device == null) {
|
|
42
44
|
return null
|
|
@@ -47,7 +49,7 @@ export function ThermostatDeviceDetails({
|
|
|
47
49
|
<ContentHeader title={t.thermostat} onBack={onBack} />
|
|
48
50
|
|
|
49
51
|
<div className='seam-body'>
|
|
50
|
-
<ThermostatCard device={device} />
|
|
52
|
+
<ThermostatCard device={device} onEditName={onEditName} />
|
|
51
53
|
|
|
52
54
|
<div className='seam-thermostat-device-details'>
|
|
53
55
|
<DetailSectionGroup>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DevicesGetParams,
|
|
3
|
+
DevicesUpdateBody,
|
|
4
|
+
SeamHttpApiError,
|
|
5
|
+
} from '@seamapi/http/connect'
|
|
6
|
+
import type { Device } from '@seamapi/types/connect'
|
|
7
|
+
import {
|
|
8
|
+
useMutation,
|
|
9
|
+
type UseMutationResult,
|
|
10
|
+
useQueryClient,
|
|
11
|
+
} from '@tanstack/react-query'
|
|
12
|
+
|
|
13
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
14
|
+
|
|
15
|
+
export type UseUpdateDeviceNameParams = never
|
|
16
|
+
|
|
17
|
+
export type UseUpdateDeviceNameData = undefined
|
|
18
|
+
|
|
19
|
+
export type UseUpdateDeviceNameMutationVariables = Pick<
|
|
20
|
+
DevicesUpdateBody,
|
|
21
|
+
'device_id' | 'name'
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
type MutationError = SeamHttpApiError
|
|
25
|
+
|
|
26
|
+
export function useUpdateDeviceName(
|
|
27
|
+
params: DevicesGetParams
|
|
28
|
+
): UseMutationResult<
|
|
29
|
+
UseUpdateDeviceNameData,
|
|
30
|
+
MutationError,
|
|
31
|
+
UseUpdateDeviceNameMutationVariables
|
|
32
|
+
> {
|
|
33
|
+
const { client } = useSeamClient()
|
|
34
|
+
const queryClient = useQueryClient()
|
|
35
|
+
|
|
36
|
+
return useMutation<
|
|
37
|
+
UseUpdateDeviceNameData,
|
|
38
|
+
MutationError,
|
|
39
|
+
UseUpdateDeviceNameMutationVariables
|
|
40
|
+
>({
|
|
41
|
+
mutationFn: async (variables) => {
|
|
42
|
+
if (client === null) throw new NullSeamClientError()
|
|
43
|
+
await client.devices.update(variables)
|
|
44
|
+
},
|
|
45
|
+
onSuccess: (_data, variables) => {
|
|
46
|
+
queryClient.setQueryData<Device | null>(
|
|
47
|
+
['devices', 'get', params],
|
|
48
|
+
(device) => {
|
|
49
|
+
if (device == null) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return getUpdatedDevice(
|
|
54
|
+
device,
|
|
55
|
+
variables.name ?? device.properties.name
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
queryClient.setQueryData<Device[]>(
|
|
61
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
62
|
+
(devices): Device[] => {
|
|
63
|
+
if (devices == null) {
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return devices.map((device) => {
|
|
68
|
+
if (device.device_id === variables.device_id) {
|
|
69
|
+
return getUpdatedDevice(
|
|
70
|
+
device,
|
|
71
|
+
variables.name ?? device.properties.name
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return device
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const getUpdatedDevice = (device: Device, name: string): Device => {
|
|
84
|
+
const { properties } = device
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
...device,
|
|
88
|
+
properties: {
|
|
89
|
+
...properties,
|
|
90
|
+
name,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import {
|
|
3
|
+
type ChangeEvent,
|
|
4
|
+
type HTMLAttributes,
|
|
5
|
+
type KeyboardEvent,
|
|
6
|
+
type PropsWithChildren,
|
|
7
|
+
useCallback,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
import { CheckIcon } from 'lib/icons/Check.js'
|
|
12
|
+
import { CloseIcon } from 'lib/icons/Close.js'
|
|
13
|
+
import { EditIcon } from 'lib/icons/Edit.js'
|
|
14
|
+
|
|
15
|
+
type EditableDeviceNameProps = {
|
|
16
|
+
onEdit?: (newName: string) => void
|
|
17
|
+
editable?: boolean
|
|
18
|
+
tagName?: string
|
|
19
|
+
value: string
|
|
20
|
+
} & HTMLAttributes<HTMLElement>
|
|
21
|
+
|
|
22
|
+
export function EditableDeviceName({
|
|
23
|
+
onEdit,
|
|
24
|
+
editable = true,
|
|
25
|
+
tagName,
|
|
26
|
+
value,
|
|
27
|
+
...props
|
|
28
|
+
}: EditableDeviceNameProps): JSX.Element {
|
|
29
|
+
const [editing, setEditing] = useState(false)
|
|
30
|
+
const [errorText, setErrorText] = useState<null | string>(null)
|
|
31
|
+
const [currentValue, setCurrentValue] = useState(value)
|
|
32
|
+
const Tag = (tagName ?? 'span') as 'div'
|
|
33
|
+
|
|
34
|
+
const handleCheck = useCallback(() => {
|
|
35
|
+
const fixedName = fixName(currentValue)
|
|
36
|
+
const valid = isValidName(fixedName)
|
|
37
|
+
|
|
38
|
+
if (valid.type === 'error') {
|
|
39
|
+
setErrorText(valid.message)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setEditing(false)
|
|
44
|
+
setCurrentValue(fixedName)
|
|
45
|
+
onEdit?.(fixedName)
|
|
46
|
+
}, [currentValue, onEdit])
|
|
47
|
+
|
|
48
|
+
const handleChange = useCallback(
|
|
49
|
+
(event: ChangeEvent<HTMLInputElement>): void => {
|
|
50
|
+
setCurrentValue(event.target.value)
|
|
51
|
+
setErrorText(null)
|
|
52
|
+
},
|
|
53
|
+
[]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const handleCancel = useCallback(() => {
|
|
57
|
+
setEditing(false)
|
|
58
|
+
setCurrentValue(value)
|
|
59
|
+
setErrorText(null)
|
|
60
|
+
}, [value])
|
|
61
|
+
|
|
62
|
+
const handleInputKeydown = useCallback(
|
|
63
|
+
(e: KeyboardEvent<HTMLInputElement>): void => {
|
|
64
|
+
if (e.repeat) return
|
|
65
|
+
|
|
66
|
+
if (e.key === 'Enter') {
|
|
67
|
+
handleCheck()
|
|
68
|
+
} else if (e.key === 'Escape') {
|
|
69
|
+
handleCancel()
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[handleCheck, handleCancel]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Tag
|
|
77
|
+
{...props}
|
|
78
|
+
className={classNames('seam-editable-device-name', props.className)}
|
|
79
|
+
>
|
|
80
|
+
<NameView
|
|
81
|
+
editing={editing}
|
|
82
|
+
value={currentValue}
|
|
83
|
+
onChange={handleChange}
|
|
84
|
+
onKeyDown={handleInputKeydown}
|
|
85
|
+
errorText={errorText}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
{editable && (
|
|
89
|
+
<span className='seam-editable-device-name-icon-wrapper'>
|
|
90
|
+
<ActionButtons
|
|
91
|
+
editing={editing}
|
|
92
|
+
onEdit={() => {
|
|
93
|
+
setEditing(true)
|
|
94
|
+
}}
|
|
95
|
+
onCancel={handleCancel}
|
|
96
|
+
onCheck={handleCheck}
|
|
97
|
+
/>
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</Tag>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface NameViewProps {
|
|
105
|
+
editing: boolean
|
|
106
|
+
value: string
|
|
107
|
+
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
|
108
|
+
onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void
|
|
109
|
+
errorText?: string | null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function NameView(props: NameViewProps): JSX.Element {
|
|
113
|
+
if (!props.editing) {
|
|
114
|
+
return <span>{props.value}</span>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<span className='seam-editable-device-name-input-wrapper'>
|
|
119
|
+
<input
|
|
120
|
+
type='text'
|
|
121
|
+
defaultValue={props.value}
|
|
122
|
+
onChange={props.onChange}
|
|
123
|
+
onKeyDown={props.onKeyDown}
|
|
124
|
+
ref={(el) => {
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
el?.focus()
|
|
127
|
+
}, 0)
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
{props.errorText != null && (
|
|
132
|
+
<span className='seam-editable-device-name-input-error'>
|
|
133
|
+
{props.errorText}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ActionButtonsProps {
|
|
141
|
+
onEdit: () => void
|
|
142
|
+
onCancel: () => void
|
|
143
|
+
onCheck: () => void
|
|
144
|
+
editing: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ActionButtons(props: ActionButtonsProps): JSX.Element {
|
|
148
|
+
if (props.editing) {
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
<IconButton onClick={props.onCheck}>
|
|
152
|
+
<CheckIcon width='1em' height='1em' viewBox='0 0 24 24' />
|
|
153
|
+
</IconButton>
|
|
154
|
+
<IconButton onClick={props.onCancel}>
|
|
155
|
+
<CloseIcon width='1em' height='1em' viewBox='0 0 24 24' />
|
|
156
|
+
</IconButton>
|
|
157
|
+
</>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<IconButton onClick={props.onEdit}>
|
|
163
|
+
<EditIcon width='1em' height='1em' viewBox='0 0 24 24' />
|
|
164
|
+
</IconButton>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function IconButton(
|
|
169
|
+
props: PropsWithChildren<HTMLAttributes<HTMLButtonElement>>
|
|
170
|
+
): JSX.Element {
|
|
171
|
+
return (
|
|
172
|
+
<button
|
|
173
|
+
{...props}
|
|
174
|
+
className={classNames(
|
|
175
|
+
'seam-editable-device-name-icon-button',
|
|
176
|
+
props.className
|
|
177
|
+
)}
|
|
178
|
+
>
|
|
179
|
+
{props.children}
|
|
180
|
+
</button>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const fixName = (name: string): string => {
|
|
185
|
+
return name.replace(/\s+/g, ' ').trim()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type Result =
|
|
189
|
+
| { type: 'success' }
|
|
190
|
+
| {
|
|
191
|
+
type: 'error'
|
|
192
|
+
message: string
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const isValidName = (name: string): Result => {
|
|
196
|
+
if (name.length < 2) {
|
|
197
|
+
return {
|
|
198
|
+
type: 'error',
|
|
199
|
+
message: 'Name must be at least 2 characters long',
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (name.length > 64) {
|
|
204
|
+
return {
|
|
205
|
+
type: 'error',
|
|
206
|
+
message: 'Name must be at most 64 characters long',
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
type: 'success',
|
|
212
|
+
} as const
|
|
213
|
+
}
|
|
@@ -5,22 +5,24 @@ import { FanIcon } from 'lib/icons/Fan.js'
|
|
|
5
5
|
import { OffIcon } from 'lib/icons/Off.js'
|
|
6
6
|
import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
|
|
7
7
|
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
|
|
8
|
+
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
|
|
8
9
|
import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
|
|
9
10
|
import { Temperature } from 'lib/ui/thermostat/Temperature.js'
|
|
10
11
|
|
|
11
12
|
interface ThermostatCardProps {
|
|
12
13
|
device: ThermostatDevice
|
|
14
|
+
onEditName?: (newName: string) => void
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function ThermostatCard(
|
|
17
|
+
export function ThermostatCard(props: ThermostatCardProps): JSX.Element {
|
|
16
18
|
return (
|
|
17
19
|
<div className='seam-thermostat-card'>
|
|
18
|
-
<Content device={device} />
|
|
20
|
+
<Content device={props.device} onEditName={props.onEditName} />
|
|
19
21
|
</div>
|
|
20
22
|
)
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
function Content(props:
|
|
25
|
+
function Content(props: ThermostatCardProps): JSX.Element | null {
|
|
24
26
|
const { device } = props
|
|
25
27
|
|
|
26
28
|
const [temperatureUnit, setTemperatureUnit] = useState<
|
|
@@ -50,9 +52,12 @@ function Content(props: { device: ThermostatDevice }): JSX.Element | null {
|
|
|
50
52
|
</div>
|
|
51
53
|
<div className='seam-thermostat-card-details'>
|
|
52
54
|
<div className='seam-thermostat-heading-wrap'>
|
|
53
|
-
<
|
|
54
|
-
{device.properties.name}
|
|
55
|
-
|
|
55
|
+
<EditableDeviceName
|
|
56
|
+
value={device.properties.name}
|
|
57
|
+
tagName='h4'
|
|
58
|
+
className='seam-thermostat-card-heading'
|
|
59
|
+
onEdit={props.onEditName}
|
|
60
|
+
/>
|
|
56
61
|
<button
|
|
57
62
|
onClick={toggleTemperatureUnit}
|
|
58
63
|
className='seam-thermostat-temperature-toggle'
|
package/src/lib/version.ts
CHANGED
package/src/styles/_main.scss
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
@use './time-zone-picker';
|
|
30
30
|
@use './tab-set';
|
|
31
31
|
@use './noise-sensor';
|
|
32
|
+
@use './seam-editable-device-name';
|
|
32
33
|
|
|
33
34
|
.seam-components {
|
|
34
35
|
// Reset
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
@include switch.all;
|
|
55
56
|
@include time-zone-picker.all;
|
|
56
57
|
@include tab-set.all;
|
|
58
|
+
@include seam-editable-device-name.all;
|
|
57
59
|
|
|
58
60
|
// Components
|
|
59
61
|
@include device-details.all;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@use './colors';
|
|
2
|
+
|
|
3
|
+
@mixin all {
|
|
4
|
+
.seam-editable-device-name {
|
|
5
|
+
input {
|
|
6
|
+
border: none;
|
|
7
|
+
background-color: transparent;
|
|
8
|
+
font-size: inherit;
|
|
9
|
+
font-weight: inherit;
|
|
10
|
+
letter-spacing: inherit;
|
|
11
|
+
line-height: inherit;
|
|
12
|
+
font-family: inherit;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
border-bottom: 1px dashed currentcolor;
|
|
16
|
+
outline: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&-input-wrapper {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
flex-flow: column nowrap;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&-input-error {
|
|
25
|
+
color: colors.$status-red;
|
|
26
|
+
font-size: 0.8em;
|
|
27
|
+
margin-top: 5px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&-icon-wrapper {
|
|
31
|
+
margin-left: 10px;
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
flex-flow: row nowrap;
|
|
34
|
+
gap: 5px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
&-icon-button {
|
|
38
|
+
display: inline-flex;
|
|
39
|
+
padding: 0;
|
|
40
|
+
margin: 0;
|
|
41
|
+
background: none;
|
|
42
|
+
border: none;
|
|
43
|
+
width: fit-content;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
transform-origin: center;
|
|
46
|
+
border-radius: 2px;
|
|
47
|
+
|
|
48
|
+
path {
|
|
49
|
+
fill: currentcolor;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
background-color: currentcolor;
|
|
54
|
+
box-shadow: 0 0 0 2px currentcolor;
|
|
55
|
+
|
|
56
|
+
path {
|
|
57
|
+
fill: white;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|