@seamapi/react 4.5.0 → 4.7.0
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 +11560 -9368
- package/dist/elements.js.map +1 -1
- package/dist/index.css +255 -3
- 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/Trash.d.ts +2 -0
- package/lib/icons/Trash.js +5 -0
- package/lib/icons/Trash.js.map +1 -0
- package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +8 -3
- package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +17 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
- package/lib/seam/thermostats/thermostat-device.d.ts +2 -1
- package/lib/seam/thermostats/thermostat-device.js.map +1 -1
- package/lib/seam/thermostats/unit-conversion.d.ts +5 -2
- package/lib/seam/thermostats/unit-conversion.js +5 -2
- package/lib/seam/thermostats/unit-conversion.js.map +1 -1
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.js +55 -0
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.js.map +1 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js +44 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js.map +1 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.js +55 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.js.map +1 -0
- package/lib/ui/Button.d.ts +3 -2
- package/lib/ui/Button.js +12 -4
- package/lib/ui/Button.js.map +1 -1
- package/lib/ui/IconButton.d.ts +5 -2
- package/lib/ui/IconButton.js +2 -2
- package/lib/ui/IconButton.js.map +1 -1
- package/lib/ui/Popover/Popover.d.ts +17 -0
- package/lib/ui/Popover/Popover.js +85 -0
- package/lib/ui/Popover/Popover.js.map +1 -0
- package/lib/ui/Popover/PopoverContentPrompt.d.ts +11 -0
- package/lib/ui/Popover/PopoverContentPrompt.js +12 -0
- package/lib/ui/Popover/PopoverContentPrompt.js.map +1 -0
- package/lib/ui/thermostat/ClimateModeMenu.d.ts +7 -2
- package/lib/ui/thermostat/ClimateModeMenu.js +7 -2
- package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
- package/lib/ui/thermostat/ClimatePreset.d.ts +8 -0
- package/lib/ui/thermostat/ClimatePreset.js +141 -0
- package/lib/ui/thermostat/ClimatePreset.js.map +1 -0
- package/lib/ui/thermostat/ClimatePresets.d.ts +9 -0
- package/lib/ui/thermostat/ClimatePresets.js +72 -0
- package/lib/ui/thermostat/ClimatePresets.js.map +1 -0
- package/lib/ui/thermostat/FanModeMenu.d.ts +3 -1
- package/lib/ui/thermostat/FanModeMenu.js +5 -2
- package/lib/ui/thermostat/FanModeMenu.js.map +1 -1
- package/lib/ui/thermostat/ThermostatCard.d.ts +1 -0
- package/lib/ui/thermostat/ThermostatCard.js +4 -2
- package/lib/ui/thermostat/ThermostatCard.js.map +1 -1
- package/lib/ui/types.d.ts +3 -3
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -2
- package/src/lib/icons/Trash.tsx +28 -0
- package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +50 -34
- package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +52 -1
- package/src/lib/seam/thermostats/thermostat-device.ts +4 -0
- package/src/lib/seam/thermostats/unit-conversion.ts +12 -2
- package/src/lib/seam/thermostats/use-create-thermostat-climate-preset.ts +101 -0
- package/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts +84 -0
- package/src/lib/seam/thermostats/use-update-thermostat-climate-preset.ts +103 -0
- package/src/lib/ui/Button.tsx +20 -3
- package/src/lib/ui/IconButton.tsx +19 -2
- package/src/lib/ui/Popover/Popover.tsx +168 -0
- package/src/lib/ui/Popover/PopoverContentPrompt.tsx +58 -0
- package/src/lib/ui/thermostat/ClimateModeMenu.tsx +33 -1
- package/src/lib/ui/thermostat/ClimatePreset.tsx +373 -0
- package/src/lib/ui/thermostat/ClimatePresets.tsx +235 -0
- package/src/lib/ui/thermostat/FanModeMenu.tsx +20 -2
- package/src/lib/ui/thermostat/ThermostatCard.tsx +10 -4
- package/src/lib/ui/types.ts +3 -3
- package/src/lib/version.ts +1 -1
- package/src/styles/_buttons.scss +56 -2
- package/src/styles/_main.scss +2 -0
- package/src/styles/_popover.scss +46 -0
- package/src/styles/_spinner.scss +1 -1
- package/src/styles/_thermostat.scss +154 -2
|
@@ -14,6 +14,7 @@ import { useHeatCoolThermostat } from 'lib/seam/thermostats/use-heat-cool-thermo
|
|
|
14
14
|
import { useHeatThermostat } from 'lib/seam/thermostats/use-heat-thermostat.js'
|
|
15
15
|
import { useSetThermostatFanMode } from 'lib/seam/thermostats/use-set-thermostat-fan-mode.js'
|
|
16
16
|
import { useSetThermostatOff } from 'lib/seam/thermostats/use-set-thermostat-off.js'
|
|
17
|
+
import { Button } from 'lib/ui/Button.js'
|
|
17
18
|
import { AccordionRow } from 'lib/ui/layout/AccordionRow.js'
|
|
18
19
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
19
20
|
import { DetailRow } from 'lib/ui/layout/DetailRow.js'
|
|
@@ -21,6 +22,7 @@ import { DetailSection } from 'lib/ui/layout/DetailSection.js'
|
|
|
21
22
|
import { DetailSectionGroup } from 'lib/ui/layout/DetailSectionGroup.js'
|
|
22
23
|
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
|
|
23
24
|
import { ClimateModeMenu } from 'lib/ui/thermostat/ClimateModeMenu.js'
|
|
25
|
+
import { ClimatePresets } from 'lib/ui/thermostat/ClimatePresets.js'
|
|
24
26
|
import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
|
|
25
27
|
import { FanModeMenu } from 'lib/ui/thermostat/FanModeMenu.js'
|
|
26
28
|
import { TemperatureControlGroup } from 'lib/ui/thermostat/TemperatureControlGroup.js'
|
|
@@ -40,16 +42,37 @@ export function ThermostatDeviceDetails({
|
|
|
40
42
|
className,
|
|
41
43
|
onEditName,
|
|
42
44
|
}: ThermostatDeviceDetailsProps): JSX.Element | null {
|
|
45
|
+
const [temperatureUnit, setTemperatureUnit] = useState<
|
|
46
|
+
'fahrenheit' | 'celsius'
|
|
47
|
+
>('fahrenheit')
|
|
48
|
+
const [climateSettingsVisible, setClimateSettingsVisible] = useState(false)
|
|
49
|
+
|
|
43
50
|
if (device == null) {
|
|
44
51
|
return null
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
if (climateSettingsVisible) {
|
|
55
|
+
return (
|
|
56
|
+
<ClimatePresets
|
|
57
|
+
device={device}
|
|
58
|
+
temperatureUnit={temperatureUnit}
|
|
59
|
+
onBack={() => {
|
|
60
|
+
setClimateSettingsVisible(false)
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
return (
|
|
48
67
|
<div className={classNames('seam-device-details', className)}>
|
|
49
68
|
<ContentHeader title={t.thermostat} onBack={onBack} />
|
|
50
69
|
|
|
51
70
|
<div className='seam-body'>
|
|
52
|
-
<ThermostatCard
|
|
71
|
+
<ThermostatCard
|
|
72
|
+
onTemperatureUnitChange={setTemperatureUnit}
|
|
73
|
+
device={device}
|
|
74
|
+
onEditName={onEditName}
|
|
75
|
+
/>
|
|
53
76
|
|
|
54
77
|
<div className='seam-thermostat-device-details'>
|
|
55
78
|
<DetailSectionGroup>
|
|
@@ -58,6 +81,12 @@ export function ThermostatDeviceDetails({
|
|
|
58
81
|
tooltipContent={t.currentSettingsTooltip}
|
|
59
82
|
>
|
|
60
83
|
<ClimateSettingRow device={device} />
|
|
84
|
+
<ClimatePresetRow
|
|
85
|
+
onClickManage={() => {
|
|
86
|
+
setClimateSettingsVisible(true)
|
|
87
|
+
}}
|
|
88
|
+
device={device}
|
|
89
|
+
/>
|
|
61
90
|
<FanModeRow device={device} />
|
|
62
91
|
</DetailSection>
|
|
63
92
|
|
|
@@ -299,16 +328,38 @@ function ClimateSettingRow({
|
|
|
299
328
|
)
|
|
300
329
|
}
|
|
301
330
|
|
|
331
|
+
interface ClimatePresetRowProps {
|
|
332
|
+
device: ThermostatDevice
|
|
333
|
+
onClickManage: () => void
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function ClimatePresetRow({
|
|
337
|
+
device,
|
|
338
|
+
onClickManage,
|
|
339
|
+
}: ClimatePresetRowProps): JSX.Element {
|
|
340
|
+
return (
|
|
341
|
+
<DetailRow label={t.climatePresets}>
|
|
342
|
+
<Button onClick={onClickManage}>
|
|
343
|
+
{t.manageNPresets(
|
|
344
|
+
(device.properties.available_climate_presets ?? []).length
|
|
345
|
+
)}
|
|
346
|
+
</Button>
|
|
347
|
+
</DetailRow>
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
302
351
|
const t = {
|
|
303
352
|
thermostat: 'Thermostat',
|
|
304
353
|
currentSettings: 'Current settings',
|
|
305
354
|
currentSettingsTooltip:
|
|
306
355
|
'These are the settings currently on the device. If you change them here, they change on the device.',
|
|
307
356
|
climate: 'Climate',
|
|
357
|
+
climatePresets: 'Climate presets',
|
|
308
358
|
fanMode: 'Fan mode',
|
|
309
359
|
none: 'None',
|
|
310
360
|
fanModeSuccess: 'Successfully updated fan mode!',
|
|
311
361
|
fanModeError: 'Error updating fan mode. Please try again.',
|
|
312
362
|
climateSettingError: 'Error updating climate setting. Please try again.',
|
|
313
363
|
saved: 'Saved',
|
|
364
|
+
manageNPresets: (n: number) => `Manage (${n} Preset${n <= 1 ? '' : 's'})`,
|
|
314
365
|
}
|
|
@@ -13,6 +13,7 @@ export type ThermostatDevice = Omit<Device, 'properties'> & {
|
|
|
13
13
|
| 'available_hvac_mode_settings'
|
|
14
14
|
| 'fan_mode_setting'
|
|
15
15
|
| 'current_climate_setting'
|
|
16
|
+
| 'available_climate_presets'
|
|
16
17
|
>
|
|
17
18
|
>
|
|
18
19
|
}
|
|
@@ -36,3 +37,6 @@ export interface ClimateSetting {
|
|
|
36
37
|
export const isThermostatDevice = (
|
|
37
38
|
device: Device
|
|
38
39
|
): device is ThermostatDevice => 'is_fan_running' in device.properties
|
|
40
|
+
|
|
41
|
+
export type ThermostatClimatePreset =
|
|
42
|
+
ThermostatDevice['properties']['available_climate_presets'][number]
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Device } from '@seamapi/types/connect'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type ConversionReturn<T> = T extends NonNullable<number> ? number : T
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const celsiusToFahrenheit = <T>(t: T): ConversionReturn<T> =>
|
|
6
|
+
(typeof t === 'number' ? (t * 9) / 5 + 32 : t) as ConversionReturn<T>
|
|
7
|
+
|
|
8
|
+
export const fahrenheitToCelsius = <T>(t: T): ConversionReturn<T> =>
|
|
9
|
+
(typeof t === 'number' ? (t - 32) * (5 / 9) : t) as ConversionReturn<T>
|
|
6
10
|
|
|
7
11
|
export const getCoolingSetPointCelsius = (
|
|
8
12
|
variables: {
|
|
@@ -87,3 +91,9 @@ export const getHeatingSetPointFahrenheit = (
|
|
|
87
91
|
undefined
|
|
88
92
|
)
|
|
89
93
|
}
|
|
94
|
+
|
|
95
|
+
export function getTemperatureUnitSymbol(
|
|
96
|
+
type: 'fahrenheit' | 'celsius'
|
|
97
|
+
): string {
|
|
98
|
+
return type === 'fahrenheit' ? '°F' : '°C'
|
|
99
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsCreateClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ThermostatClimatePreset,
|
|
13
|
+
ThermostatDevice,
|
|
14
|
+
} from 'lib/seam/thermostats/thermostat-device.js'
|
|
15
|
+
import { fahrenheitToCelsius } from 'lib/seam/thermostats/unit-conversion.js'
|
|
16
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
17
|
+
|
|
18
|
+
export type UseCreateThermostatClimatePresetParams = never
|
|
19
|
+
export type UseCreateThermostatClimatePresetData = undefined
|
|
20
|
+
|
|
21
|
+
export type UseCreateThermostatClimatePresetVariables =
|
|
22
|
+
ThermostatsCreateClimatePresetBody
|
|
23
|
+
|
|
24
|
+
export function useCreateThermostatClimatePreset(): UseMutationResult<
|
|
25
|
+
UseCreateThermostatClimatePresetData,
|
|
26
|
+
SeamHttpApiError,
|
|
27
|
+
UseCreateThermostatClimatePresetVariables
|
|
28
|
+
> {
|
|
29
|
+
const { client } = useSeamClient()
|
|
30
|
+
const queryClient = useQueryClient()
|
|
31
|
+
|
|
32
|
+
return useMutation<
|
|
33
|
+
UseCreateThermostatClimatePresetData,
|
|
34
|
+
SeamHttpApiError,
|
|
35
|
+
UseCreateThermostatClimatePresetVariables
|
|
36
|
+
>({
|
|
37
|
+
mutationFn: async (variables) => {
|
|
38
|
+
if (client === null) throw new NullSeamClientError()
|
|
39
|
+
await client.thermostats.createClimatePreset(variables)
|
|
40
|
+
},
|
|
41
|
+
onSuccess: (_data, variables) => {
|
|
42
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
43
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
44
|
+
(device) => {
|
|
45
|
+
if (device == null) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return getUpdatedDevice(device, variables)
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
54
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
55
|
+
(devices): ThermostatDevice[] => {
|
|
56
|
+
if (devices == null) {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return devices.map((device) => {
|
|
61
|
+
if (device.device_id === variables.device_id) {
|
|
62
|
+
return getUpdatedDevice(device, variables)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return device
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const getUpdatedDevice = (
|
|
74
|
+
device: ThermostatDevice,
|
|
75
|
+
variables: UseCreateThermostatClimatePresetVariables
|
|
76
|
+
): ThermostatDevice => {
|
|
77
|
+
const preset: ThermostatClimatePreset = {
|
|
78
|
+
...variables,
|
|
79
|
+
cooling_set_point_celsius: fahrenheitToCelsius(
|
|
80
|
+
variables.cooling_set_point_fahrenheit
|
|
81
|
+
),
|
|
82
|
+
heating_set_point_celsius: fahrenheitToCelsius(
|
|
83
|
+
variables.heating_set_point_fahrenheit
|
|
84
|
+
),
|
|
85
|
+
display_name: variables.name ?? variables.climate_preset_key,
|
|
86
|
+
can_delete: true,
|
|
87
|
+
can_edit: true,
|
|
88
|
+
manual_override_allowed: false,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...device,
|
|
93
|
+
properties: {
|
|
94
|
+
...device.properties,
|
|
95
|
+
available_climate_presets: [
|
|
96
|
+
preset,
|
|
97
|
+
...(device.properties.available_climate_presets ?? []),
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsDeleteClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
|
|
12
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
13
|
+
|
|
14
|
+
export type UseDeleteThermostatClimatePresetParams = never
|
|
15
|
+
|
|
16
|
+
export type UseDeleteThermostatClimatePresetData = undefined
|
|
17
|
+
|
|
18
|
+
export type UseDeleteThermostatClimatePresetVariables =
|
|
19
|
+
ThermostatsDeleteClimatePresetBody
|
|
20
|
+
|
|
21
|
+
export function useDeleteThermostatClimatePreset(): UseMutationResult<
|
|
22
|
+
UseDeleteThermostatClimatePresetData,
|
|
23
|
+
SeamHttpApiError,
|
|
24
|
+
UseDeleteThermostatClimatePresetVariables
|
|
25
|
+
> {
|
|
26
|
+
const { client } = useSeamClient()
|
|
27
|
+
const queryClient = useQueryClient()
|
|
28
|
+
|
|
29
|
+
return useMutation<
|
|
30
|
+
UseDeleteThermostatClimatePresetData,
|
|
31
|
+
SeamHttpApiError,
|
|
32
|
+
UseDeleteThermostatClimatePresetVariables
|
|
33
|
+
>({
|
|
34
|
+
mutationFn: async (variables) => {
|
|
35
|
+
if (client === null) throw new NullSeamClientError()
|
|
36
|
+
await client.thermostats.deleteClimatePreset(variables)
|
|
37
|
+
},
|
|
38
|
+
onSuccess: (_data, variables) => {
|
|
39
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
40
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
41
|
+
(device) => {
|
|
42
|
+
if (device == null) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return getUpdatedDevice(device, variables)
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
51
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
52
|
+
(devices): ThermostatDevice[] => {
|
|
53
|
+
if (devices == null) {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return devices.map((device) => {
|
|
58
|
+
if (device.device_id === variables.device_id) {
|
|
59
|
+
return getUpdatedDevice(device, variables)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return device
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getUpdatedDevice(
|
|
71
|
+
device: ThermostatDevice,
|
|
72
|
+
variables: UseDeleteThermostatClimatePresetVariables
|
|
73
|
+
): ThermostatDevice {
|
|
74
|
+
return {
|
|
75
|
+
...device,
|
|
76
|
+
properties: {
|
|
77
|
+
...device.properties,
|
|
78
|
+
available_climate_presets:
|
|
79
|
+
device.properties.available_climate_presets.filter((preset) => {
|
|
80
|
+
return preset.climate_preset_key !== variables.climate_preset_key
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsUpdateClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ThermostatClimatePreset,
|
|
13
|
+
ThermostatDevice,
|
|
14
|
+
} from 'lib/seam/thermostats/thermostat-device.js'
|
|
15
|
+
import { fahrenheitToCelsius } from 'lib/seam/thermostats/unit-conversion.js'
|
|
16
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
17
|
+
|
|
18
|
+
export type UseUpdateThermostatClimatePresetParams = never
|
|
19
|
+
export type UseUpdateThermostatClimatePresetData = undefined
|
|
20
|
+
|
|
21
|
+
export type UseUpdateThermostatClimatePresetVariables = Omit<
|
|
22
|
+
ThermostatsUpdateClimatePresetBody,
|
|
23
|
+
'manual_override_allowed'
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
export function useUpdateThermostatClimatePreset(): UseMutationResult<
|
|
27
|
+
UseUpdateThermostatClimatePresetData,
|
|
28
|
+
SeamHttpApiError,
|
|
29
|
+
UseUpdateThermostatClimatePresetVariables
|
|
30
|
+
> {
|
|
31
|
+
const { client } = useSeamClient()
|
|
32
|
+
const queryClient = useQueryClient()
|
|
33
|
+
|
|
34
|
+
return useMutation<
|
|
35
|
+
UseUpdateThermostatClimatePresetData,
|
|
36
|
+
SeamHttpApiError,
|
|
37
|
+
UseUpdateThermostatClimatePresetVariables
|
|
38
|
+
>({
|
|
39
|
+
mutationFn: async (variables) => {
|
|
40
|
+
if (client === null) throw new NullSeamClientError()
|
|
41
|
+
await client.thermostats.createClimatePreset(variables)
|
|
42
|
+
},
|
|
43
|
+
onSuccess: (_data, variables) => {
|
|
44
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
45
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
46
|
+
(device) => {
|
|
47
|
+
if (device == null) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return getUpdatedDevice(device, variables)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
56
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
57
|
+
(devices): ThermostatDevice[] => {
|
|
58
|
+
if (devices == null) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return devices.map((device) => {
|
|
63
|
+
if (device.device_id === variables.device_id) {
|
|
64
|
+
return getUpdatedDevice(device, variables)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return device
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getUpdatedDevice(
|
|
76
|
+
device: ThermostatDevice,
|
|
77
|
+
variables: UseUpdateThermostatClimatePresetVariables
|
|
78
|
+
): ThermostatDevice {
|
|
79
|
+
const preset: ThermostatClimatePreset = {
|
|
80
|
+
...variables,
|
|
81
|
+
cooling_set_point_celsius: fahrenheitToCelsius(
|
|
82
|
+
variables.cooling_set_point_fahrenheit
|
|
83
|
+
),
|
|
84
|
+
heating_set_point_celsius: fahrenheitToCelsius(
|
|
85
|
+
variables.heating_set_point_fahrenheit
|
|
86
|
+
),
|
|
87
|
+
display_name: variables.name ?? variables.climate_preset_key,
|
|
88
|
+
can_delete: true,
|
|
89
|
+
can_edit: true,
|
|
90
|
+
manual_override_allowed: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...device,
|
|
95
|
+
properties: {
|
|
96
|
+
...device.properties,
|
|
97
|
+
available_climate_presets: [
|
|
98
|
+
preset,
|
|
99
|
+
...(device.properties.available_climate_presets ?? []),
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/lib/ui/Button.tsx
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import type { MouseEventHandler, PropsWithChildren } from 'react'
|
|
3
3
|
|
|
4
|
+
import { Spinner } from 'lib/ui/Spinner/Spinner.js'
|
|
5
|
+
|
|
4
6
|
interface ButtonProps extends PropsWithChildren {
|
|
5
|
-
variant?: 'solid' | 'outline' | 'neutral'
|
|
7
|
+
variant?: 'solid' | 'outline' | 'neutral' | 'danger'
|
|
6
8
|
size?: 'small' | 'medium' | 'large'
|
|
7
9
|
type?: 'button' | 'submit'
|
|
8
10
|
disabled?: boolean
|
|
9
11
|
onClick?: MouseEventHandler<HTMLButtonElement>
|
|
10
12
|
className?: string
|
|
11
13
|
onMouseDown?: MouseEventHandler<HTMLButtonElement>
|
|
14
|
+
loading?: boolean
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function Button({
|
|
@@ -20,6 +23,7 @@ export function Button({
|
|
|
20
23
|
className,
|
|
21
24
|
onMouseDown,
|
|
22
25
|
type = 'button',
|
|
26
|
+
loading = false,
|
|
23
27
|
}: ButtonProps): JSX.Element {
|
|
24
28
|
return (
|
|
25
29
|
<button
|
|
@@ -27,15 +31,28 @@ export function Button({
|
|
|
27
31
|
`seam-btn seam-btn-${variant} seam-btn-${size}`,
|
|
28
32
|
{
|
|
29
33
|
'seam-btn-disabled': disabled,
|
|
34
|
+
'seam-btn-loading': loading,
|
|
30
35
|
},
|
|
31
36
|
className
|
|
32
37
|
)}
|
|
33
38
|
disabled={disabled}
|
|
34
|
-
onClick={
|
|
39
|
+
onClick={(e) => {
|
|
40
|
+
if (loading || disabled) {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onClick?.(e)
|
|
46
|
+
}}
|
|
35
47
|
onMouseDown={onMouseDown}
|
|
36
48
|
type={type}
|
|
37
49
|
>
|
|
38
|
-
{children}
|
|
50
|
+
<span className='seam-btn-content'>{children}</span>
|
|
51
|
+
{loading && (
|
|
52
|
+
<div className='seam-btn-loading'>
|
|
53
|
+
<Spinner size='small' />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
39
56
|
</button>
|
|
40
57
|
)
|
|
41
58
|
}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
|
+
import type { Ref } from 'react'
|
|
2
3
|
|
|
3
4
|
import type { ButtonProps } from 'lib/ui/types.js'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export type IconProps = ButtonProps & {
|
|
7
|
+
elRef?: Ref<HTMLButtonElement>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function IconButton({
|
|
11
|
+
className,
|
|
12
|
+
elRef,
|
|
13
|
+
...props
|
|
14
|
+
}: IconProps): JSX.Element {
|
|
6
15
|
return (
|
|
7
|
-
<button
|
|
16
|
+
<button
|
|
17
|
+
{...props}
|
|
18
|
+
ref={elRef}
|
|
19
|
+
className={classNames(
|
|
20
|
+
'seam-icon-btn',
|
|
21
|
+
props.disabled === true && 'seam-icon-btn-disabled',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
8
25
|
)
|
|
9
26
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autoUpdate,
|
|
3
|
+
flip,
|
|
4
|
+
limitShift,
|
|
5
|
+
offset,
|
|
6
|
+
type ReferenceElement,
|
|
7
|
+
shift,
|
|
8
|
+
useFloating,
|
|
9
|
+
} from '@floating-ui/react'
|
|
10
|
+
import {
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type Ref,
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useImperativeHandle,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react'
|
|
20
|
+
import { createPortal } from 'react-dom'
|
|
21
|
+
|
|
22
|
+
import { seamComponentsClassName } from 'lib/seam/SeamProvider.js'
|
|
23
|
+
|
|
24
|
+
export interface PopoverInstance {
|
|
25
|
+
show: () => void
|
|
26
|
+
hide: () => void
|
|
27
|
+
toggle: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type PopoverChildren = (
|
|
31
|
+
params: {
|
|
32
|
+
setRef: (ref: HTMLElement | undefined | null) => void
|
|
33
|
+
} & PopoverInstance
|
|
34
|
+
) => ReactNode
|
|
35
|
+
|
|
36
|
+
export interface PopoverProps {
|
|
37
|
+
children: PopoverChildren
|
|
38
|
+
content: ReactNode | ((instance: PopoverInstance) => ReactNode)
|
|
39
|
+
instanceRef?: Ref<PopoverInstance>
|
|
40
|
+
preventCloseOnClickOutside?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Popover(props: PopoverProps): JSX.Element {
|
|
44
|
+
const { children, content, instanceRef, preventCloseOnClickOutside } = props
|
|
45
|
+
|
|
46
|
+
const [open, setOpen] = useState(false)
|
|
47
|
+
|
|
48
|
+
const { refs, floatingStyles } = useFloating({
|
|
49
|
+
whileElementsMounted: autoUpdate,
|
|
50
|
+
transform: false,
|
|
51
|
+
open,
|
|
52
|
+
onOpenChange: setOpen,
|
|
53
|
+
placement: 'bottom',
|
|
54
|
+
middleware: [
|
|
55
|
+
shift({
|
|
56
|
+
crossAxis: true,
|
|
57
|
+
limiter: limitShift(),
|
|
58
|
+
}),
|
|
59
|
+
flip(),
|
|
60
|
+
offset(5),
|
|
61
|
+
],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const referenceEl = useRef<HTMLElement | null>()
|
|
65
|
+
const floatingEl = useRef<HTMLElement | null>()
|
|
66
|
+
|
|
67
|
+
const setFLoating = useCallback(
|
|
68
|
+
(ref: HTMLElement | null): void => {
|
|
69
|
+
refs.setFloating(ref)
|
|
70
|
+
floatingEl.current = ref
|
|
71
|
+
},
|
|
72
|
+
[refs, floatingEl]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const toggle = useCallback(() => {
|
|
76
|
+
setOpen((value) => !value)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const instance = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
show: () => {
|
|
82
|
+
setOpen(true)
|
|
83
|
+
},
|
|
84
|
+
hide: () => {
|
|
85
|
+
setOpen(false)
|
|
86
|
+
},
|
|
87
|
+
toggle,
|
|
88
|
+
}),
|
|
89
|
+
[toggle]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const setReference = useCallback(
|
|
93
|
+
(ref: ReferenceElement | undefined | null): void => {
|
|
94
|
+
if (!(ref instanceof HTMLElement) || referenceEl.current === ref) return
|
|
95
|
+
|
|
96
|
+
if (referenceEl.current != null) {
|
|
97
|
+
referenceEl.current.removeEventListener('click', toggle)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
refs.setReference(ref)
|
|
101
|
+
ref.addEventListener('click', toggle)
|
|
102
|
+
referenceEl.current = ref
|
|
103
|
+
},
|
|
104
|
+
[toggle, refs]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
useImperativeHandle(instanceRef, () => instance)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Closes the popover when the user clicks outside of it.
|
|
111
|
+
*/
|
|
112
|
+
const windowClickHandler = useCallback((e: MouseEvent): void => {
|
|
113
|
+
const target = e.target as HTMLElement
|
|
114
|
+
|
|
115
|
+
// If the target is the reference element, do nothing.
|
|
116
|
+
if (
|
|
117
|
+
referenceEl.current === target ||
|
|
118
|
+
referenceEl.current?.contains(target) === true
|
|
119
|
+
) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const closest = target.closest('[data-seam-popover]')
|
|
124
|
+
|
|
125
|
+
// Prevents closing if target is floating element, also adds support for nested popovers somehow :)
|
|
126
|
+
if (
|
|
127
|
+
closest != null &&
|
|
128
|
+
referenceEl.current != null &&
|
|
129
|
+
!closest.contains(referenceEl.current)
|
|
130
|
+
) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setOpen(false)
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (preventCloseOnClickOutside === false) return
|
|
140
|
+
|
|
141
|
+
globalThis.addEventListener('click', windowClickHandler)
|
|
142
|
+
}, 0)
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
globalThis.removeEventListener('click', windowClickHandler)
|
|
146
|
+
}
|
|
147
|
+
}, [windowClickHandler, preventCloseOnClickOutside])
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
{children({ setRef: setReference, ...instance })}
|
|
152
|
+
{open &&
|
|
153
|
+
createPortal(
|
|
154
|
+
<div
|
|
155
|
+
className={seamComponentsClassName}
|
|
156
|
+
data-seam-popover=''
|
|
157
|
+
ref={setFLoating}
|
|
158
|
+
style={floatingStyles}
|
|
159
|
+
>
|
|
160
|
+
<div className='seam-popover'>
|
|
161
|
+
{typeof content === 'function' ? content(instance) : content}
|
|
162
|
+
</div>
|
|
163
|
+
</div>,
|
|
164
|
+
globalThis.document.body
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
167
|
+
)
|
|
168
|
+
}
|