@seamapi/react 1.43.0 → 1.45.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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +5353 -5226
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +28 -0
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/seam/access-codes/use-delete-access-code.d.ts +8 -0
  9. package/lib/seam/access-codes/use-delete-access-code.js +22 -0
  10. package/lib/seam/access-codes/use-delete-access-code.js.map +1 -0
  11. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.d.ts +2 -1
  12. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +7 -2
  13. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  14. package/lib/seam/components/AccessCodeTable/AccessCodeMenu.d.ts +10 -0
  15. package/lib/seam/components/AccessCodeTable/AccessCodeMenu.js +44 -0
  16. package/lib/seam/components/AccessCodeTable/AccessCodeMenu.js.map +1 -0
  17. package/lib/seam/components/AccessCodeTable/AccessCodeRow.d.ts +10 -0
  18. package/lib/seam/components/AccessCodeTable/AccessCodeRow.js +26 -0
  19. package/lib/seam/components/AccessCodeTable/AccessCodeRow.js.map +1 -0
  20. package/lib/seam/components/AccessCodeTable/AccessCodeTable.d.ts +2 -1
  21. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +7 -40
  22. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
  23. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.d.ts +2 -1
  24. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.js +2 -1
  25. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.js.map +1 -1
  26. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.d.ts +2 -1
  27. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.js +10 -12
  28. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.js.map +1 -1
  29. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.d.ts +2 -1
  30. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.js +3 -3
  31. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.js.map +1 -1
  32. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.d.ts +2 -1
  33. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.js +3 -3
  34. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.js.map +1 -1
  35. package/lib/ui/Menu/MenuItem.d.ts +4 -2
  36. package/lib/ui/Menu/MenuItem.js +7 -4
  37. package/lib/ui/Menu/MenuItem.js.map +1 -1
  38. package/lib/ui/thermostat/ClimateSettingStatus.d.ts +9 -0
  39. package/lib/ui/thermostat/ClimateSettingStatus.js +33 -0
  40. package/lib/ui/thermostat/ClimateSettingStatus.js.map +1 -0
  41. package/lib/version.d.ts +1 -1
  42. package/lib/version.js +1 -1
  43. package/package.json +1 -1
  44. package/src/lib/seam/access-codes/use-delete-access-code.ts +47 -0
  45. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts +1 -0
  46. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +22 -4
  47. package/src/lib/seam/components/AccessCodeTable/AccessCodeMenu.tsx +118 -0
  48. package/src/lib/seam/components/AccessCodeTable/AccessCodeRow.tsx +73 -0
  49. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts +1 -0
  50. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +9 -87
  51. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx +3 -0
  52. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.tsx +16 -14
  53. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.element.ts +1 -0
  54. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.tsx +4 -0
  55. package/src/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.ts +5 -3
  56. package/src/lib/ui/Menu/MenuItem.tsx +17 -6
  57. package/src/lib/ui/thermostat/ClimateSettingStatus.tsx +88 -0
  58. package/src/lib/version.ts +1 -1
  59. package/src/styles/_access-code-details.scss +2 -0
  60. package/src/styles/_access-code-table.scss +12 -0
  61. package/src/styles/_main.scss +2 -0
  62. package/src/styles/_thermostat.scss +21 -0
