Skip to content

Commit dfe64fe

Browse files
committed
feat: Add error messages to Thermostat Climate Preset Management
1 parent dec0e92 commit dfe64fe

File tree

2 files changed

+189
-74
lines changed

2 files changed

+189
-74
lines changed

src/lib/ui/thermostat/ClimatePreset.tsx

Lines changed: 97 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Button } from 'lib/ui/Button.js'
2121
import { FormField } from 'lib/ui/FormField.js'
2222
import { InputLabel } from 'lib/ui/InputLabel.js'
2323
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
24+
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
2425
import { TextField } from 'lib/ui/TextField/TextField.js'
2526
import { ClimateModeMenu } from 'lib/ui/thermostat/ClimateModeMenu.js'
2627
import { FanModeMenu } from 'lib/ui/thermostat/FanModeMenu.js'
@@ -265,12 +266,35 @@ interface CreateFormProps {
265266
onComplete: () => void
266267
}
267268

269+
/**
270+
* @see https://github.com/seamapi/seam-connect/blob/a0b081e086e6f031ad3bcac6dd3f27c46e5b54e5/pages/api/public/thermostats/create_climate_preset.ts#L78-L81
271+
**/
272+
const CreateClimatePresetErrorCodes = {
273+
DeviceNotFound: 'device_not_found',
274+
ClimatePresetExists: 'climate_preset_exists',
275+
}
276+
268277
function CreateForm({ device, onComplete }: CreateFormProps): JSX.Element {
269-
const mutation = useCreateThermostatClimatePreset()
278+
const { mutate, isError, error, isPending } =
279+
useCreateThermostatClimatePreset()
280+
281+
const errorMessage = useMemo(() => {
282+
if (!isError) return ''
283+
284+
if (error?.code === CreateClimatePresetErrorCodes.ClimatePresetExists) {
285+
return t.keyAlreadyExists
286+
}
287+
288+
if (error?.code === CreateClimatePresetErrorCodes.DeviceNotFound) {
289+
return t.deviceNotFound
290+
}
291+
292+
return t.unknownErrorOccured
293+
}, [error, isError])
270294

271295
const onSubmit = useCallback(
272296
(values: PresetFormProps['defaultValues']) => {
273-
mutation.mutate(
297+
mutate(
274298
{
275299
climate_preset_key: values.key,
276300
device_id: device.device_id,
@@ -285,24 +309,33 @@ function CreateForm({ device, onComplete }: CreateFormProps): JSX.Element {
285309
{ onSuccess: onComplete }
286310
)
287311
},
288-
[device, mutation, onComplete]
312+
[device, mutate, onComplete]
289313
)
290314

291315
return (
292-
<PresetForm
293-
defaultValues={{
294-
key: '',
295-
coolPoint: 60,
296-
heatPoint: 80,
297-
name: '',
298-
hvacMode: 'off',
299-
fanMode: 'auto',
300-
}}
301-
device={device}
302-
loading={mutation.isPending}
303-
onSubmit={onSubmit}
304-
withKeyField
305-
/>
316+
<>
317+
<Snackbar
318+
message={errorMessage}
319+
variant='error'
320+
visible={isError}
321+
automaticVisibility
322+
/>
323+
324+
<PresetForm
325+
defaultValues={{
326+
key: '',
327+
coolPoint: 60,
328+
heatPoint: 80,
329+
name: '',
330+
hvacMode: 'off',
331+
fanMode: 'auto',
332+
}}
333+
device={device}
334+
loading={isPending}
335+
onSubmit={onSubmit}
336+
withKeyField
337+
/>
338+
</>
306339
)
307340
}
308341

@@ -312,16 +345,26 @@ interface UpdateFormProps {
312345
preset: ThermostatClimatePreset
313346
}
314347

348+
/**
349+
* @see https://github.com/seamapi/seam-connect/blob/a0b081e086e6f031ad3bcac6dd3f27c46e5b54e5/pages/api/public/thermostats/update_climate_preset.ts
350+
**/
351+
const UpdateClimatePresetErrorCodes = {
352+
DeviceNotFound: 'device_not_found',
353+
ClimatePresetNotFound: 'climate_preset_not_found',
354+
}
355+
315356
function UpdateForm({
316357
device,
317358
onComplete,
318359
preset,
319360
}: UpdateFormProps): JSX.Element {
320-
const mutation = useUpdateThermostatClimatePreset()
361+
const { mutate, isError, error, isPending } =
362+
useUpdateThermostatClimatePreset()
363+
321364
const defaultValues = useMemo<PresetFormProps['defaultValues']>(
322365
() => ({
323-
coolPoint: preset.cooling_set_point_fahrenheit ?? 60,
324-
heatPoint: preset.heating_set_point_fahrenheit ?? 80,
366+
coolPoint: preset.cooling_set_point_fahrenheit,
367+
heatPoint: preset.heating_set_point_fahrenheit,
325368
name: preset.display_name,
326369
hvacMode: preset.hvac_mode_setting,
327370
fanMode: preset.fan_mode_setting,
@@ -332,7 +375,7 @@ function UpdateForm({
332375

333376
const onSubmit = useCallback(
334377
(values: PresetFormProps['defaultValues']) => {
335-
mutation.mutate(
378+
mutate(
336379
{
337380
climate_preset_key: values.key,
338381
device_id: device.device_id,
@@ -347,27 +390,53 @@ function UpdateForm({
347390
{ onSuccess: onComplete }
348391
)
349392
},
350-
[device, mutation, onComplete]
393+
[device, mutate, onComplete]
351394
)
352395

396+
const errorMessage = useMemo(() => {
397+
if (!isError) return ''
398+
399+
if (error?.code === UpdateClimatePresetErrorCodes.ClimatePresetNotFound) {
400+
return t.climatePresetNotFound
401+
}
402+
403+
if (error?.code === UpdateClimatePresetErrorCodes.DeviceNotFound) {
404+
return t.deviceNotFound
405+
}
406+
407+
return t.unknownErrorOccured
408+
}, [error, isError])
409+
353410
return (
354-
<PresetForm
355-
defaultValues={defaultValues}
356-
device={device}
357-
loading={mutation.isPending}
358-
onSubmit={onSubmit}
359-
/>
411+
<>
412+
<Snackbar
413+
message={errorMessage}
414+
variant='error'
415+
visible={isError}
416+
automaticVisibility
417+
/>
418+
419+
<PresetForm
420+
defaultValues={defaultValues}
421+
device={device}
422+
loading={isPending}
423+
onSubmit={onSubmit}
424+
/>
425+
</>
360426
)
361427
}
362428

363429
const t = {
364430
keyAlreadyExists: 'Climate Preset with this key already exists.',
365431
keyCannotContainSpaces: 'Climate Preset key cannot contain spaces.',
432+
climatePresetNotFound: 'Climate Preset not found.',
433+
deviceNotFound: 'Device not found.',
366434
nameField: 'Display Name (Optional)',
367435
fanModeField: 'Fan Mode',
368436
hvacModeField: 'HVAC Mode',
369437
heatCoolField: 'Heat / Cool',
370438
delete: 'Delete',
371439
save: 'Save',
372440
crateNewPreset: 'Create New Climate Preset',
441+
unknownErrorOccured: 'An unknown error occurred.',
373442
}

src/lib/ui/thermostat/ClimatePresets.tsx

Lines changed: 92 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classNames from 'classnames'
2-
import { type HTMLAttributes, type ReactNode, useState } from 'react'
2+
import { type HTMLAttributes, type ReactNode, useMemo, useState } from 'react'
33

44
import { AddIcon } from 'lib/icons/Add.js'
55
import { EditIcon } from 'lib/icons/Edit.js'
@@ -18,6 +18,7 @@ import { IconButton } from 'lib/ui/IconButton.js'
1818
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
1919
import { Popover } from 'lib/ui/Popover/Popover.js'
2020
import { PopoverContentPrompt } from 'lib/ui/Popover/PopoverContentPrompt.js'
21+
import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
2122
import { Spinner } from 'lib/ui/Spinner/Spinner.js'
2223
import { ClimatePreset } from 'lib/ui/thermostat/ClimatePreset.js'
2324

@@ -29,6 +30,15 @@ interface ClimatePresetsManagement {
2930

3031
const CreateNewPresetSymbol = Symbol('CreateNewPreset')
3132

33+
/**
34+
* @see https://github.com/seamapi/seam-connect/blob/a0b081e086e6f031ad3bcac6dd3f27c46e5b54e5/pages/api/public/thermostats/delete_climate_preset.ts
35+
*/
36+
const DeleteClimatePresetErrorCodes = {
37+
ClimatePresetNotFound: 'climate_preset_not_found',
38+
DeviceNotFound: 'device_not_found',
39+
ClimatePresetIsScheduled: 'climate_preset_is_scheduled',
40+
}
41+
3242
export function ClimatePresets(props: ClimatePresetsManagement): JSX.Element {
3343
const { device, onBack } = props
3444

@@ -40,7 +50,29 @@ export function ClimatePresets(props: ClimatePresetsManagement): JSX.Element {
4050
climatePresetKeySelectedForDeletion,
4151
setClimatePresetKeySelectedForDeletion,
4252
] = useState<ThermostatClimatePreset['climate_preset_key'] | null>(null)
43-
const deleteMutation = useDeleteThermostatClimatePreset()
53+
54+
const { mutate, isError, error, isPending } =
55+
useDeleteThermostatClimatePreset()
56+
57+
const errorMessage = useMemo(() => {
58+
if (!isError) return ''
59+
60+
if (error?.code === DeleteClimatePresetErrorCodes.ClimatePresetNotFound) {
61+
return t.climatePresetNotFound
62+
}
63+
64+
if (error?.code === DeleteClimatePresetErrorCodes.DeviceNotFound) {
65+
return t.deviceNotFound
66+
}
67+
68+
if (
69+
error?.code === DeleteClimatePresetErrorCodes.ClimatePresetIsScheduled
70+
) {
71+
return t.climatePresetIsScheduled
72+
}
73+
74+
return t.unknownErrorOccured
75+
}, [error, isError])
4476

4577
if (
4678
selectedClimatePreset != null ||
@@ -62,52 +94,61 @@ export function ClimatePresets(props: ClimatePresetsManagement): JSX.Element {
6294
}
6395

6496
return (
65-
<div className='seam-thermostat-climate-presets'>
66-
<ContentHeader title={t.title} onBack={onBack} />
67-
<div className='seam-thermostat-climate-presets-body'>
68-
<Button
69-
onClick={() => {
70-
setSelectedClimatePreset(CreateNewPresetSymbol)
71-
}}
72-
className='seam-climate-presets-add-button'
73-
>
74-
<AddIcon />
75-
{t.createNew}
76-
</Button>
77-
78-
<div className='seam-thermostat-climate-presets-cards'>
79-
{device.properties.available_climate_presets.map((preset) => (
80-
<PresetCard
81-
onClickEdit={() => {
82-
setSelectedClimatePreset(preset)
83-
}}
84-
onClickDelete={() => {
85-
setClimatePresetKeySelectedForDeletion(
86-
preset.climate_preset_key
87-
)
88-
deleteMutation.mutate({
89-
climate_preset_key: preset.climate_preset_key,
90-
device_id: device.device_id,
91-
})
92-
}}
93-
temperatureUnit={props.temperatureUnit}
94-
preset={preset}
95-
key={preset.climate_preset_key}
96-
deletionLoading={
97-
deleteMutation.isPending &&
98-
climatePresetKeySelectedForDeletion ===
99-
preset.climate_preset_key
100-
}
101-
disabled={
102-
deleteMutation.isPending &&
103-
climatePresetKeySelectedForDeletion !==
104-
preset.climate_preset_key
105-
}
106-
/>
107-
))}
97+
<>
98+
<Snackbar
99+
message={errorMessage}
100+
variant='error'
101+
visible={isError}
102+
automaticVisibility
103+
/>
104+
105+
<div className='seam-thermostat-climate-presets'>
106+
<ContentHeader title={t.title} onBack={onBack} />
107+
<div className='seam-thermostat-climate-presets-body'>
108+
<Button
109+
onClick={() => {
110+
setSelectedClimatePreset(CreateNewPresetSymbol)
111+
}}
112+
className='seam-climate-presets-add-button'
113+
>
114+
<AddIcon />
115+
{t.createNew}
116+
</Button>
117+
118+
<div className='seam-thermostat-climate-presets-cards'>
119+
{device.properties.available_climate_presets.map((preset) => (
120+
<PresetCard
121+
onClickEdit={() => {
122+
setSelectedClimatePreset(preset)
123+
}}
124+
onClickDelete={() => {
125+
setClimatePresetKeySelectedForDeletion(
126+
preset.climate_preset_key
127+
)
128+
mutate({
129+
climate_preset_key: preset.climate_preset_key,
130+
device_id: device.device_id,
131+
})
132+
}}
133+
temperatureUnit={props.temperatureUnit}
134+
preset={preset}
135+
key={preset.climate_preset_key}
136+
deletionLoading={
137+
isPending &&
138+
climatePresetKeySelectedForDeletion ===
139+
preset.climate_preset_key
140+
}
141+
disabled={
142+
isPending &&
143+
climatePresetKeySelectedForDeletion !==
144+
preset.climate_preset_key
145+
}
146+
/>
147+
))}
148+
</div>
108149
</div>
109150
</div>
110-
</div>
151+
</>
111152
)
112153
}
113154

@@ -232,4 +273,9 @@ const t = {
232273
createNew: 'Create New',
233274
delete: 'Delete',
234275
edit: 'Edit',
276+
unknownErrorOccured: 'An unknown error occurred.',
277+
climatePresetNotFound: 'Climate Preset not found.',
278+
deviceNotFound: 'Device not found.',
279+
climatePresetIsScheduled:
280+
'The climate preset has upcoming schedules and cannot be deleted.',
235281
}

0 commit comments

Comments
 (0)