@seamapi/react 1.23.0 → 1.25.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 (93) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +5077 -4846
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +160 -45
  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/components/AccessCodeTable/AccessCodeTable.d.ts +2 -2
  9. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +1 -1
  10. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
  11. package/lib/seam/components/DeviceTable/DeviceHealthBar.d.ts +3 -2
  12. package/lib/seam/components/DeviceTable/DeviceHealthBar.js +23 -11
  13. package/lib/seam/components/DeviceTable/DeviceHealthBar.js.map +1 -1
  14. package/lib/seam/components/DeviceTable/DeviceRow.js +19 -1
  15. package/lib/seam/components/DeviceTable/DeviceRow.js.map +1 -1
  16. package/lib/seam/components/DeviceTable/DeviceTable.d.ts +2 -2
  17. package/lib/seam/components/DeviceTable/DeviceTable.js +6 -2
  18. package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
  19. package/lib/seam/components/SupportedDeviceTable/HiddenDevicesOverlay.d.ts +5 -0
  20. package/lib/seam/components/SupportedDeviceTable/HiddenDevicesOverlay.js +8 -0
  21. package/lib/seam/components/SupportedDeviceTable/HiddenDevicesOverlay.js.map +1 -0
  22. package/lib/seam/components/SupportedDeviceTable/ShowAllDevicesButton.d.ts +8 -0
  23. package/lib/seam/components/SupportedDeviceTable/ShowAllDevicesButton.js +14 -0
  24. package/lib/seam/components/SupportedDeviceTable/ShowAllDevicesButton.js.map +1 -0
  25. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceBrandSection.d.ts +7 -0
  26. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceBrandSection.js +45 -0
  27. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceBrandSection.js.map +1 -0
  28. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.d.ts +2 -1
  29. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.js +36 -11
  30. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.js.map +1 -1
  31. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContentRows.d.ts +7 -0
  32. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContentRows.js +12 -0
  33. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceContentRows.js.map +1 -0
  34. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.d.ts +2 -1
  35. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.js +12 -4
  36. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.js.map +1 -1
  37. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterResultRowProps.d.ts +7 -0
  38. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterResultRowProps.js +12 -0
  39. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterResultRowProps.js.map +1 -0
  40. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.d.ts +4 -0
  41. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.js +19 -5
  42. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.js.map +1 -1
  43. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.d.ts +2 -1
  44. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.js +3 -3
  45. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.js.map +1 -1
  46. package/lib/seam/components/SupportedDeviceTable/use-device-provider.d.ts +2 -0
  47. package/lib/seam/components/SupportedDeviceTable/use-device-provider.js +15 -0
  48. package/lib/seam/components/SupportedDeviceTable/use-device-provider.js.map +1 -0
  49. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.d.ts +5 -1
  50. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.js +9 -2
  51. package/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.js.map +1 -1
  52. package/lib/seam/devices/use-device-providers.d.ts +5 -0
  53. package/lib/seam/devices/use-device-providers.js +17 -0
  54. package/lib/seam/devices/use-device-providers.js.map +1 -0
  55. package/lib/ui/device/OnlineStatus.js +14 -4
  56. package/lib/ui/device/OnlineStatus.js.map +1 -1
  57. package/lib/version.d.ts +1 -1
  58. package/lib/version.js +1 -1
  59. package/package.json +2 -2
  60. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts +1 -0
  61. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts +1 -0
  62. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +2 -2
  63. package/src/lib/seam/components/DeviceDetails/DeviceDetails.element.ts +1 -0
  64. package/src/lib/seam/components/DeviceTable/DeviceHealthBar.tsx +49 -19
  65. package/src/lib/seam/components/DeviceTable/DeviceRow.tsx +48 -9
  66. package/src/lib/seam/components/DeviceTable/DeviceTable.element.ts +1 -0
  67. package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +16 -4
  68. package/src/lib/seam/components/SupportedDeviceTable/HiddenDevicesOverlay.tsx +11 -0
  69. package/src/lib/seam/components/SupportedDeviceTable/ShowAllDevicesButton.tsx +32 -0
  70. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceBrandSection.tsx +96 -0
  71. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx +62 -16
  72. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContentRows.tsx +27 -0
  73. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterArea.tsx +22 -3
  74. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterResultRowProps.tsx +49 -0
  75. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.tsx +52 -20
  76. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.element.ts +1 -0
  77. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.tsx +4 -0
  78. package/src/lib/seam/components/SupportedDeviceTable/use-device-provider.ts +22 -0
  79. package/src/lib/seam/components/SupportedDeviceTable/use-filtered-device-models.ts +21 -4
  80. package/src/lib/seam/devices/use-device-providers.ts +25 -0
  81. package/src/lib/ui/device/OnlineStatus.tsx +23 -7
  82. package/src/lib/version.ts +1 -1
  83. package/src/styles/_access-code-details.scss +1 -1
  84. package/src/styles/_buttons.scss +3 -3
  85. package/src/styles/_colors.scss +6 -2
  86. package/src/styles/_device-table.scss +4 -0
  87. package/src/styles/_menus.scss +1 -1
  88. package/src/styles/_supported-device-table.scss +192 -52
  89. package/src/styles/_tables.scss +1 -1
  90. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceHeader.d.ts +0 -2
  91. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceHeader.js +0 -12
  92. package/lib/seam/components/SupportedDeviceTable/SupportedDeviceHeader.js.map +0 -1
  93. package/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceHeader.tsx +0 -22