@@ -0,0 +1,118 @@
1
+ import { CopyIcon } from 'lib/icons/Copy.js'
2
+ import type { UseAccessCodeData } from 'lib/seam/access-codes/use-access-code.js'
3
+ import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js'
4
+ import { Button } from 'lib/ui/Button.js'
5
+ import { copyToClipboard } from 'lib/ui/clipboard.js'
6
+ import { MenuItem } from 'lib/ui/Menu/MenuItem.js'
7
+ import { MoreActionsMenu } from 'lib/ui/Menu/MoreActionsMenu.js'
8
+ import { useToggle } from 'lib/ui/use-toggle.js'
9
+
10
+ export interface AccessCodeMenuProps {
11
+ accessCode: NonNullable<UseAccessCodeData>
12
+ onEdit: () => void
13
+ onViewDetails: () => void
14
+ disableEditAccessCode: boolean
15
+ disableDeleteAccessCode: boolean
16
+ }
17
+
18
+ export function AccessCodeMenu(props: AccessCodeMenuProps): JSX.Element {
19
+ return (
20
+ <MoreActionsMenu
21
+ menuProps={{
22
+ backgroundProps: {
23
+ className: 'seam-access-code-table-action-menu',
24
+ },
25
+ }}
26
+ >
27
+ <Content {...props} />
28
+ </MoreActionsMenu>
29
+ )
30
+ }
31
+
32
+ function Content({
33
+ accessCode,
34
+ onViewDetails,
35
+ disableEditAccessCode,
36
+ disableDeleteAccessCode,
37
+ onEdit,
38
+ }: AccessCodeMenuProps): JSX.Element {
39
+ const [deleteConfirmationVisible, toggleDeleteConfirmation] = useToggle()
40
+
41
+ const deleteAccessCode = useDeleteAccessCode()
42
+
43
+ if (deleteConfirmationVisible) {
44
+ return (
45
+ <div className='seam-delete-confirmation'>
46
+ <span>{t.deleteCodeConfirmation}</span>
47
+ <div className='seam-actions'>
48
+ <Button
49
+ onClick={toggleDeleteConfirmation}
50
+ disabled={deleteAccessCode.isLoading}
51
+ >
52
+ {t.cancelDelete}
53
+ </Button>
54
+ <Button
55
+ variant='solid'
56
+ disabled={deleteAccessCode.isLoading}
57
+ onClick={() => {
58
+ deleteAccessCode.mutate({
59
+ access_code_id: accessCode.access_code_id,
60
+ })
61
+ }}
62
+ >
63
+ {t.confirmDelete}
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ return (
71
+ <>
72
+ <MenuItem
73
+ onClick={() => {
74
+ void copyToClipboard(accessCode.code ?? '')
75
+ }}
76
+ >
77
+ <div className='seam-menu-item-copy'>
78
+ <span>
79
+ {t.copyCode} - {accessCode.code}
80
+ </span>
81
+ <CopyIcon />
82
+ </div>
83
+ </MenuItem>
84
+ <div className='seam-divider' />
85
+ <MenuItem onClick={onViewDetails}>{t.viewCodeDetails}</MenuItem>
86
+ {!disableEditAccessCode && (
87
+ <MenuItem onClick={onEdit}>{t.editCode}</MenuItem>
88
+ )}
89
+ {!disableDeleteAccessCode && (
90
+ <>
91
+ <div className='seam-divider' />
92
+ <MenuItem
93
+ onClick={(event) => {
94
+ event.stopPropagation() // Prevent hiding menu on outside click
95
+ toggleDeleteConfirmation()
96
+ }}
97
+ preventDefaultOnClick
98
+ className='seam-text-danger'
99
+ >
100
+ {t.deleteCode}
101
+ </MenuItem>
102
+ </>
103
+ )}
104
+ </>
105
+ )
106
+ }
107
+
108
+ const t = {
109
+ copyCode: 'Copy code',
110
+ codeIssue: 'code issue',
111
+ codeIssues: 'code issues',
112
+ editCode: 'Edit code',
113
+ viewCodeDetails: 'View code details',
114
+ deleteCode: 'Delete code',
115
+ deleteCodeConfirmation: 'Delete this code and data?',
116
+ cancelDelete: 'Cancel',
117
+ confirmDelete: 'Delete',
118
+ }
@@ -0,0 +1,73 @@
1
+ import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
2
+ import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
3
+ import { TriangleWarningOutlineIcon } from 'lib/icons/TriangleWarningOutline.js'
4
+ import type { UseAccessCodesData } from 'lib/seam/access-codes/use-access-codes.js'
5
+ import { AccessCodeMenu } from 'lib/seam/components/AccessCodeTable/AccessCodeMenu.js'
6
+ import { CodeDetails } from 'lib/seam/components/AccessCodeTable/CodeDetails.js'
7
+ import { TableCell } from 'lib/ui/Table/TableCell.js'
8
+ import { TableRow } from 'lib/ui/Table/TableRow.js'
9
+ import { Title } from 'lib/ui/typography/Title.js'
10
+
11
+ export interface AccessCodeRowProps {
12
+ accessCode: UseAccessCodesData[number]
13
+ onClick: () => void
14
+ onEdit: () => void
15
+ disableEditAccessCode: boolean
16
+ disableDeleteAccessCode: boolean
17
+ }
18
+
19
+ export function AccessCodeRow({
20
+ onClick,
21
+ accessCode,
22
+ onEdit,
23
+ disableEditAccessCode,
24
+ disableDeleteAccessCode,
25
+ }: AccessCodeRowProps): JSX.Element {
26
+ const errorCount = accessCode.errors.length
27
+ const warningCount = accessCode.warnings.length
28
+ const isPlural = errorCount === 0 || errorCount > 1
29
+ const errorIconTitle = isPlural
30
+ ? `${errorCount} ${t.codeIssues}`
31
+ : `${errorCount} ${t.codeIssue}`
32
+ const warningIconTitle = isPlural
33
+ ? `${warningCount} ${t.codeIssues}`
34
+ : `${warningCount} ${t.codeIssue}`
35
+
36
+ return (
37
+ <TableRow onClick={onClick}>
38
+ <TableCell className='seam-icon-cell'>
39
+ <div>
40
+ <AccessCodeKeyIcon />
41
+ </div>
42
+ </TableCell>
43
+ <TableCell className='seam-name-cell'>
44
+ <Title className='seam-truncated-text'>{accessCode.name}</Title>
45
+ <CodeDetails accessCode={accessCode} />
46
+ </TableCell>
47
+ <TableCell className='seam-action-cell'>
48
+ {errorCount > 0 && (
49
+ <div className='seam-code-issue-icon-wrap' title={errorIconTitle}>
50
+ <ExclamationCircleOutlineIcon />
51
+ </div>
52
+ )}
53
+ {errorCount === 0 && warningCount > 0 && (
54
+ <div className='seam-code-issue-icon-wrap' title={warningIconTitle}>
55
+ <TriangleWarningOutlineIcon />
56
+ </div>
57
+ )}
58
+ <AccessCodeMenu
59
+ accessCode={accessCode}
60
+ onEdit={onEdit}
61
+ onViewDetails={onClick}
62
+ disableDeleteAccessCode={disableDeleteAccessCode}
63
+ disableEditAccessCode={disableEditAccessCode}
64
+ />
65
+ </TableCell>
66
+ </TableRow>
67
+ )
68
+ }
69
+
70
+ const t = {
71
+ codeIssue: 'code issue',
72
+ codeIssues: 'code issues',
73
+ }
@@ -7,6 +7,7 @@ export const name = 'seam-access-code-table'
7
7
  export const props: ElementProps<Omit<AccessCodeTableProps, 'title'>> = {
8
8
  deviceId: 'string',
9
9
  disableLockUnlock: 'boolean',
10
+ disableDeleteAccessCode: 'boolean',
10
11
  disableSearch: 'boolean',
11
12
  accessCodeFilter: 'object',
12
13
  accessCodeComparator: 'object',
@@ -2,11 +2,7 @@ import classNames from 'classnames'
2
2
  import { useCallback, useMemo, useState } from 'react'
3
3
 
4
4
  import { compareByCreatedAtDesc } from 'lib/dates.js'
5
- import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
6
5
  import { AddIcon } from 'lib/icons/Add.js'
7
- import { CopyIcon } from 'lib/icons/Copy.js'
8
- import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
9
- import { TriangleWarningOutlineIcon } from 'lib/icons/TriangleWarningOutline.js'
10
6
  import {
11
7
  useAccessCodes,
12
8
  type UseAccessCodesData,
@@ -16,27 +12,21 @@ import {
16
12
  type AccessCodeFilter,
17
13
  AccessCodeHealthBar,
18
14
  } from 'lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js'
19
- import { CodeDetails } from 'lib/seam/components/AccessCodeTable/CodeDetails.js'
15
+ import { AccessCodeRow } from 'lib/seam/components/AccessCodeTable/AccessCodeRow.js'
20
16
  import { CreateAccessCodeForm } from 'lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.js'
21
17
  import { EditAccessCodeForm } from 'lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js'
22
- import { copyToClipboard } from 'lib/ui/clipboard.js'
23
18
  import { IconButton } from 'lib/ui/IconButton.js'
24
19
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
25
- import { MenuItem } from 'lib/ui/Menu/MenuItem.js'
26
- import { MoreActionsMenu } from 'lib/ui/Menu/MoreActionsMenu.js'
27
20
  import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
28
21
  import { TableBody } from 'lib/ui/Table/TableBody.js'
29
- import { TableCell } from 'lib/ui/Table/TableCell.js'
30
22
  import { TableHeader } from 'lib/ui/Table/TableHeader.js'
31
- import { TableRow } from 'lib/ui/Table/TableRow.js'
32
23
  import { TableTitle } from 'lib/ui/Table/TableTitle.js'
33
24
  import { SearchTextField } from 'lib/ui/TextField/SearchTextField.js'
34
25
  import { Caption } from 'lib/ui/typography/Caption.js'
35
- import { Title } from 'lib/ui/typography/Title.js'
36
26
  import { useToggle } from 'lib/ui/use-toggle.js'
37
27
 
38
28
  const disableCreateAccessCode = true
39
- const disableEditAccessCode = false
29
+ const disableEditAccessCode = true
40
30
 
41
31
  export interface AccessCodeTableProps {
42
32
  deviceId: string
@@ -59,6 +49,7 @@ export interface AccessCodeTableProps {
59
49
  */
60
50
  title?: string | null
61
51
  className?: string
52
+ disableDeleteAccessCode?: boolean
62
53
  }
63
54
 
64
55
  type AccessCode = UseAccessCodesData[number]
@@ -88,6 +79,7 @@ export function AccessCodeTable({
88
79
  heading = t.accessCodes,
89
80
  title = t.accessCodes,
90
81
  className,
82
+ disableDeleteAccessCode = false,
91
83
  }: AccessCodeTableProps): JSX.Element {
92
84
  const { accessCodes } = useAccessCodes({
93
85
  device_id: deviceId,
@@ -155,6 +147,7 @@ export function AccessCodeTable({
155
147
  onEdit={() => {
156
148
  setSelectedEditAccessCodeId(selectedViewAccessCodeId)
157
149
  }}
150
+ disableDeleteAccessCode={disableDeleteAccessCode}
158
151
  />
159
152
  )
160
153
  }
@@ -205,6 +198,7 @@ export function AccessCodeTable({
205
198
  onAccessCodeClick={handleAccessCodeClick}
206
199
  onAccessCodeEdit={handleAccessCodeEdit}
207
200
  disableEditAccessCode={disableEditAccessCode}
201
+ disableDeleteAccessCode={disableDeleteAccessCode}
208
202
  />
209
203
  </TableBody>
210
204
  </div>
@@ -216,12 +210,14 @@ function Content(props: {
216
210
  onAccessCodeClick: (accessCodeId: string) => void
217
211
  onAccessCodeEdit: (accessCodeId: string) => void
218
212
  disableEditAccessCode: boolean
213
+ disableDeleteAccessCode: boolean
219
214
  }): JSX.Element {
220
215
  const {
221
216
  accessCodes,
222
217
  onAccessCodeClick,
223
218
  onAccessCodeEdit,
224
219
  disableEditAccessCode,
220
+ disableDeleteAccessCode,
225
221
  } = props
226
222
  const [filter, setFilter] = useState<AccessCodeFilter | null>(null)
227
223
 
@@ -258,6 +254,7 @@ function Content(props: {
258
254
  onAccessCodeClick(accessCode.access_code_id)
259
255
  }}
260
256
  disableEditAccessCode={disableEditAccessCode}
257
+ disableDeleteAccessCode={disableDeleteAccessCode}
261
258
  onEdit={() => {
262
259
  onAccessCodeEdit(accessCode.access_code_id)
263
260
  }}
@@ -267,82 +264,7 @@ function Content(props: {
267
264
  )
268
265
  }
269
266
 
270
- function AccessCodeRow(props: {
271
- accessCode: UseAccessCodesData[number]
272
- onClick: () => void
273
- onEdit: () => void
274
- disableEditAccessCode: boolean
275
- }): JSX.Element {
276
- const { onClick, accessCode, onEdit, disableEditAccessCode } = props
277
-
278
- const errorCount = accessCode.errors.length
279
- const warningCount = accessCode.warnings.length
280
- const isPlural = errorCount === 0 || errorCount > 1
281
- const errorIconTitle = isPlural
282
- ? `${errorCount} ${t.codeIssues}`
283
- : `${errorCount} ${t.codeIssue}`
284
- const warningIconTitle = isPlural
285
- ? `${warningCount} ${t.codeIssues}`
286
- : `${warningCount} ${t.codeIssue}`
287
-
288
- return (
289
- <TableRow onClick={onClick}>
290
- <TableCell className='seam-icon-cell'>
291
- <div>
292
- <AccessCodeKeyIcon />
293
- </div>
294
- </TableCell>
295
- <TableCell className='seam-name-cell'>
296
- <Title className='seam-truncated-text'>{accessCode.name}</Title>
297
- <CodeDetails accessCode={accessCode} />
298
- </TableCell>
299
- <TableCell className='seam-action-cell'>
300
- {errorCount > 0 && (
301
- <div className='seam-code-issue-icon-wrap' title={errorIconTitle}>
302
- <ExclamationCircleOutlineIcon />
303
- </div>
304
- )}
305
- {errorCount === 0 && warningCount > 0 && (
306
- <div className='seam-code-issue-icon-wrap' title={warningIconTitle}>
307
- <TriangleWarningOutlineIcon />
308
- </div>
309
- )}
310
- <MoreActionsMenu
311
- menuProps={{
312
- backgroundProps: {
313
- className: 'seam-access-code-table-action-menu',
314
- },
315
- }}
316
- >
317
- <MenuItem
318
- onClick={() => {
319
- void copyToClipboard(accessCode.code ?? '')
320
- }}
321
- >
322
- <div className='seam-menu-item-copy'>
323
- <span>
324
- {t.copyCode} - {accessCode.code}
325
- </span>
326
- <CopyIcon />
327
- </div>
328
- </MenuItem>
329
- {!disableEditAccessCode && (
330
- <>
331
- <div className='seam-divider' />
332
- <MenuItem onClick={onEdit}>{t.editCode}</MenuItem>
333
- </>
334
- )}
335
- </MoreActionsMenu>
336
- </TableCell>
337
- </TableRow>
338
- )
339
- }
340
-
341
267
  const t = {
342
268
  accessCodes: 'Access Codes',
343
- copyCode: 'Copy code',
344
269
  noAccessCodesMessage: 'Sorry, no access codes were found',
345
- codeIssue: 'code issue',
346
- codeIssues: 'code issues',
347
- editCode: 'Edit code',
348
270
  }
@@ -14,6 +14,7 @@ interface SupportedDeviceContentProps {
14
14
  resetFilterValue: () => void
15
15
  filters: DeviceModelFilters
16
16
  brands: string[] | null
17
+ excludedBrands: string[]
17
18
  }
18
19
 
19
20
  export function SupportedDeviceContent({
@@ -21,12 +22,14 @@ export function SupportedDeviceContent({
21
22
  filterValue,
22
23
  filters,
23
24
  brands,
25
+ excludedBrands,
24
26
  }: SupportedDeviceContentProps): JSX.Element | null {
25
27
  const { deviceModels, isLoading, isError, refetch } = useFilteredDeviceModels(
26
28
  {
27
29
  filterValue,
28
30
  filters,
29
31
  brands,
32
+ excludedBrands,
30
33
  }
31
34
  )
32
35
 
@@ -14,6 +14,7 @@ interface SupportedDeviceFilterAreaProps {
14
14
  filters: DeviceModelFilters
15
15
  setFilters: Dispatch<SetStateAction<DeviceModelFilters>>
16
16
  brands: string[] | null
17
+ excludedBrands: string[]
17
18
  }
18
19
 
19
20
  export function SupportedDeviceFilterArea({
@@ -22,10 +23,11 @@ export function SupportedDeviceFilterArea({
22
23
  filters,
23
24
  setFilters,
24
25
  brands,
26
+ excludedBrands,
25
27
  }: SupportedDeviceFilterAreaProps): JSX.Element {
26
28
  const appliedFiltersCount = getAppliedFilterCount(filters)
27
29
 
28
- const availableBrands = useAvailableBrands(brands)
30
+ const availableBrands = useAvailableBrands(brands, excludedBrands)
29
31
 
30
32
  const resetFilter = (filterType: keyof DeviceModelFilters): void => {
31
33
  setFilters((filters) => ({
@@ -121,27 +123,27 @@ const getAppliedFilterCount = (filters: DeviceModelFilters): number => {
121
123
  return count
122
124
  }
123
125
 
124
- const useAvailableBrands = (brands: string[] | null): string[] => {
126
+ const useAvailableBrands = (
127
+ brands: string[] | null,
128
+ excludedBrands: string[]
129
+ ): string[] => {
125
130
  const { deviceModels } = useDeviceModels()
126
131
 
127
132
  if (deviceModels == null) return []
128
133
 
129
- const uniqueBrands = new Set<string>()
130
- for (const deviceModel of deviceModels) {
131
- const value = deviceModel.brand
132
-
133
- // UPSTREAM: API can return an empty value for brand.
134
- if (value.trim() === '') continue
135
-
136
- uniqueBrands.add(deviceModel.brand)
137
- }
138
-
139
- return Array.from(uniqueBrands)
134
+ const availableBrands = deviceModels
135
+ .map(({ brand }) => brand.trim())
136
+ .filter((brand) => {
137
+ // UPSTREAM: API can return an empty value for brand.
138
+ return brand !== ''
139
+ })
140
140
  .filter((brand) => {
141
141
  if (brands === null) return true
142
- return brands.includes(brand)
142
+ return brands.includes(brand) && !excludedBrands.includes(brand)
143
143
  })
144
144
  .map((brand) => capitalize(brand))
145
+
146
+ return Array.from(new Set(availableBrands))
145
147
  }
146
148
 
147
149
  const t = {
@@ -8,6 +8,7 @@ export const props: ElementProps<SupportedDeviceTableProps> = {
8
8
  cannotFilter: 'boolean',
9
9
  disableFilter: 'boolean',
10
10
  brands: 'array',
11
+ excludedBrands: 'array',
11
12
  className: 'string',
12
13
  }
13
14
 
@@ -12,6 +12,7 @@ export interface SupportedDeviceTableProps {
12
12
  */
13
13
  cannotFilter?: boolean
14
14
  brands?: string[] | null
15
+ excludedBrands?: string[]
15
16
  className?: string
16
17
  }
17
18
 
@@ -19,6 +20,7 @@ export function SupportedDeviceTable({
19
20
  disableFilter = false,
20
21
  cannotFilter,
21
22
  brands = null,
23
+ excludedBrands = [],
22
24
  className,
23
25
  }: SupportedDeviceTableProps = {}): JSX.Element {
24
26
  const [filterValue, setFilterValue] = useState('')
@@ -43,6 +45,7 @@ export function SupportedDeviceTable({
43
45
  filters={filters}
44
46
  setFilters={setFilters}
45
47
  brands={brands}
48
+ excludedBrands={excludedBrands}
46
49
  />
47
50
  )}
48
51
  <SupportedDeviceContent
@@ -52,6 +55,7 @@ export function SupportedDeviceTable({
52
55
  filterValue={filterValue}
53
56
  filters={filters}
54
57
  brands={brands}
58
+ excludedBrands={excludedBrands}
55
59
  />
56
60
  </div>
57
61
  )
@@ -12,10 +12,12 @@ export const useFilteredDeviceModels = ({
12
12
  filterValue,
13
13
  filters,
14
14
  brands,
15
+ excludedBrands,
15
16
  }: {
16
17
  filterValue: string
17
18
  filters: DeviceModelFilters
18
19
  brands: string[] | null
20
+ excludedBrands: string[]
19
21
  }): ReturnType<typeof useDeviceModels> => {
20
22
  const params: UseDeviceModelsParams = {}
21
23
 
@@ -37,12 +39,12 @@ export const useFilteredDeviceModels = ({
37
39
  return query
38
40
  }
39
41
 
40
- // UPSTREAM: The API does not have a brands query parameter,
42
+ // UPSTREAM: The API does not have a brands or excludedBrands query parameter,
41
43
  // so selected brands are filtered here.
42
44
  return {
43
45
  ...query,
44
- deviceModels: query.deviceModels?.filter((deviceModel) =>
45
- brands.includes(deviceModel.brand)
46
+ deviceModels: query.deviceModels?.filter(
47
+ ({ brand }) => brands.includes(brand) && !excludedBrands.includes(brand)
46
48
  ),
47
49
  }
48
50
  }
@@ -1,20 +1,31 @@
1
+ import classNames from 'classnames'
1
2
  import type { PropsWithChildren } from 'react'
2
3
 
3
4
  import { useMenu } from 'lib/ui/Menu/Menu.js'
4
5
 
5
6
  interface MenuItemProps extends PropsWithChildren {
6
- onClick: () => void
7
+ onClick: (event: React.MouseEvent) => void
8
+ className?: string
9
+ preventDefaultOnClick?: boolean
7
10
  }
8
11
 
9
- export function MenuItem({ onClick, children }: MenuItemProps): JSX.Element {
12
+ export function MenuItem({
13
+ onClick,
14
+ children,
15
+ className,
16
+ preventDefaultOnClick = false,
17
+ }: MenuItemProps): JSX.Element {
10
18
  const { close: closeMenu } = useMenu()
11
19
 
12
20
  return (
13
21
  <div
14
- className='seam-menu-item'
15
- onClick={() => {
16
- onClick()
17
- closeMenu()
22
+ className={classNames('seam-menu-item', className)}
23
+ onClick={(event: React.MouseEvent) => {
24
+ onClick(event)
25
+
26
+ if (!preventDefaultOnClick) {
27
+ closeMenu()
28
+ }
18
29
  }}
19
30
  >
20
31
  {children}
@@ -0,0 +1,88 @@
1
+ import type { ClimateSetting } from 'seamapi'
2
+
3
+ import { ThermostatCoolIcon } from 'lib/icons/ThermostatCool.js'
4
+ import { ThermostatHeatIcon } from 'lib/icons/ThermostatHeat.js'
5
+ import { ThermostatHeatCoolIcon } from 'lib/icons/ThermostatHeatCool.js'
6
+ import { ThermostatOffIcon } from 'lib/icons/ThermostatOff.js'
7
+
8
+ interface ClimateSettingStatusProps {
9
+ climateSetting: ClimateSetting
10
+ temperatureUnit?: 'fahrenheit' | 'celsius'
11
+ iconPlacement?: 'left' | 'right'
12
+ }
13
+
14
+ export function ClimateSettingStatus({
15
+ climateSetting,
16
+ temperatureUnit = 'fahrenheit',
17
+ iconPlacement = 'left',
18
+ }: ClimateSettingStatusProps): JSX.Element {
19
+ return (
20
+ <div className='seam-climate-setting-status'>
21
+ {iconPlacement === 'left' && (
22
+ <ClimateSettingIcon mode={climateSetting.hvac_mode_setting} />
23
+ )}
24
+ <Content
25
+ mode={climateSetting.hvac_mode_setting}
26
+ coolingSetPoint={climateSetting.cooling_set_point_fahrenheit}
27
+ heatingSetPoint={climateSetting.heating_set_point_fahrenheit}
28
+ temperatureUnit={temperatureUnit}
29
+ />
30
+ {iconPlacement === 'right' && (
31
+ <ClimateSettingIcon mode={climateSetting.hvac_mode_setting} />
32
+ )}
33
+ </div>
34
+ )
35
+ }
36
+
37
+ function ClimateSettingIcon(props: {
38
+ mode: ClimateSetting['hvac_mode_setting']
39
+ }): JSX.Element | null {
40
+ const { mode } = props
41
+
42
+ return (
43
+ <div className='seam-climate-setting-status-icon'>
44
+ {mode === 'cool' && <ThermostatCoolIcon />}
45
+ {mode === 'heat' && <ThermostatHeatIcon />}
46
+ {mode === 'heatcool' && <ThermostatHeatCoolIcon />}
47
+ {mode === 'off' && <ThermostatOffIcon />}
48
+ </div>
49
+ )
50
+ }
51
+
52
+ function Content(props: {
53
+ mode: ClimateSetting['hvac_mode_setting']
54
+ coolingSetPoint:
55
+ | ClimateSetting['cooling_set_point_fahrenheit']
56
+ | ClimateSetting['cooling_set_point_celsius']
57
+ heatingSetPoint:
58
+ | ClimateSetting['heating_set_point_fahrenheit']
59
+ | ClimateSetting['heating_set_point_celsius']
60
+ temperatureUnit: 'fahrenheit' | 'celsius'
61
+ }): JSX.Element | null {
62
+ const { mode, coolingSetPoint, heatingSetPoint, temperatureUnit } = props
63
+ const hasCoolingSetPoint = coolingSetPoint !== undefined
64
+ const hasHeatingSetPoint = heatingSetPoint !== undefined
65
+ const unit =
66
+ temperatureUnit === 'fahrenheit' ? t.degreeFahrenheit : t.degreeCelsius
67
+
68
+ if (mode === 'cool' && hasCoolingSetPoint)
69
+ return <span>{`${coolingSetPoint}${unit}`}</span>
70
+
71
+ if (mode === 'heat' && hasHeatingSetPoint)
72
+ return <span>{`${heatingSetPoint}${unit}`}</span>
73
+
74
+ if (mode === 'heatcool' && hasHeatingSetPoint && hasCoolingSetPoint)
75
+ return (
76
+ <span>{`${heatingSetPoint}${unit} - ${coolingSetPoint}${unit}`}</span>
77
+ )
78
+
79
+ if (mode === 'off') return <span>{t.off}</span>
80
+
81
+ return null
82
+ }
83
+
84
+ const t = {
85
+ degreeFahrenheit: '°F',
86
+ degreeCelsius: '°C',
87
+ off: 'Off',
88
+ }
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '1.43.0'
1
+ const seamapiReactVersion = '1.45.0'
2
2
 
3
3
  export default seamapiReactVersion
@@ -82,6 +82,8 @@
82
82
  .seam-actions {
83
83
  margin-bottom: 16px;
84
84
  padding: 0 24px;
85
+ display: flex;
86
+ gap: 8px;
85
87
  }
86
88
 
87
89
  .seam-details {