@@ -1,11 +1,20 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { OnlineStatusAccountOfflineIcon } from '../../../lib/icons/OnlineStatusAccountOffline.js';
2
3
  import { OnlineStatusDeviceOfflineIcon } from '../../../lib/icons/OnlineStatusDeviceOffline.js';
3
4
  import { OnlineStatusOnlineIcon } from '../../../lib/icons/OnlineStatusOnline.js';
4
5
  export function OnlineStatus(props) {
5
- const { device: { properties: { online }, }, } = props;
6
- return (_jsx("div", { className: 'seam-online-status', children: _jsx(Content, { isOnline: online }) }));
6
+ const { device } = props;
7
+ const isAccountOffline = device.errors.filter((error) => error.error_code === 'account_disconnected')
8
+ .length > 0;
9
+ if (isAccountOffline) {
10
+ return _jsx(AccountOfflineContent, {});
11
+ }
12
+ return (_jsxs("div", { className: 'seam-online-status', children: [isAccountOffline && _jsx(OnlineStatusAccountOfflineIcon, {}), _jsx(DeviceOnlineContent, { isOnline: device.properties.online })] }));
13
+ }
14
+ function AccountOfflineContent() {
15
+ return (_jsxs("div", { className: 'seam-online-status', children: [_jsx(OnlineStatusAccountOfflineIcon, {}), _jsx("span", { className: 'seam-text-danger', children: t.accountOffline })] }));
7
16
  }
8
- function Content(props) {
17
+ function DeviceOnlineContent(props) {
9
18
  const { isOnline } = props;
10
19
  if (isOnline) {
11
20
  return (_jsxs(_Fragment, { children: [_jsx(OnlineStatusOnlineIcon, {}), _jsx("span", { className: 'seam-status-text', children: t.online })] }));
@@ -14,6 +23,7 @@ function Content(props) {
14
23
  }
15
24
  const t = {
16
25
  online: 'Online',
26
+ accountOffline: 'Account Offline',
17
27
  deviceOffline: 'Device Offline',
18
28
  };
19
29
  //# sourceMappingURL=OnlineStatus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"OnlineStatus.js","sourceRoot":"","sources":["../../../src/lib/ui/device/OnlineStatus.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAA;AACtF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAA;AAMxE,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EACJ,MAAM,EAAE,EACN,UAAU,EAAE,EAAE,MAAM,EAAE,GACvB,GACF,GAAG,KAAK,CAAA;IAET,OAAO,CACL,cAAK,SAAS,EAAC,oBAAoB,YACjC,KAAC,OAAO,IAAC,QAAQ,EAAE,MAAM,GAAI,GACzB,CACP,CAAA;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAA4B;IAC3C,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IAC1B,IAAI,QAAQ,EAAE;QACZ,OAAO,CACL,8BACE,KAAC,sBAAsB,KAAG,EAC1B,eAAM,SAAS,EAAC,kBAAkB,YAAE,CAAC,CAAC,MAAM,GAAQ,IACnD,CACJ,CAAA;KACF;IAED,OAAO,CACL,8BACE,KAAC,6BAA6B,KAAG,EACjC,eAAM,SAAS,EAAC,kBAAkB,YAAE,CAAC,CAAC,aAAa,GAAQ,IAC1D,CACJ,CAAA;AACH,CAAC;AAED,MAAM,CAAC,GAAG;IACR,MAAM,EAAE,QAAQ;IAChB,aAAa,EAAE,gBAAgB;CAChC,CAAA"}
1
+ {"version":3,"file":"OnlineStatus.js","sourceRoot":"","sources":["../../../src/lib/ui/device/OnlineStatus.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,8BAA8B,EAAE,MAAM,yCAAyC,CAAA;AACxF,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAA;AACtF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAA;AAMxE,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAExB,MAAM,gBAAgB,GACpB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,KAAK,sBAAsB,CAAC;SACzE,MAAM,GAAG,CAAC,CAAA;IAEf,IAAI,gBAAgB,EAAE;QACpB,OAAO,KAAC,qBAAqB,KAAG,CAAA;KACjC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,oBAAoB,aAChC,gBAAgB,IAAI,KAAC,8BAA8B,KAAG,EACvD,KAAC,mBAAmB,IAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,GAAI,IACvD,CACP,CAAA;AACH,CAAC;AAED,SAAS,qBAAqB;IAC5B,OAAO,CACL,eAAK,SAAS,EAAC,oBAAoB,aACjC,KAAC,8BAA8B,KAAG,EAClC,eAAM,SAAS,EAAC,kBAAkB,YAAE,CAAC,CAAC,cAAc,GAAQ,IACxD,CACP,CAAA;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,KAA4B;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IAC1B,IAAI,QAAQ,EAAE;QACZ,OAAO,CACL,8BACE,KAAC,sBAAsB,KAAG,EAC1B,eAAM,SAAS,EAAC,kBAAkB,YAAE,CAAC,CAAC,MAAM,GAAQ,IACnD,CACJ,CAAA;KACF;IAED,OAAO,CACL,8BACE,KAAC,6BAA6B,KAAG,EACjC,eAAM,SAAS,EAAC,kBAAkB,YAAE,CAAC,CAAC,aAAa,GAAQ,IAC1D,CACJ,CAAA;AACH,CAAC;AAED,MAAM,CAAC,GAAG;IACR,MAAM,EAAE,QAAQ;IAChB,cAAc,EAAE,iBAAiB;IACjC,aAAa,EAAE,gBAAgB;CAChC,CAAA"}
package/lib/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const seamapiReactVersion = "1.23.0";
1
+ declare const seamapiReactVersion = "1.25.0";
2
2
  export default seamapiReactVersion;
package/lib/version.js CHANGED
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '1.23.0';
1
+ const seamapiReactVersion = '1.25.0';
2
2
  export default seamapiReactVersion;
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamapi/react",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "Seam Components.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -121,7 +121,7 @@
121
121
  "@mui/icons-material": "^5.11.16",
122
122
  "@mui/material": "^5.12.2",
123
123
  "@r2wc/react-to-web-component": "^2.0.2",
124
- "@seamapi/fake-seam-connect": "^0.8.0",
124
+ "@seamapi/fake-seam-connect": "^0.10.0",
125
125
  "@storybook/addon-essentials": "^7.0.2",
126
126
  "@storybook/addon-interactions": "^7.0.2",
127
127
  "@storybook/addon-links": "^7.0.2",
@@ -6,6 +6,7 @@ export const name = 'seam-access-code-details'
6
6
 
7
7
  export const props: ElementProps<AccessCodeDetailsProps> = {
8
8
  accessCodeId: 'string',
9
+ disableLockUnlock: 'boolean',
9
10
  onBack: 'function',
10
11
  className: 'string',
11
12
  }
@@ -6,6 +6,7 @@ export const name = 'seam-access-code-table'
6
6
 
7
7
  export const props: ElementProps<AccessCodeTableProps> = {
8
8
  deviceId: 'string',
9
+ disableLockUnlock: 'boolean',
9
10
  accessCodeFilter: 'function',
10
11
  accessCodeComparator: 'function',
11
12
  onAccessCodeClick: 'function',
@@ -32,6 +32,7 @@ import { Title } from 'lib/ui/typography/Title.js'
32
32
 
33
33
  export interface AccessCodeTableProps {
34
34
  deviceId: string
35
+ disableLockUnlock?: boolean
35
36
  accessCodeFilter?: (
36
37
  accessCode: AccessCode,
37
38
  searchInputValue: string
@@ -42,7 +43,6 @@ export interface AccessCodeTableProps {
42
43
  ) => number
43
44
  onAccessCodeClick?: (accessCodeId: string) => void
44
45
  preventDefaultOnAccessCodeClick?: boolean
45
- disableLockUnlock?: boolean
46
46
  onBack?: () => void
47
47
  className?: string
48
48
  }
@@ -64,9 +64,9 @@ const defaultAccessCodeFilter = (
64
64
 
65
65
  export function AccessCodeTable({
66
66
  deviceId,
67
+ disableLockUnlock = false,
67
68
  onAccessCodeClick = () => {},
68
69
  preventDefaultOnAccessCodeClick = false,
69
- disableLockUnlock = false,
70
70
  onBack,
71
71
  accessCodeFilter = defaultAccessCodeFilter,
72
72
  accessCodeComparator = compareByCreatedAtDesc,
@@ -6,6 +6,7 @@ export const name = 'seam-device-details'
6
6
 
7
7
  export const props: ElementProps<DeviceDetailsProps> = {
8
8
  deviceId: 'string',
9
+ disableLockUnlock: 'boolean',
9
10
  onBack: 'function',
10
11
  className: 'string',
11
12
  }
@@ -1,15 +1,17 @@
1
1
  import { CheckIcon } from 'lib/icons/Check.js'
2
2
  import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
3
+ import { OnlineStatusAccountOfflineIcon } from 'lib/icons/OnlineStatusAccountOffline.js'
3
4
  import type { UseDevicesData } from 'lib/seam/devices/use-devices.js'
4
5
  import { TableFilterBar } from 'lib/ui/Table/TableFilterBar/TableFilterBar.js'
5
6
  import { TableFilterItem } from 'lib/ui/Table/TableFilterBar/TableFilterItem.js'
6
7
 
8
+ export type AccountFilter = 'account_issues'
7
9
  export type DeviceFilter = 'device_issues'
8
10
 
9
11
  interface DeviceHealthBarProps {
10
12
  devices: Array<UseDevicesData[number]>
11
- filter: DeviceFilter | null
12
- onFilterSelect: (filter: DeviceFilter | null) => void
13
+ filter: AccountFilter | DeviceFilter | null
14
+ onFilterSelect: (filter: AccountFilter | DeviceFilter | null) => void
13
15
  }
14
16
 
15
17
  export function DeviceHealthBar({
@@ -17,25 +19,40 @@ export function DeviceHealthBar({
17
19
  filter,
18
20
  onFilterSelect,
19
21
  }: DeviceHealthBarProps): JSX.Element {
20
- const erroredDevices = devices.filter((device) => device.errors.length > 0)
21
- const issueCount = erroredDevices.length
22
+ const erroredAccounts = devices.filter((device) => {
23
+ return (
24
+ device.errors.filter((error) => 'is_connected_account_error' in error)
25
+ .length > 0
26
+ )
27
+ })
28
+ const erroredDevices = devices.filter((device) => {
29
+ return (
30
+ device.errors.filter((error) => 'is_device_error' in error).length > 0
31
+ )
32
+ })
33
+ const accountIssueCount = erroredAccounts.length
34
+ const deviceIssueCount = erroredDevices.length
22
35
 
23
- const toggle = (target: DeviceFilter) => () => {
36
+ const toggle = (target: AccountFilter | DeviceFilter) => () => {
24
37
  const newFilter = target === filter ? null : target
25
38
  onFilterSelect(newFilter)
26
39
  }
27
40
 
28
- const isPlural = issueCount === 0 || issueCount > 1
29
- const label = isPlural
30
- ? `${issueCount} ${t.deviceIssues}`
31
- : `${issueCount} ${t.deviceIssue}`
41
+ const deviceLabel =
42
+ deviceIssueCount > 0
43
+ ? `${deviceIssueCount} ${t.deviceIssues}`
44
+ : `${deviceIssueCount} ${t.deviceIssue}`
45
+ const accountLabel =
46
+ accountIssueCount > 0
47
+ ? `${accountIssueCount} ${t.accountIssues}`
48
+ : `${accountIssueCount} ${t.accountIssue}`
32
49
 
33
- if (issueCount === 0) {
50
+ if (accountIssueCount === 0 && deviceIssueCount === 0) {
34
51
  return (
35
52
  <TableFilterBar filterCleared>
36
53
  <TableFilterItem>
37
54
  <CheckIcon />
38
- {t.devicesOk}
55
+ {t.devicesAndAccountsOk}
39
56
  </TableFilterItem>
40
57
  </TableFilterBar>
41
58
  )
@@ -48,19 +65,32 @@ export function DeviceHealthBar({
48
65
  onFilterSelect(null)
49
66
  }}
50
67
  >
51
- <TableFilterItem
52
- onClick={toggle('device_issues')}
53
- selected={filter === 'device_issues'}
54
- >
55
- <ExclamationCircleOutlineIcon />
56
- {label}
57
- </TableFilterItem>
68
+ {deviceIssueCount > 0 && (
69
+ <TableFilterItem
70
+ onClick={toggle('device_issues')}
71
+ selected={filter === 'device_issues'}
72
+ >
73
+ <ExclamationCircleOutlineIcon />
74
+ {deviceLabel}
75
+ </TableFilterItem>
76
+ )}
77
+ {accountIssueCount > 0 && (
78
+ <TableFilterItem
79
+ onClick={toggle('account_issues')}
80
+ selected={filter === 'account_issues'}
81
+ >
82
+ <OnlineStatusAccountOfflineIcon />
83
+ {accountLabel}
84
+ </TableFilterItem>
85
+ )}
58
86
  </TableFilterBar>
59
87
  )
60
88
  }
61
89
 
62
90
  const t = {
63
- devicesOk: 'Devices OK',
91
+ devicesAndAccountsOk: 'Devices & accounts OK',
92
+ accountIssue: 'account issue',
93
+ accountIssues: 'account issues',
64
94
  deviceIssue: 'device issue',
65
95
  deviceIssues: 'device issues',
66
96
  }
@@ -1,3 +1,5 @@
1
+ import classNames from 'classnames'
2
+
1
3
  import type {
2
4
  UseDevicesData,
3
5
  UseDevicesParams,
@@ -25,24 +27,61 @@ export function DeviceRow({
25
27
  device,
26
28
  onClick,
27
29
  }: DeviceRowProps): JSX.Element | null {
30
+ const opacityClass =
31
+ isAccountOffline(device) || isDeviceOffline(device)
32
+ ? 'seam-offline-fade'
33
+ : undefined
28
34
  return (
29
- <TableRow key={device.device_id} onClick={onClick}>
30
- <TableCell className='seam-image-cell'>
35
+ <TableRow onClick={onClick}>
36
+ <TableCell className={classNames('seam-image-cell', opacityClass)}>
31
37
  <DeviceImage device={device} />
32
38
  </TableCell>
33
39
  <TableCell className='seam-body-cell'>
34
- <Title className='seam-truncated-text'>{device.properties.name}</Title>
40
+ <Title className={classNames('seam-truncated-text', opacityClass)}>
41
+ {device.properties.name}
42
+ </Title>
35
43
  <div className='seam-bottom'>
36
- <span className='seam-device-model'>
44
+ <span className={classNames('seam-device-model', opacityClass)}>
37
45
  {device.properties.model.display_name}
38
46
  </span>
39
- <div className='seam-device-statuses'>
40
- <OnlineStatus device={device} />
41
- <BatteryStatus device={device} />
42
- <LockStatus device={device} />
43
- </div>
47
+ <DeviceStatuses device={device} />
44
48
  </div>
45
49
  </TableCell>
46
50
  </TableRow>
47
51
  )
48
52
  }
53
+
54
+ interface DeviceStatusesProps {
55
+ device: UseDevicesData[number]
56
+ }
57
+
58
+ function DeviceStatuses({ device }: DeviceStatusesProps): JSX.Element | null {
59
+ if (isAccountOffline(device) || isDeviceOffline(device)) {
60
+ return (
61
+ <div className='seam-device-statuses'>
62
+ <OnlineStatus device={device} />
63
+ </div>
64
+ )
65
+ }
66
+ return (
67
+ <div className='seam-device-statuses'>
68
+ <OnlineStatus device={device} />
69
+ <BatteryStatus device={device} />
70
+ <LockStatus device={device} />
71
+ </div>
72
+ )
73
+ }
74
+
75
+ const isAccountOffline = (device: UseDevicesData[number]): boolean => {
76
+ return (
77
+ device.errors.filter((error) => error.error_code === 'account_disconnected')
78
+ .length > 0
79
+ )
80
+ }
81
+
82
+ const isDeviceOffline = (device: UseDevicesData[number]): boolean => {
83
+ return (
84
+ device.errors.filter((error) => error.error_code === 'device_disconnected')
85
+ .length > 0
86
+ )
87
+ }
@@ -7,6 +7,7 @@ export const name = 'seam-device-table'
7
7
  export const props: ElementProps<DeviceTableProps> = {
8
8
  deviceIds: 'json',
9
9
  connectedAccountIds: 'json',
10
+ disableLockUnlock: 'boolean',
10
11
  deviceFilter: 'function',
11
12
  deviceComparator: 'function',
12
13
  onDeviceClick: 'function',
@@ -5,6 +5,7 @@ import { isLockDevice } from 'seamapi'
5
5
  import { compareByCreatedAtDesc } from 'lib/dates.js'
6
6
  import { DeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
7
7
  import {
8
+ type AccountFilter,
8
9
  type DeviceFilter,
9
10
  DeviceHealthBar,
10
11
  } from 'lib/seam/components/DeviceTable/DeviceHealthBar.js'
@@ -26,11 +27,11 @@ type Device = UseDevicesData[number]
26
27
  export interface DeviceTableProps {
27
28
  deviceIds?: string[]
28
29
  connectedAccountIds?: string[]
30
+ disableLockUnlock?: boolean
29
31
  deviceFilter?: (device: Device, searchInputValue: string) => boolean
30
32
  deviceComparator?: (deviceA: Device, deviceB: Device) => number
31
33
  onDeviceClick?: (deviceId: string) => void
32
34
  preventDefaultOnDeviceClick?: boolean
33
- disableLockUnlock?: boolean
34
35
  onBack?: () => void
35
36
  className?: string
36
37
  }
@@ -47,9 +48,9 @@ const defaultDeviceFilter = (
47
48
  export function DeviceTable({
48
49
  deviceIds,
49
50
  connectedAccountIds,
51
+ disableLockUnlock = false,
50
52
  onDeviceClick = () => {},
51
53
  preventDefaultOnDeviceClick = false,
52
- disableLockUnlock = false,
53
54
  onBack,
54
55
  deviceFilter = defaultDeviceFilter,
55
56
  deviceComparator = compareByCreatedAtDesc,
@@ -127,7 +128,9 @@ function Content(props: {
127
128
  onDeviceClick: (deviceId: string) => void
128
129
  }): JSX.Element {
129
130
  const { devices, onDeviceClick } = props
130
- const [filter, setFilter] = useState<DeviceFilter | null>(null)
131
+ const [filter, setFilter] = useState<AccountFilter | DeviceFilter | null>(
132
+ null
133
+ )
131
134
 
132
135
  if (devices.length === 0) {
133
136
  return <EmptyPlaceholder>{t.noDevicesMessage}</EmptyPlaceholder>
@@ -138,8 +141,17 @@ function Content(props: {
138
141
  return true
139
142
  }
140
143
 
144
+ if (filter === 'account_issues') {
145
+ return (
146
+ device.errors.filter((error) => 'is_connected_account_error' in error)
147
+ .length > 0
148
+ )
149
+ }
150
+
141
151
  if (filter === 'device_issues') {
142
- return device.errors.length > 0
152
+ return (
153
+ device.errors.filter((error) => 'is_device_error' in error).length > 0
154
+ )
143
155
  }
144
156
 
145
157
  return true
@@ -0,0 +1,11 @@
1
+ interface HiddenDevicesOverlayProps {
2
+ visible: boolean
3
+ }
4
+
5
+ export function HiddenDevicesOverlay({ visible }: HiddenDevicesOverlayProps) {
6
+ if (!visible) {
7
+ return null
8
+ }
9
+
10
+ return <div className='hidden-devices-overlay' />
11
+ }
@@ -0,0 +1,32 @@
1
+ import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
2
+
3
+ interface ShowAllDevicesButtonProps {
4
+ onClick: () => void
5
+ visible: boolean
6
+ expanded: boolean
7
+ totalDeviceCount: number
8
+ }
9
+
10
+ export function ShowAllDevicesButton({
11
+ onClick,
12
+ visible,
13
+ expanded,
14
+ totalDeviceCount,
15
+ }: ShowAllDevicesButtonProps) {
16
+ if (!visible) {
17
+ return null
18
+ }
19
+
20
+ const label = expanded ? t.showLess : t.showAll(totalDeviceCount)
21
+
22
+ return (
23
+ <button className='show-all-devices-button' onClick={onClick}>
24
+ <ChevronRightIcon /> {label}
25
+ </button>
26
+ )
27
+ }
28
+
29
+ const t = {
30
+ showLess: 'Show less',
31
+ showAll: (count: number) => `See all ${count} devices`,
32
+ }
@@ -0,0 +1,96 @@
1
+ import classNames from 'classnames'
2
+ import type { DeviceModel } from 'seamapi'
3
+
4
+ import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
5
+ import { HiddenDevicesOverlay } from 'lib/seam/components/SupportedDeviceTable/HiddenDevicesOverlay.js'
6
+ import { ShowAllDevicesButton } from 'lib/seam/components/SupportedDeviceTable/ShowAllDevicesButton.js'
7
+ import { SupportedDeviceRow } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.js'
8
+ import { useDeviceProvider } from 'lib/seam/components/SupportedDeviceTable/use-device-provider.js'
9
+ import { useToggle } from 'lib/ui/use-toggle.js'
10
+
11
+ /**
12
+ * How many device models required before we collapse
13
+ * the list, and require the user to click to
14
+ * view all.
15
+ */
16
+ const maxDevicesBeforeCollapsing = 3
17
+
18
+ export interface SupportedDeviceBrandSectionProps {
19
+ brand: string
20
+ deviceModels: DeviceModel[]
21
+ }
22
+
23
+ export function SupportedDeviceBrandSection({
24
+ brand,
25
+ deviceModels,
26
+ }: SupportedDeviceBrandSectionProps): JSX.Element | null {
27
+ const deviceProvider = useDeviceProvider(brand)
28
+
29
+ const [expanded, toggleExpand] = useToggle()
30
+
31
+ const canExpand = deviceModels.length > maxDevicesBeforeCollapsing
32
+
33
+ const visibleDevices = () => {
34
+ if (!canExpand || expanded) {
35
+ return deviceModels
36
+ }
37
+
38
+ return deviceModels.filter(
39
+ (_deviceModel, index) => index < maxDevicesBeforeCollapsing
40
+ )
41
+ }
42
+
43
+ const handleHeaderClick = () => {
44
+ if (!canExpand) {
45
+ return
46
+ }
47
+
48
+ toggleExpand()
49
+ }
50
+
51
+ return (
52
+ <div
53
+ className={classNames('seam-brand-section', {
54
+ 'can-expand': canExpand,
55
+ expanded,
56
+ })}
57
+ >
58
+ <div className='seam-header' onClick={handleHeaderClick}>
59
+ <img
60
+ src={deviceProvider.image_url}
61
+ alt={brand}
62
+ className='seam-brand-image'
63
+ />
64
+ <h5 className='seam-brand-name'>
65
+ {deviceProvider.display_name} {t.devices}
66
+ </h5>
67
+ {canExpand && <ChevronRightIcon className='chevron' />}
68
+ </div>
69
+ <div className='seam-supported-device-table-content'>
70
+ {visibleDevices().map((deviceModel, index) => (
71
+ <SupportedDeviceRow
72
+ key={[
73
+ deviceModel.main_category,
74
+ deviceModel.brand,
75
+ deviceModel.model_name,
76
+ deviceModel.manufacturer_model_id,
77
+ index,
78
+ ].join(':')}
79
+ deviceModel={deviceModel}
80
+ />
81
+ ))}
82
+ </div>
83
+ <ShowAllDevicesButton
84
+ visible={canExpand}
85
+ onClick={toggleExpand}
86
+ expanded={expanded}
87
+ totalDeviceCount={deviceModels.length}
88
+ />
89
+ <HiddenDevicesOverlay visible={canExpand && !expanded} />
90
+ </div>
91
+ )
92
+ }
93
+
94
+ const t = {
95
+ devices: 'Devices',
96
+ }
@@ -1,25 +1,33 @@
1
- import { SupportedDeviceHeader } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceHeader.js'
2
- import { SupportedDeviceRow } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.js'
1
+ import type { DeviceModel } from 'seamapi'
2
+
3
+ import { SupportedDeviceBrandSection } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceBrandSection.js'
4
+ import { SupportedDeviceFilterResultRow } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceFilterResultRowProps.js'
3
5
  import {
4
6
  type DeviceModelFilters,
5
7
  useFilteredDeviceModels,
6
8
  } from 'lib/seam/components/SupportedDeviceTable/use-filtered-device-models.js'
9
+ import type { UseDeviceModelsData } from 'lib/seam/device-models/use-device-models.js'
7
10
  import { Button } from 'lib/ui/Button.js'
8
11
 
9
12
  interface SupportedDeviceContentProps {
10
13
  filterValue: string
11
14
  resetFilterValue: () => void
12
15
  filters: DeviceModelFilters
16
+ brands: string[]
13
17
  }
14
18
 
15
19
  export function SupportedDeviceContent({
16
20
  resetFilterValue,
17
21
  filterValue,
18
22
  filters,
23
+ brands,
19
24
  }: SupportedDeviceContentProps): JSX.Element | null {
20
25
  const { deviceModels, isLoading, isError, refetch } = useFilteredDeviceModels(
21
- filterValue,
22
- filters
26
+ {
27
+ filterValue,
28
+ filters,
29
+ brands,
30
+ }
23
31
  )
24
32
 
25
33
  if (isLoading) {
@@ -51,12 +59,29 @@ export function SupportedDeviceContent({
51
59
  return null
52
60
  }
53
61
 
54
- return (
55
- <table className='seam-supported-device-table-content'>
56
- <SupportedDeviceHeader />
57
- <tbody>
62
+ const isEmpty = deviceModels.length === 0
63
+ if (isEmpty) {
64
+ return (
65
+ <div className='seam-supported-device-table-content'>
66
+ <EmptyResult
67
+ filterValue={filterValue}
68
+ resetFilterValue={resetFilterValue}
69
+ />
70
+ </div>
71
+ )
72
+ }
73
+
74
+ // If there are no active filters or search, show all the rows without any brand sections.
75
+ const hasFilters =
76
+ filterValue.trim() !== '' ||
77
+ filters.supportedOnly ||
78
+ filters.category !== null ||
79
+ filters.brand !== null
80
+ if (hasFilters) {
81
+ return (
82
+ <div className='seam-supported-device-table-content'>
58
83
  {deviceModels.map((deviceModel, index) => (
59
- <SupportedDeviceRow
84
+ <SupportedDeviceFilterResultRow
60
85
  key={[
61
86
  deviceModel.main_category,
62
87
  deviceModel.brand,
@@ -67,14 +92,22 @@ export function SupportedDeviceContent({
67
92
  deviceModel={deviceModel}
68
93
  />
69
94
  ))}
70
- {deviceModels.length === 0 && (
71
- <EmptyResult
72
- filterValue={filterValue}
73
- resetFilterValue={resetFilterValue}
95
+ </div>
96
+ )
97
+ }
98
+
99
+ return (
100
+ <>
101
+ {Object.entries(groupDeviceModelsByBrand(deviceModels)).map(
102
+ ([brand, models]) => (
103
+ <SupportedDeviceBrandSection
104
+ key={brand}
105
+ brand={brand}
106
+ deviceModels={models}
74
107
  />
75
- )}
76
- </tbody>
77
- </table>
108
+ )
109
+ )}
110
+ </>
78
111
  )
79
112
  }
80
113
 
@@ -110,6 +143,19 @@ function EmptyResult({
110
143
  )
111
144
  }
112
145
 
146
+ function groupDeviceModelsByBrand(
147
+ deviceModels: UseDeviceModelsData
148
+ ): Record<string, DeviceModel[]> {
149
+ const result: Record<string, DeviceModel[]> = {}
150
+
151
+ for (const model of deviceModels) {
152
+ const { brand } = model
153
+ const list = result[brand] ?? []
154
+ result[brand] = [...list, model]
155
+ }
156
+ return result
157
+ }
158
+
113
159
  const t = {
114
160
  loading: 'Loading device models...',
115
161
  retry: 'Retry',
@@ -0,0 +1,27 @@
1
+ import { SupportedDeviceRow } from 'lib/seam/components/SupportedDeviceTable/SupportedDeviceRow.js'
2
+ import type { UseDeviceModelsData } from 'lib/seam/device-models/use-device-models.js'
3
+
4
+ interface SupportedDeviceContentRowsProps {
5
+ deviceModels: UseDeviceModelsData
6
+ }
7
+
8
+ export function SupportedDeviceContentRows({
9
+ deviceModels,
10
+ }: SupportedDeviceContentRowsProps): JSX.Element | null {
11
+ return (
12
+ <div className='seam-supported-device-table-content'>
13
+ {deviceModels.map((deviceModel, index) => (
14
+ <SupportedDeviceRow
15
+ key={[
16
+ deviceModel.main_category,
17
+ deviceModel.brand,
18
+ deviceModel.model_name,
19
+ deviceModel.manufacturer_model_id,
20
+ index,
21
+ ].join(':')}
22
+ deviceModel={deviceModel}
23
+ />
24
+ ))}
25
+ </div>
26
+ )
27
+ }