@iobroker/adapter-react-v5 7.0.1 → 7.1.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 (314) hide show
  1. package/Components/404.d.ts +3 -2
  2. package/Components/404.js +16 -15
  3. package/Components/ColorPicker.d.ts +22 -8
  4. package/Components/ColorPicker.js +34 -17
  5. package/Components/ComplexCron.js +24 -24
  6. package/Components/CopyToClipboard.d.ts +10 -1
  7. package/Components/CopyToClipboard.js +17 -8
  8. package/Components/CustomModal.d.ts +1 -1
  9. package/Components/CustomModal.js +8 -8
  10. package/Components/FileBrowser.d.ts +11 -11
  11. package/Components/FileBrowser.js +173 -164
  12. package/Components/FileViewer.js +34 -23
  13. package/Components/Icon.d.ts +16 -2
  14. package/Components/Icon.js +19 -8
  15. package/Components/IconPicker.js +10 -14
  16. package/Components/IconSelector.d.ts +1 -1
  17. package/Components/IconSelector.js +64 -74
  18. package/Components/Image.d.ts +8 -4
  19. package/Components/Image.js +13 -32
  20. package/Components/Loader.d.ts +2 -2
  21. package/Components/Loader.js +244 -241
  22. package/Components/Loaders/MV.d.ts +6 -1
  23. package/Components/Loaders/MV.js +23 -7
  24. package/Components/Loaders/PT.d.ts +7 -2
  25. package/Components/Loaders/PT.js +123 -110
  26. package/Components/Loaders/Vendor.d.ts +2 -2
  27. package/Components/Loaders/Vendor.js +22 -14
  28. package/Components/Logo.js +16 -18
  29. package/Components/MDUtils.d.ts +1 -1
  30. package/Components/MDUtils.js +8 -4
  31. package/Components/ObjectBrowser.d.ts +49 -38
  32. package/Components/ObjectBrowser.js +757 -494
  33. package/Components/Router.d.ts +1 -3
  34. package/Components/Router.js +3 -1
  35. package/Components/SaveCloseButtons.d.ts +3 -3
  36. package/Components/SaveCloseButtons.js +3 -3
  37. package/Components/Schedule.d.ts +15 -15
  38. package/Components/Schedule.js +177 -154
  39. package/Components/SelectWithIcon.d.ts +2 -2
  40. package/Components/SelectWithIcon.js +45 -34
  41. package/Components/SimpleCron/index.js +83 -43
  42. package/Components/TabContainer.js +2 -2
  43. package/Components/TabContent.js +1 -1
  44. package/Components/TabHeader.js +1 -1
  45. package/Components/TableResize.d.ts +2 -2
  46. package/Components/TableResize.js +5 -5
  47. package/Components/TextWithIcon.d.ts +1 -1
  48. package/Components/TextWithIcon.js +10 -8
  49. package/Components/ToggleThemeMenu.d.ts +2 -2
  50. package/Components/ToggleThemeMenu.js +3 -3
  51. package/Components/TreeTable.d.ts +18 -18
  52. package/Components/TreeTable.js +76 -72
  53. package/Components/UploadImage.d.ts +2 -2
  54. package/Components/UploadImage.js +330 -326
  55. package/Components/Utils.d.ts +42 -22
  56. package/Components/Utils.js +66 -65
  57. package/Components/withWidth.d.ts +2 -2
  58. package/Components/withWidth.js +10 -6
  59. package/Dialogs/ComplexCron.d.ts +2 -2
  60. package/Dialogs/ComplexCron.js +3 -3
  61. package/Dialogs/Confirm.d.ts +4 -4
  62. package/Dialogs/Confirm.js +18 -8
  63. package/Dialogs/Cron.d.ts +3 -3
  64. package/Dialogs/Cron.js +21 -17
  65. package/Dialogs/Error.d.ts +3 -3
  66. package/Dialogs/Error.js +6 -4
  67. package/Dialogs/Message.d.ts +3 -3
  68. package/Dialogs/Message.js +6 -4
  69. package/Dialogs/SelectFile.d.ts +4 -4
  70. package/Dialogs/SelectFile.js +6 -4
  71. package/Dialogs/SelectID.d.ts +12 -10
  72. package/Dialogs/SelectID.js +12 -8
  73. package/Dialogs/SimpleCron.d.ts +2 -2
  74. package/Dialogs/SimpleCron.js +2 -2
  75. package/Dialogs/TextInput.d.ts +2 -2
  76. package/Dialogs/TextInput.js +3 -3
  77. package/GenericApp.d.ts +19 -13
  78. package/GenericApp.js +177 -134
  79. package/LICENSE +22 -22
  80. package/LegacyConnection.d.ts +240 -248
  81. package/LegacyConnection.js +500 -525
  82. package/Prompt.js +7 -7
  83. package/README.md +1239 -1166
  84. package/Theme.d.ts +1 -1
  85. package/Theme.js +9 -12
  86. package/assets/devices.json +1 -0
  87. package/assets/lamp_ceiling.svg +8 -8
  88. package/assets/lamp_table.svg +7 -7
  89. package/assets/no_icon.svg +9 -9
  90. package/assets/rooms.json +1 -0
  91. package/craco-module-federation.js +62 -71
  92. package/i18n/de.json +434 -431
  93. package/i18n/en.json +434 -431
  94. package/i18n/es.json +434 -431
  95. package/i18n/fr.json +434 -431
  96. package/i18n/it.json +434 -431
  97. package/i18n/nl.json +434 -431
  98. package/i18n/pl.json +434 -431
  99. package/i18n/pt.json +434 -431
  100. package/i18n/ru.json +434 -431
  101. package/i18n/uk.json +434 -431
  102. package/i18n/zh-cn.json +434 -431
  103. package/i18n.d.ts +26 -19
  104. package/i18n.js +28 -22
  105. package/icons/IconAdapter.js +2 -2
  106. package/icons/IconAlias.js +2 -2
  107. package/icons/IconChannel.js +2 -2
  108. package/icons/IconClearFilter.js +2 -2
  109. package/icons/IconClosed.js +2 -2
  110. package/icons/IconCopy.js +2 -2
  111. package/icons/IconDevice.js +2 -2
  112. package/icons/IconDocument.js +2 -2
  113. package/icons/IconDocumentReadOnly.js +2 -2
  114. package/icons/IconExpert.js +2 -2
  115. package/icons/IconFx.js +2 -2
  116. package/icons/IconInstance.js +2 -2
  117. package/icons/IconLogout.js +2 -2
  118. package/icons/IconNoIcon.js +2 -2
  119. package/icons/IconOpen.d.ts +2 -2
  120. package/icons/IconOpen.js +2 -2
  121. package/icons/IconProps.d.ts +4 -3
  122. package/icons/IconState.d.ts +2 -2
  123. package/icons/IconState.js +2 -2
  124. package/index.css +56 -55
  125. package/modulefederation.admin.config.js +31 -31
  126. package/package.json +5 -5
  127. package/src/AdminConnection.tsx +3 -3
  128. package/src/Components/404.tsx +122 -121
  129. package/src/Components/ColorPicker.tsx +343 -315
  130. package/src/Components/ComplexCron.tsx +544 -507
  131. package/src/Components/CopyToClipboard.tsx +178 -165
  132. package/src/Components/CustomModal.tsx +170 -163
  133. package/src/Components/FileBrowser.tsx +2550 -2414
  134. package/src/Components/FileViewer.tsx +412 -393
  135. package/src/Components/Icon.tsx +238 -210
  136. package/src/Components/IconPicker.tsx +165 -149
  137. package/src/Components/IconSelector.tsx +2220 -2202
  138. package/src/Components/Image.tsx +193 -176
  139. package/src/Components/Loader.tsx +328 -304
  140. package/src/Components/Logo.tsx +176 -166
  141. package/src/Components/MDUtils.tsx +104 -100
  142. package/src/Components/ObjectBrowser.tsx +8935 -8032
  143. package/src/Components/Router.tsx +90 -90
  144. package/src/Components/SaveCloseButtons.tsx +117 -113
  145. package/src/Components/Schedule.tsx +1962 -1724
  146. package/src/Components/SelectWithIcon.tsx +239 -197
  147. package/src/Components/TabContainer.tsx +57 -55
  148. package/src/Components/TabContent.tsx +38 -37
  149. package/src/Components/TabHeader.tsx +20 -19
  150. package/src/Components/TableResize.tsx +274 -259
  151. package/src/Components/TextWithIcon.tsx +159 -148
  152. package/src/Components/ToggleThemeMenu.tsx +52 -34
  153. package/src/Components/TreeTable.tsx +1002 -919
  154. package/src/Components/UploadImage.tsx +631 -599
  155. package/src/Components/Utils.tsx +1802 -1794
  156. package/src/Components/loader.css +231 -222
  157. package/src/Components/withWidth.tsx +32 -21
  158. package/src/Connection.tsx +5 -7
  159. package/src/Dialogs/ComplexCron.tsx +123 -129
  160. package/src/Dialogs/Confirm.tsx +185 -162
  161. package/src/Dialogs/Cron.tsx +192 -182
  162. package/src/Dialogs/Error.tsx +67 -72
  163. package/src/Dialogs/Message.tsx +73 -71
  164. package/src/Dialogs/SelectFile.tsx +280 -270
  165. package/src/Dialogs/SelectID.tsx +310 -298
  166. package/src/Dialogs/SimpleCron.tsx +100 -100
  167. package/src/Dialogs/TextInput.tsx +99 -107
  168. package/src/GenericApp.tsx +1076 -976
  169. package/src/LegacyConnection.tsx +3719 -3589
  170. package/src/Prompt.tsx +22 -20
  171. package/src/Theme.tsx +472 -479
  172. package/src/icons/IconAdapter.tsx +22 -20
  173. package/src/icons/IconAlias.tsx +22 -20
  174. package/src/icons/IconChannel.tsx +60 -21
  175. package/src/icons/IconClearFilter.tsx +24 -22
  176. package/src/icons/IconClosed.tsx +22 -17
  177. package/src/icons/IconCopy.tsx +21 -16
  178. package/src/icons/IconDevice.tsx +126 -27
  179. package/src/icons/IconDocument.tsx +22 -17
  180. package/src/icons/IconDocumentReadOnly.tsx +27 -18
  181. package/src/icons/IconExpert.tsx +26 -18
  182. package/src/icons/IconFx.tsx +38 -36
  183. package/src/icons/IconInstance.tsx +22 -20
  184. package/src/icons/IconLogout.tsx +32 -30
  185. package/src/icons/IconNoIcon.tsx +21 -19
  186. package/src/icons/IconOpen.tsx +22 -17
  187. package/src/icons/IconProps.tsx +16 -15
  188. package/src/icons/IconState.tsx +38 -17
  189. package/src/index.css +56 -55
  190. package/tasks.js +91 -0
  191. package/types.d.ts +141 -134
  192. package/Components/Loaders/PT.css +0 -109
  193. package/Components/Loaders/Vendor.css +0 -13
  194. package/Components/loader.css +0 -222
  195. package/Components/types.d.ts +0 -82
  196. package/assets/devices/Alarm Systems.svg +0 -19
  197. package/assets/devices/Amplifier.svg +0 -22
  198. package/assets/devices/Awnings.svg +0 -5
  199. package/assets/devices/Battery Status.svg +0 -5
  200. package/assets/devices/Ceiling Spotlights.svg +0 -16
  201. package/assets/devices/Chandelier.svg +0 -7
  202. package/assets/devices/Climate.svg +0 -12
  203. package/assets/devices/Coffee Makers.svg +0 -6
  204. package/assets/devices/Cold Water.svg +0 -31
  205. package/assets/devices/Computer.svg +0 -21
  206. package/assets/devices/Consumption.svg +0 -8
  207. package/assets/devices/Curtains.svg +0 -43
  208. package/assets/devices/Dishwashers.svg +0 -12
  209. package/assets/devices/Doors.svg +0 -6
  210. package/assets/devices/Doorstep.svg +0 -35
  211. package/assets/devices/Dryer.svg +0 -14
  212. package/assets/devices/Fan.svg +0 -20
  213. package/assets/devices/Floor Lamps.svg +0 -5
  214. package/assets/devices/Garage Doors.svg +0 -9
  215. package/assets/devices/Gates.svg +0 -32
  216. package/assets/devices/Hairdryer.svg +0 -23
  217. package/assets/devices/Handle.svg +0 -6
  218. package/assets/devices/Hanging Lamps.svg +0 -9
  219. package/assets/devices/Heater.svg +0 -44
  220. package/assets/devices/Hoods.svg +0 -12
  221. package/assets/devices/Hot Water.svg +0 -10
  222. package/assets/devices/Humidity.svg +0 -41
  223. package/assets/devices/Iron.svg +0 -5
  224. package/assets/devices/Irrigation.svg +0 -23
  225. package/assets/devices/Led Strip.svg +0 -31
  226. package/assets/devices/Light.svg +0 -30
  227. package/assets/devices/Lightings.svg +0 -46
  228. package/assets/devices/Lock.svg +0 -19
  229. package/assets/devices/Louvre.svg +0 -7
  230. package/assets/devices/Mowing Machine.svg +0 -9
  231. package/assets/devices/Music.svg +0 -13
  232. package/assets/devices/Outdoor Blinds.svg +0 -7
  233. package/assets/devices/People.svg +0 -19
  234. package/assets/devices/Pool.svg +0 -8
  235. package/assets/devices/Power Consumption.svg +0 -13
  236. package/assets/devices/Printer.svg +0 -10
  237. package/assets/devices/Pump.svg +0 -10
  238. package/assets/devices/Receiver.svg +0 -19
  239. package/assets/devices/Sconces.svg +0 -10
  240. package/assets/devices/Security.svg +0 -34
  241. package/assets/devices/Shading.svg +0 -5
  242. package/assets/devices/Shutters.svg +0 -11
  243. package/assets/devices/SmokeDetector.svg +0 -13
  244. package/assets/devices/Sockets.svg +0 -13
  245. package/assets/devices/Speaker.svg +0 -35
  246. package/assets/devices/Stove.svg +0 -12
  247. package/assets/devices/Table Lamps.svg +0 -12
  248. package/assets/devices/Temperature Sensors.svg +0 -28
  249. package/assets/devices/Tv.svg +0 -8
  250. package/assets/devices/Vacuum Cleaner.svg +0 -16
  251. package/assets/devices/Ventilation.svg +0 -12
  252. package/assets/devices/Washing Machines.svg +0 -16
  253. package/assets/devices/Water Consumption.svg +0 -6
  254. package/assets/devices/Water Heater.svg +0 -8
  255. package/assets/devices/Water.svg +0 -40
  256. package/assets/devices/Weather.svg +0 -28
  257. package/assets/devices/Window.svg +0 -8
  258. package/assets/rooms/Anteroom.svg +0 -53
  259. package/assets/rooms/Attic.svg +0 -21
  260. package/assets/rooms/Balcony.svg +0 -13
  261. package/assets/rooms/Barn.svg +0 -6
  262. package/assets/rooms/Basement.svg +0 -5
  263. package/assets/rooms/Bathroom.svg +0 -38
  264. package/assets/rooms/Bedroom.svg +0 -5
  265. package/assets/rooms/Boiler Room.svg +0 -13
  266. package/assets/rooms/Carport.svg +0 -17
  267. package/assets/rooms/Cellar.svg +0 -89
  268. package/assets/rooms/Chamber.svg +0 -9
  269. package/assets/rooms/Corridor.svg +0 -53
  270. package/assets/rooms/Dining Area.svg +0 -37
  271. package/assets/rooms/Dining Room.svg +0 -37
  272. package/assets/rooms/Dining.svg +0 -37
  273. package/assets/rooms/Dressing Room.svg +0 -5
  274. package/assets/rooms/Driveway.svg +0 -15
  275. package/assets/rooms/Entrance.svg +0 -44
  276. package/assets/rooms/Equipment Room.svg +0 -15
  277. package/assets/rooms/Front Yard.svg +0 -64
  278. package/assets/rooms/Gallery.svg +0 -14
  279. package/assets/rooms/Garage.svg +0 -20
  280. package/assets/rooms/Garden.svg +0 -13
  281. package/assets/rooms/Ground Floor.svg +0 -95
  282. package/assets/rooms/Guest Bathroom.svg +0 -33
  283. package/assets/rooms/Guest Room.svg +0 -5
  284. package/assets/rooms/Gym.svg +0 -5
  285. package/assets/rooms/Hall.svg +0 -19
  286. package/assets/rooms/Home Theater.svg +0 -8
  287. package/assets/rooms/Kitchen.svg +0 -18
  288. package/assets/rooms/Laundry Room.svg +0 -12
  289. package/assets/rooms/Living Area.svg +0 -11
  290. package/assets/rooms/Living Room.svg +0 -10
  291. package/assets/rooms/Locker Room.svg +0 -17
  292. package/assets/rooms/Nursery.svg +0 -5
  293. package/assets/rooms/Office.svg +0 -8
  294. package/assets/rooms/Outdoors.svg +0 -7
  295. package/assets/rooms/Playroom.svg +0 -6
  296. package/assets/rooms/Pool.svg +0 -8
  297. package/assets/rooms/Rear Wall.svg +0 -30
  298. package/assets/rooms/Second Floor.svg +0 -95
  299. package/assets/rooms/Shed.svg +0 -16
  300. package/assets/rooms/Sleeping Area.svg +0 -22
  301. package/assets/rooms/Stairway.svg +0 -5
  302. package/assets/rooms/Stairwell.svg +0 -15
  303. package/assets/rooms/Storeroom.svg +0 -5
  304. package/assets/rooms/Summer House.svg +0 -27
  305. package/assets/rooms/Swimming Pool.svg +0 -21
  306. package/assets/rooms/Terrace.svg +0 -7
  307. package/assets/rooms/Toilet.svg +0 -10
  308. package/assets/rooms/Upstairs.svg +0 -6
  309. package/assets/rooms/Wardrobe.svg +0 -60
  310. package/assets/rooms/Washroom.svg +0 -20
  311. package/assets/rooms/Wc.svg +0 -10
  312. package/assets/rooms/Windscreen.svg +0 -60
  313. package/assets/rooms/Workshop.svg +0 -23
  314. package/assets/rooms/Workspace.svg +0 -8
@@ -1,919 +1,1002 @@
1
- import React, { Component } from 'react';
2
-
3
- import { HexColorPicker as ColorPicker } from 'react-colorful';
4
-
5
- import {
6
- Fab,
7
- Table,
8
- TableBody,
9
- TableCell,
10
- TableHead,
11
- TableRow,
12
- TableSortLabel,
13
- IconButton,
14
- Select,
15
- MenuItem,
16
- TextField,
17
- Checkbox,
18
- Dialog,
19
- } from '@mui/material';
20
-
21
- import {
22
- Edit as IconEdit,
23
- Delete as IconDelete,
24
- NavigateNext as IconExpand,
25
- ExpandMore as IconCollapse,
26
- Check as IconCheck,
27
- Close as IconClose,
28
- Add as IconAdd,
29
- ViewHeadline as IconList,
30
- Colorize as IconColor,
31
- } from '@mui/icons-material';
32
-
33
- import type Connection from '../Connection';
34
-
35
- import DialogSelectID from '../Dialogs/SelectID';
36
- import Utils from './Utils';
37
- import { IobTheme } from '../types';
38
-
39
- function getAttr(
40
- obj: Record<string, any>,
41
- attr: string | string[],
42
- lookup?: Record<string, string>,
43
- ): any {
44
- if (typeof attr === 'string') {
45
- attr = attr.split('.');
46
- }
47
-
48
- if (!obj) {
49
- return null;
50
- }
51
-
52
- if (attr.length === 1) {
53
- if (lookup && lookup[obj[attr[0]]]) {
54
- return lookup[obj[attr[0]]];
55
- }
56
- return obj[attr[0]];
57
- }
58
-
59
- const name: string = attr.shift() as string;
60
- return getAttr(obj[name], attr);
61
- }
62
-
63
- function setAttr(
64
- obj: Record<string, any>,
65
- attr: string | string[],
66
- value: any,
67
- ): void {
68
- if (typeof attr === 'string') {
69
- attr = attr.split('.');
70
- }
71
-
72
- if (attr.length === 1) {
73
- return obj[attr[0]] = value;
74
- }
75
- const name: string = attr.shift() as string;
76
- if (obj[name] === null || obj[name] === undefined) {
77
- obj[name] = {};
78
- }
79
- return setAttr(obj[name], attr, value);
80
- }
81
-
82
- const styles: Record<string, any> = {
83
- tableContainer: {
84
- width: '100%',
85
- height: '100%',
86
- overflow: 'auto',
87
- },
88
- table: {
89
- width: '100%',
90
- minWidth: 800,
91
- maxWidth: 1920,
92
- },
93
- cell: {
94
- paddingTop: 0,
95
- paddingBottom: 0,
96
- paddingLeft: 4,
97
- paddingRight: 4,
98
- },
99
- rowMainWithChildren: {
100
-
101
- },
102
- rowMainWithoutChildren: {
103
-
104
- },
105
- rowNoEdit: {
106
- opacity: 0.3,
107
- },
108
- cellExpand: {
109
- width: 30,
110
- },
111
- cellButton: {
112
- width: 30,
113
- },
114
- cellHeader: {
115
- fontWeight: 'bold',
116
- background: (theme: IobTheme) => (theme.palette.mode === 'dark' ? '#888' : '#888'),
117
- color: (theme: IobTheme) => (theme.palette.mode === 'dark' ? '#EEE' : '#111'),
118
- height: 48,
119
- wordBreak: 'break-word',
120
- whiteSpace: 'pre',
121
- },
122
- width_name_nicknames: {
123
- maxWidth: 150,
124
- },
125
- width_ioType: {
126
- maxWidth: 100,
127
- },
128
- width_type: {
129
- maxWidth: 100,
130
- },
131
- width_displayTraits: {
132
- maxWidth: 100,
133
- },
134
- width_roomHint: {
135
- maxWidth: 100,
136
- },
137
- rowSecondary: {
138
- fontStyle: 'italic',
139
- },
140
- cellSecondary: {
141
- fontSize: 10,
142
- },
143
- visuallyHidden: {
144
- border: 0,
145
- clip: 'rect(0 0 0 0)',
146
- height: 1,
147
- margin: -1,
148
- overflow: 'hidden',
149
- padding: 0,
150
- position: 'absolute',
151
- top: 20,
152
- width: 1,
153
- },
154
- fieldEditWithButton: {
155
- width: 'calc(100% - 33px)',
156
- display: 'inline-block',
157
- },
158
- fieldEdit: {
159
- width: '100%',
160
- display: 'inline-block',
161
- lineHeight: '50px',
162
- verticalAlign: 'middle',
163
- },
164
- fieldButton: {
165
- width: 30,
166
- display: 'inline-block',
167
- },
168
- colorDialog: {
169
- overflow: 'hidden',
170
- padding: 15,
171
- },
172
- subText: {
173
- fontSize: 10,
174
- fontStyle: 'italic',
175
- },
176
- glow: {
177
- animation: 'glow 0.2s 2 alternate',
178
- },
179
- };
180
-
181
- function descendingComparator(
182
- a: Record<string, any>,
183
- b: Record<string, any>,
184
- orderBy: string,
185
- lookup?: Record<string, string>,
186
- ): number {
187
- const _a = getAttr(a, orderBy, lookup) || '';
188
- const _b = getAttr(b, orderBy, lookup) || '';
189
-
190
- if (_b < _a) {
191
- return -1;
192
- }
193
- if (_b > _a) {
194
- return 1;
195
- }
196
- return 0;
197
- }
198
-
199
- function getComparator(
200
- order: 'desc' | 'asc',
201
- orderBy: string,
202
- lookup?: Record<string, string>,
203
- ): (a: Record<string, any>, b: Record<string, any>) => number {
204
- return order === 'desc'
205
- ? (a, b) => descendingComparator(a, b, orderBy, lookup)
206
- : (a, b) => -descendingComparator(a, b, orderBy, lookup);
207
- }
208
-
209
- function stableSort(
210
- array: Record<string, any>[],
211
- comparator: (a: Record<string, any>, b: Record<string, any>) => number,
212
- ): Record<string, any>[] {
213
- const stabilizedThis: { e: Record<string, any>; i: number }[] =
214
- array.map((el, index) => ({ e: el, i: index }));
215
-
216
- stabilizedThis.sort((a, b) => {
217
- const order = comparator(a.e, b.e);
218
- if (order) {
219
- return order;
220
- }
221
- return a.i - b.i;
222
- });
223
-
224
- return stabilizedThis.map(item => item.e);
225
- }
226
-
227
- interface Column {
228
- cellStyle?: Record<string, any>;
229
- editComponent?: React.FC<{ value: any; rowData: Record<string, any>; onChange: (newValue: any) => void }>;
230
- field: string;
231
- headerStyle?: Record<string, any>;
232
- hidden?: boolean;
233
- lookup?: Record<string, string>;
234
- editable?: boolean | 'never';
235
- title?: string;
236
- type?: 'string' | 'boolean' | 'numeric' | 'icon' | 'oid' | 'color';
237
- subField?: string;
238
- subLookup?: Record<string, string>;
239
- subStyle?: Record<string, any>;
240
- }
241
-
242
- interface TreeTableProps {
243
- data: Record<string, any>[];
244
- className?: string;
245
- /** name of table to save settings in localStorage */
246
- name?: string;
247
- columns: Column[];
248
- noSort?: boolean;
249
- onUpdate?: ((newData: Record<string, any>, oldData: Record<string, any>) => void) | ((addNew: true) => void);
250
- onDelete?: (oldData: Record<string, any>) => void;
251
- /** hide add button */
252
- noAdd?: boolean;
253
- themeType?: string;
254
- glowOnChange?: boolean;
255
- /** only if an oid type is used */
256
- socket?: Connection;
257
- /** Shift in pixels for every level */
258
- levelShift?: number;
259
- adapterName: string;
260
- theme: IobTheme;
261
- }
262
-
263
- interface TreeTableState {
264
- opened: string[];
265
- editMode: number | false;
266
- deleteMode: number | false;
267
- editData: Record<string, any> | null;
268
- order: 'desc' | 'asc';
269
- update: string[] | null;
270
- orderBy: string;
271
- showSelectColor: boolean;
272
- selectIdValue?: string | null;
273
- showSelectId?: boolean;
274
- data?: Record<string, any>[];
275
- }
276
-
277
- class TreeTable extends Component<TreeTableProps, TreeTableState> {
278
- private selectCallback: ((selected: string) => void) | null = null;
279
-
280
- private updateTimeout: ReturnType<typeof setTimeout> | null = null;
281
-
282
- constructor(props: TreeTableProps) {
283
- super(props);
284
-
285
- let opened = ((window as any)._localStorage || window.localStorage).getItem(this.props.name || 'iob-table') || '[]';
286
- try {
287
- opened = JSON.parse(opened) || [];
288
- } catch (e) {
289
- opened = [];
290
- }
291
- if (!Array.isArray(opened)) {
292
- opened = [];
293
- }
294
-
295
- this.state = {
296
- opened,
297
- editMode: false,
298
- deleteMode: false,
299
- editData: null,
300
- order: 'asc',
301
- update: null,
302
- orderBy: this.props.columns[0].field,
303
- showSelectColor: false,
304
- };
305
- }
306
-
307
- static getDerivedStateFromProps(props: TreeTableProps, state: TreeTableState): Partial<TreeTableState> {
308
- if (props.glowOnChange) {
309
- const update: string[] = [];
310
- let count = 0;
311
- if (props.data && state.data) {
312
- props.data.forEach(line => {
313
- count++;
314
- const oldLine = state.data?.find(it => it.id === line.id);
315
- if (oldLine) {
316
- if (JSON.stringify(oldLine) !== JSON.stringify(line)) {
317
- update.push(line.id);
318
- }
319
- } else {
320
- update.push(line.id);
321
- }
322
- });
323
- }
324
-
325
- if (update.length && update.length !== count) {
326
- return { data: props.data, update };
327
- }
328
- return { data: props.data };
329
- }
330
-
331
- return { data: props.data };
332
- }
333
-
334
- renderCellEdit(
335
- item: Record<string, any>,
336
- col: Column,
337
- ) {
338
- let val = getAttr(item, col.field);
339
- if (Array.isArray(val)) {
340
- val = val[0];
341
- }
342
-
343
- if (col.lookup) {
344
- return this.renderCellEditSelect(col, val);
345
- } if (col.editComponent) {
346
- return this.renderCellEditCustom(col, val, item);
347
- }
348
- if (col.type === 'boolean' || (!col.type && typeof val === 'boolean')) {
349
- return this.renderCellEditBoolean(col, val);
350
- }
351
- if (col.type === 'color') {
352
- return this.renderCellEditColor(col, val);
353
- }
354
- if (col.type === 'oid') {
355
- return this.renderCellEditObjectID(col, val);
356
- }
357
- if (col.type === 'numeric') {
358
- return this.renderCellEditNumber(col, val);
359
- }
360
-
361
- return this.renderCellEditString(col, val);
362
- }
363
-
364
- onChange(col: Column, oldValue: string | number | boolean, newValue: string | number | boolean) {
365
- const editData = this.state.editData ? { ...this.state.editData } : {};
366
- if (newValue === oldValue) {
367
- delete editData[col.field];
368
- } else {
369
- editData[col.field] = newValue;
370
- }
371
- this.setState({ editData });
372
- }
373
-
374
- renderCellEditSelect(
375
- col: Column,
376
- val: string | number,
377
- ) {
378
- return <Select
379
- variant="standard"
380
- onChange={e => this.onChange(col, val, e.target.value)}
381
- value={(this.state.editData && this.state.editData[col.field]) || val}
382
- >
383
- {col.lookup && Object.keys(col.lookup)
384
- .map((v, i) => <MenuItem key={i} value={v}>{col.lookup?.[v]}</MenuItem>)}
385
- </Select>;
386
- }
387
-
388
- renderCellEditString(
389
- col: Column,
390
- val: string,
391
- ) {
392
- return <TextField
393
- variant="standard"
394
- style={styles.fieldEdit}
395
- fullWidth
396
- value={this.state.editData && this.state.editData[col.field] !== undefined ? this.state.editData[col.field] : val}
397
- onChange={e => this.onChange(col, val, e.target.value)}
398
- />;
399
- }
400
-
401
- renderCellEditNumber(
402
- col: Column,
403
- val: number,
404
- ) {
405
- return <TextField
406
- variant="standard"
407
- style={styles.fieldEdit}
408
- type="number"
409
- fullWidth
410
- value={this.state.editData && this.state.editData[col.field] !== undefined ? this.state.editData[col.field] : val}
411
- onChange={e => this.onChange(col, val, e.target.value)}
412
- />;
413
- }
414
-
415
- renderCellEditCustom(
416
- col: Column,
417
- val: any,
418
- item: Record<string, any>,
419
- ) {
420
- const EditComponent = col.editComponent;
421
-
422
- // use new value if exists
423
- if (this.state.editData && this.state.editData[col.field] !== undefined) {
424
- val = this.state.editData[col.field];
425
- item = JSON.parse(JSON.stringify(item));
426
- item[col.field] = val;
427
- }
428
-
429
- return EditComponent ? <EditComponent
430
- value={val}
431
- rowData={item}
432
- onChange={(newVal: any) => this.onChange(col, val, newVal as string | number)}
433
- /> : null;
434
- }
435
-
436
- renderCellEditBoolean(
437
- col: Column,
438
- val: boolean,
439
- ) {
440
- return <Checkbox
441
- checked={this.state.editData && this.state.editData[col.field] !== undefined ? !!this.state.editData[col.field] : !!val}
442
- onChange={e => this.onChange(col, !!val, e.target.checked)}
443
- inputProps={{ 'aria-label': 'checkbox' }}
444
- />;
445
- }
446
-
447
- renderSelectColorDialog() {
448
- return <Dialog
449
- sx={{
450
- '& .MuiPaper-root': styles.root,
451
- '& .MuiPaper-paper': styles.paper,
452
- }}
453
- onClose={() => {
454
- this.selectCallback = null;
455
- this.setState({ showSelectColor: false });
456
- }}
457
- open={this.state.showSelectColor}
458
- >
459
- <ColorPicker
460
- color={this.state.selectIdValue as string}
461
- onChange={color =>
462
- this.setState({ selectIdValue: color }, () =>
463
- this.selectCallback && this.selectCallback(color))}
464
- />
465
- </Dialog>;
466
- }
467
-
468
- renderCellEditColor(
469
- col: Column,
470
- val: string,
471
- ) {
472
- const _val = this.state.editData && this.state.editData[col.field] !== undefined ? this.state.editData[col.field] : val;
473
- return <div style={styles.fieldEdit}>
474
- <TextField
475
- variant="standard"
476
- fullWidth
477
- style={styles.fieldEditWithButton}
478
- value={_val}
479
- inputProps={{ style: { backgroundColor: _val, color: Utils.isUseBright(_val) ? '#FFF' : '#000' } }}
480
- onChange={e => this.onChange(col, !!val, e.target.value)}
481
- />
482
-
483
- <IconButton
484
- style={styles.fieldButton}
485
- onClick={() => {
486
- this.selectCallback = newColor => this.onChange(col, val, newColor);
487
- this.setState({ showSelectColor: true, selectIdValue: val });
488
- }}
489
- size="large"
490
- >
491
- <IconColor />
492
- </IconButton>
493
- </div>;
494
- }
495
-
496
- renderSelectIdDialog() {
497
- if (this.state.showSelectId && this.props.socket) {
498
- return <DialogSelectID
499
- key="tableSelect"
500
- imagePrefix="../.."
501
- dialogName={this.props.adapterName}
502
- themeType={this.props.themeType}
503
- theme={this.props.theme}
504
- socket={this.props.socket as Connection}
505
- selected={this.state.selectIdValue as string}
506
- onClose={() => this.setState({ showSelectId: false })}
507
- onOk={(selected /* , name */) => {
508
- this.setState({ showSelectId: false, selectIdValue: null });
509
- this.selectCallback && this.selectCallback(selected as string);
510
- this.selectCallback = null;
511
- }}
512
- />;
513
- }
514
-
515
- return null;
516
- }
517
-
518
- renderCellEditObjectID(
519
- col: Column,
520
- val: string,
521
- ) {
522
- return <div style={styles.fieldEdit}>
523
- <TextField
524
- variant="standard"
525
- fullWidth
526
- style={styles.fieldEditWithButton}
527
- value={this.state.editData && this.state.editData[col.field] !== undefined ? this.state.editData[col.field] : val}
528
- onChange={e => this.onChange(col, val, e.target.value)}
529
- />
530
-
531
- <IconButton
532
- style={styles.fieldButton}
533
- onClick={() => {
534
- this.selectCallback = selected => this.onChange(col, val, selected);
535
- this.setState({ showSelectId: true, selectIdValue: val });
536
- }}
537
- size="large"
538
- >
539
- <IconList />
540
- </IconButton>
541
- </div>;
542
- }
543
-
544
- static renderCellNonEdit(
545
- item: Record<string, any>,
546
- col: Column,
547
- ) {
548
- let val = getAttr(item, col.field, col.lookup);
549
- if (Array.isArray(val)) {
550
- val = val[0];
551
- }
552
-
553
- if (col.type === 'boolean') {
554
- return <Checkbox
555
- checked={!!val}
556
- disabled
557
- inputProps={{ 'aria-label': 'checkbox' }}
558
- />;
559
- }
560
-
561
- return val;
562
- }
563
-
564
- renderCell(
565
- item: Record<string, any>,
566
- col: Column,
567
- level: number,
568
- i: number,
569
- ) {
570
- if (this.state.editMode === i && col.editable !== 'never' && col.editable !== false) {
571
- return <TableCell
572
- key={col.field}
573
- style={{ ...styles.cell, ...(level ? styles.cellSecondary : undefined), ...col.cellStyle }}
574
- component="th"
575
- >
576
- {this.renderCellEdit(item, col)}
577
- </TableCell>;
578
- }
579
- return <TableCell
580
- key={col.field}
581
- style={{ ...styles.cell, ...(level ? styles.cellSecondary : undefined), ...col.cellStyle }}
582
- component="th"
583
- >
584
- {TreeTable.renderCellNonEdit(item, col)}
585
- </TableCell>;
586
- }
587
-
588
- static renderCellWithSubField(
589
- item: Record<string, any>,
590
- col: Column,
591
- ) {
592
- const main = getAttr(item, col.field, col.lookup);
593
- if (col.subField) {
594
- const sub = getAttr(item, col.subField, col.subLookup);
595
- return <div>
596
- <div style={styles.mainText}>{main}</div>
597
- <div style={{ ...styles.subText, ...(col.subStyle || undefined) }}>{sub}</div>
598
- </div>;
599
- }
600
- return <div>
601
- <div style={styles.mainText}>{main}</div>
602
- </div>;
603
- }
604
-
605
- renderLine(
606
- item: Record<string, any>,
607
- level?: number,
608
- ): React.JSX.Element | React.JSX.Element[] | null {
609
- const levelShift = this.props.levelShift === undefined ? 24 : this.props.levelShift;
610
-
611
- level = level || 0;
612
- const i = this.props.data.indexOf(item);
613
- if (!item) {
614
- return null;
615
- }
616
- if (!level && item.parentId) {
617
- return null;
618
- }
619
- if (level && !item.parentId) {
620
- return null; // should never happen
621
- }
622
- // try to find children
623
- const opened = this.state.opened.includes(item.id);
624
- const children = this.props.data.filter(it => it.parentId === item.id);
625
-
626
- const row = <TableRow
627
- key={item.id}
628
- className={`table-row-${(item.id || '').toString().replace(/[.$]/g, '_')}`}
629
- style={{
630
- ...((this.state.update && this.state.update.includes(item.id) && styles.glow) || undefined),
631
- ...styles.row,
632
- ...(level ? styles.rowSecondary : undefined),
633
- ...(!level && children.length ? styles.rowMainWithChildren : undefined),
634
- ...(!level && !children.length ? styles.rowMainWithoutChildren : undefined),
635
- ...(this.state.editMode !== false && this.state.editMode !== i ? styles.rowNoEdit : undefined),
636
- ...(this.state.deleteMode !== false && this.state.deleteMode !== i ? styles.rowNoEdit : undefined),
637
- }}
638
- >
639
- <TableCell
640
- style={{ ...styles.cell, ...styles.cellExpand, ...(level ? styles.cellSecondary : undefined) }}
641
- >
642
- {children.length ? <IconButton
643
- onClick={() => {
644
- const _opened = [...this.state.opened];
645
- const pos = _opened.indexOf(item.id);
646
- if (pos === -1) {
647
- _opened.push(item.id);
648
- _opened.sort();
649
- } else {
650
- _opened.splice(pos, 1);
651
- }
652
-
653
- ((window as any)._localStorage || window.localStorage).setItem(this.props.name || 'iob-table', JSON.stringify(_opened));
654
-
655
- this.setState({ opened: _opened });
656
- }}
657
- size="small"
658
- >
659
- {opened ? <IconCollapse /> : <IconExpand />}
660
- </IconButton> : null}
661
- </TableCell>
662
- <TableCell
663
- scope="row"
664
- style={{
665
- ...styles.cell,
666
- ...(level ? styles.cellSecondary : undefined),
667
- ...this.props.columns[0].cellStyle,
668
- paddingLeft: levelShift * level,
669
- }}
670
- >
671
- {this.props.columns[0].subField ?
672
- TreeTable.renderCellWithSubField(item, this.props.columns[0])
673
- :
674
- getAttr(item, this.props.columns[0].field, this.props.columns[0].lookup)}
675
- </TableCell>
676
-
677
- {this.props.columns.map((col, ii) =>
678
- (!ii && !col.hidden ? null : this.renderCell(item, col, level, i)))}
679
-
680
- {this.props.onUpdate ? <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
681
- {this.state.editMode === i || this.state.deleteMode === i ?
682
- <IconButton
683
- disabled={this.state.editMode !== false && (!this.state.editData || !Object.keys(this.state.editData).length)}
684
- onClick={() => {
685
- if (this.state.editMode !== false) {
686
- const newData = JSON.parse(JSON.stringify(item));
687
- this.state.editData && Object.keys(this.state.editData).forEach(attr =>
688
- setAttr(newData, attr, this.state.editData?.[attr]));
689
- this.setState({ editMode: false }, () => this.props.onUpdate && this.props.onUpdate(newData, item));
690
- } else {
691
- this.setState({ deleteMode: false }, () => this.props.onDelete && this.props.onDelete(item));
692
- }
693
- }}
694
- size="large"
695
- >
696
- <IconCheck />
697
- </IconButton>
698
- :
699
- <IconButton
700
- disabled={this.state.editMode !== false}
701
- onClick={() => this.setState({ editMode: i, editData: null })}
702
- size="large"
703
- >
704
- <IconEdit />
705
- </IconButton>}
706
- </TableCell> : null}
707
-
708
- {this.props.onDelete && !this.props.onUpdate ?
709
- <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
710
- {this.state.deleteMode === i ?
711
- <IconButton
712
- disabled={this.state.editMode !== false && (!this.state.editData || !Object.keys(this.state.editData).length)}
713
- onClick={() => this.setState({ deleteMode: false }, () => this.props.onDelete && this.props.onDelete(item))}
714
- size="large"
715
- >
716
- <IconCheck />
717
- </IconButton>
718
- :
719
- null}
720
- </TableCell> : null}
721
-
722
- {this.props.onUpdate || this.props.onDelete ? <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
723
- {this.state.editMode === i || this.state.deleteMode === i ?
724
- <IconButton
725
- onClick={() => this.setState({ editMode: false, deleteMode: false })}
726
- size="large"
727
- >
728
- <IconClose />
729
- </IconButton>
730
- :
731
- (this.props.onDelete ? <IconButton
732
- disabled={this.state.deleteMode !== false}
733
- onClick={() => this.setState({ deleteMode: i })}
734
- size="large"
735
- >
736
- <IconDelete />
737
- </IconButton> : null)}
738
- </TableCell> : null}
739
- </TableRow>;
740
-
741
- if (!level && opened) {
742
- const items: React.JSX.Element[] = children.map(it =>
743
- this.renderLine(it, level + 1)) as React.JSX.Element[];
744
- items.unshift(row);
745
- return items;
746
- }
747
- return row;
748
- }
749
-
750
- handleRequestSort(property: string) {
751
- const isAsc = this.state.orderBy === property && this.state.order === 'asc';
752
- this.setState({ order: isAsc ? 'desc' : 'asc', orderBy: property });
753
- }
754
-
755
- renderHead() {
756
- return <TableHead>
757
- <TableRow key="headerRow">
758
- <TableCell
759
- component="th"
760
- sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellExpand)}
761
- />
762
- <TableCell
763
- component="th"
764
- sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles[`width_${this.props.columns[0].field.replace(/\./g, '_')}`])}
765
- style={this.props.columns[0].headerStyle || this.props.columns[0].cellStyle}
766
- sortDirection={this.props.noSort ? false : (this.state.orderBy === this.props.columns[0].field ? this.state.order : false)}
767
- >
768
- {this.props.noSort ? null : <TableSortLabel
769
- active={this.state.orderBy === this.props.columns[0].field}
770
- direction={this.state.orderBy === this.props.columns[0].field ? this.state.order : 'asc'}
771
- onClick={() => this.handleRequestSort(this.props.columns[0].field)}
772
- >
773
- {this.props.columns[0].title || this.props.columns[0].field}
774
- {this.state.orderBy === this.props.columns[0].field ?
775
- <span style={styles.visuallyHidden}>
776
- {this.state.order === 'desc' ? 'sorted descending' : 'sorted ascending'}
777
- </span> : null}
778
- </TableSortLabel>}
779
- </TableCell>
780
- {this.props.columns.map((col, i) =>
781
- (!i && !col.hidden ? null : <TableCell
782
- key={col.field}
783
- sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles[`width_${col.field.replace(/\./g, '_')}`])}
784
- style={col.headerStyle || col.cellStyle}
785
- component="th"
786
- >
787
- {this.props.noSort ? null : <TableSortLabel
788
- active={this.state.orderBy === col.field}
789
- direction={this.state.orderBy === col.field ? this.state.order : 'asc'}
790
- onClick={() => this.handleRequestSort(col.field)}
791
- >
792
- {col.title || col.field}
793
- {this.state.orderBy === col.field ?
794
- <span style={styles.visuallyHidden}>
795
- {this.state.order === 'desc' ? 'sorted descending' : 'sorted ascending'}
796
- </span> : null}
797
- </TableSortLabel> }
798
- </TableCell>))}
799
- {this.props.onUpdate ? <TableCell component="th" sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellButton)}>
800
- {!this.props.noAdd ? <Fab
801
- color="primary"
802
- size="small"
803
- disabled={this.state.editMode !== false}
804
- onClick={() => this.props.onUpdate && (this.props.onUpdate as (addNew: true) => void)(true)}
805
- >
806
- <IconAdd />
807
- </Fab> : null }
808
- </TableCell> : null}
809
- {this.props.onDelete || this.props.onUpdate ?
810
- <TableCell component="th" sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellButton)} /> : null}
811
- </TableRow>
812
- </TableHead>;
813
- }
814
-
815
- render() {
816
- const col = this.props.columns.find(_col => _col.field === this.state.orderBy);
817
- if (col) {
818
- const lookup = col.lookup;
819
- const table = stableSort(this.props.data, getComparator(this.state.order, this.state.orderBy, lookup));
820
-
821
- if (this.state.update && this.state.update.length) {
822
- this.updateTimeout && clearTimeout(this.updateTimeout);
823
- this.updateTimeout = setTimeout(() => {
824
- this.updateTimeout = null;
825
- this.setState({ update: null });
826
- }, 500);
827
- }
828
-
829
- return <div style={styles.tableContainer} className={this.props.className}>
830
- <Table style={styles.table} aria-label="simple table" size="small" stickyHeader>
831
- {this.renderHead()}
832
- <TableBody>
833
- {table.map(it => this.renderLine(it))}
834
- </TableBody>
835
- </Table>
836
- {this.renderSelectIdDialog()}
837
- {this.renderSelectColorDialog()}
838
- </div>;
839
- }
840
-
841
- return null;
842
- }
843
- }
844
- /*
845
- const columns = [
846
- {
847
- title: 'Name of field', // required, else it will be "field"
848
- field: 'fieldIdInData', // required
849
- editable: false, // or true [default - true]
850
- cellStyle: { // CSS style - // optional
851
- maxWidth: '12rem',
852
- overflow: 'hidden',
853
- wordBreak: 'break-word'
854
- },
855
- lookup: { // optional => edit will be automatically "SELECT"
856
- 'value1': 'text1',
857
- 'value2': 'text2',
858
- }
859
- },
860
- {
861
- title: 'Type', // required, else it will be "field"
862
- field: 'myType', // required
863
- editable: true, // or true [default - true]
864
- lookup: { // optional => edit will be automatically "SELECT"
865
- 'number': 'Number',
866
- 'string': 'String',
867
- 'boolean': 'Boolean',
868
- },
869
- type: 'number/string/color/oid/icon/boolean', // oid=ObjectID,icon=base64-icon
870
- editComponent: props =>
871
- <div>Prefix&#123; <br/>
872
- <textarea
873
- rows={4}
874
- style={{width: '100%', resize: 'vertical'}}
875
- value={props.value}
876
- onChange={e => props.onChange(e.target.value)}
877
- />
878
- Suffix
879
- </div>,
880
- },
881
- ];
882
- */
883
- /* const data = [
884
- {
885
- id: 'UniqueID1' // required
886
- fieldIdInData: 'Name1',
887
- myType: 'number',
888
- },
889
- {
890
- id: 'UniqueID2' // required
891
- fieldIdInData: 'Name12',
892
- myType: 'string',
893
- },
894
- ];
895
- */
896
-
897
- /*
898
- // STYLES
899
- const styles = theme => ({
900
- tableDiv: {
901
- width: '100%',
902
- overflow: 'hidden',
903
- height: 'calc(100% - 48px)',
904
- },
905
- });
906
- // renderTable
907
- renderTable() {
908
- return <div style={styles.tableDiv}>
909
- <TreeTable
910
- columns={this.columns}
911
- data={lines}
912
- onUpdate={(newData, oldData) => console.log('Update: ' + JSON.stringify(newData))}
913
- onDelete={oldData => console.log('Delete: ' + JSON.stringify(oldData))}
914
- />
915
- </div>;
916
- }
917
- */
918
-
919
- export default TreeTable;
1
+ import React, { Component, type JSX } from 'react';
2
+
3
+ import { HexColorPicker as ColorPicker } from 'react-colorful';
4
+
5
+ import {
6
+ Fab,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableRow,
12
+ TableSortLabel,
13
+ IconButton,
14
+ Select,
15
+ MenuItem,
16
+ TextField,
17
+ Checkbox,
18
+ Dialog,
19
+ } from '@mui/material';
20
+
21
+ import {
22
+ Edit as IconEdit,
23
+ Delete as IconDelete,
24
+ NavigateNext as IconExpand,
25
+ ExpandMore as IconCollapse,
26
+ Check as IconCheck,
27
+ Close as IconClose,
28
+ Add as IconAdd,
29
+ ViewHeadline as IconList,
30
+ Colorize as IconColor,
31
+ } from '@mui/icons-material';
32
+
33
+ import type Connection from '../Connection';
34
+
35
+ import DialogSelectID from '../Dialogs/SelectID';
36
+ import Utils from './Utils';
37
+ import type { IobTheme } from '../types';
38
+
39
+ function getAttr(obj: Record<string, any>, attr: string | string[], lookup?: Record<string, string>): any {
40
+ if (typeof attr === 'string') {
41
+ attr = attr.split('.');
42
+ }
43
+
44
+ if (!obj) {
45
+ return null;
46
+ }
47
+
48
+ if (attr.length === 1) {
49
+ if (lookup && lookup[obj[attr[0]]]) {
50
+ return lookup[obj[attr[0]]];
51
+ }
52
+ return obj[attr[0]];
53
+ }
54
+
55
+ const name: string = attr.shift() as string;
56
+ return getAttr(obj[name], attr);
57
+ }
58
+
59
+ function setAttr(obj: Record<string, any>, attr: string | string[], value: any): void {
60
+ if (typeof attr === 'string') {
61
+ attr = attr.split('.');
62
+ }
63
+
64
+ if (attr.length === 1) {
65
+ return (obj[attr[0]] = value);
66
+ }
67
+ const name: string = attr.shift() as string;
68
+ if (obj[name] === null || obj[name] === undefined) {
69
+ obj[name] = {};
70
+ }
71
+ return setAttr(obj[name], attr, value);
72
+ }
73
+
74
+ const styles: Record<string, any> = {
75
+ tableContainer: {
76
+ width: '100%',
77
+ height: '100%',
78
+ overflow: 'auto',
79
+ },
80
+ table: {
81
+ width: '100%',
82
+ minWidth: 800,
83
+ maxWidth: 1920,
84
+ },
85
+ cell: {
86
+ paddingTop: 0,
87
+ paddingBottom: 0,
88
+ paddingLeft: 4,
89
+ paddingRight: 4,
90
+ },
91
+ rowMainWithChildren: {},
92
+ rowMainWithoutChildren: {},
93
+ rowNoEdit: {
94
+ opacity: 0.3,
95
+ },
96
+ cellExpand: {
97
+ width: 30,
98
+ },
99
+ cellButton: {
100
+ width: 30,
101
+ },
102
+ cellHeader: {
103
+ fontWeight: 'bold',
104
+ background: (theme: IobTheme) => (theme.palette.mode === 'dark' ? '#888' : '#888'),
105
+ color: (theme: IobTheme) => (theme.palette.mode === 'dark' ? '#EEE' : '#111'),
106
+ height: 48,
107
+ wordBreak: 'break-word',
108
+ whiteSpace: 'pre',
109
+ },
110
+ width_name_nicknames: {
111
+ maxWidth: 150,
112
+ },
113
+ width_ioType: {
114
+ maxWidth: 100,
115
+ },
116
+ width_type: {
117
+ maxWidth: 100,
118
+ },
119
+ width_displayTraits: {
120
+ maxWidth: 100,
121
+ },
122
+ width_roomHint: {
123
+ maxWidth: 100,
124
+ },
125
+ rowSecondary: {
126
+ fontStyle: 'italic',
127
+ },
128
+ cellSecondary: {
129
+ fontSize: 10,
130
+ },
131
+ visuallyHidden: {
132
+ border: 0,
133
+ clip: 'rect(0 0 0 0)',
134
+ height: 1,
135
+ margin: -1,
136
+ overflow: 'hidden',
137
+ padding: 0,
138
+ position: 'absolute',
139
+ top: 20,
140
+ width: 1,
141
+ },
142
+ fieldEditWithButton: {
143
+ width: 'calc(100% - 33px)',
144
+ display: 'inline-block',
145
+ },
146
+ fieldEdit: {
147
+ width: '100%',
148
+ display: 'inline-block',
149
+ lineHeight: '50px',
150
+ verticalAlign: 'middle',
151
+ },
152
+ fieldButton: {
153
+ width: 30,
154
+ display: 'inline-block',
155
+ },
156
+ colorDialog: {
157
+ overflow: 'hidden',
158
+ padding: 15,
159
+ },
160
+ subText: {
161
+ fontSize: 10,
162
+ fontStyle: 'italic',
163
+ },
164
+ glow: {
165
+ animation: 'glow 0.2s 2 alternate',
166
+ },
167
+ };
168
+
169
+ function descendingComparator(
170
+ a: Record<string, any>,
171
+ b: Record<string, any>,
172
+ orderBy: string,
173
+ lookup?: Record<string, string>,
174
+ ): number {
175
+ const _a = getAttr(a, orderBy, lookup) || '';
176
+ const _b = getAttr(b, orderBy, lookup) || '';
177
+
178
+ if (_b < _a) {
179
+ return -1;
180
+ }
181
+ if (_b > _a) {
182
+ return 1;
183
+ }
184
+ return 0;
185
+ }
186
+
187
+ function getComparator(
188
+ order: 'desc' | 'asc',
189
+ orderBy: string,
190
+ lookup?: Record<string, string>,
191
+ ): (a: Record<string, any>, b: Record<string, any>) => number {
192
+ return order === 'desc'
193
+ ? (a, b) => descendingComparator(a, b, orderBy, lookup)
194
+ : (a, b) => -descendingComparator(a, b, orderBy, lookup);
195
+ }
196
+
197
+ function stableSort(
198
+ array: Record<string, any>[],
199
+ comparator: (a: Record<string, any>, b: Record<string, any>) => number,
200
+ ): Record<string, any>[] {
201
+ const stabilizedThis: { e: Record<string, any>; i: number }[] = array.map((el, index) => ({ e: el, i: index }));
202
+
203
+ stabilizedThis.sort((a, b) => {
204
+ const order = comparator(a.e, b.e);
205
+ if (order) {
206
+ return order;
207
+ }
208
+ return a.i - b.i;
209
+ });
210
+
211
+ return stabilizedThis.map(item => item.e);
212
+ }
213
+
214
+ interface Column {
215
+ cellStyle?: Record<string, any>;
216
+ editComponent?: React.FC<{ value: any; rowData: Record<string, any>; onChange: (newValue: any) => void }>;
217
+ field: string;
218
+ headerStyle?: Record<string, any>;
219
+ hidden?: boolean;
220
+ lookup?: Record<string, string>;
221
+ editable?: boolean | 'never';
222
+ title?: string;
223
+ type?: 'string' | 'boolean' | 'numeric' | 'icon' | 'oid' | 'color';
224
+ subField?: string;
225
+ subLookup?: Record<string, string>;
226
+ subStyle?: Record<string, any>;
227
+ }
228
+
229
+ interface TreeTableProps {
230
+ data: Record<string, any>[];
231
+ className?: string;
232
+ /** name of table to save settings in localStorage */
233
+ name?: string;
234
+ columns: Column[];
235
+ noSort?: boolean;
236
+ onUpdate?: ((newData: Record<string, any>, oldData: Record<string, any>) => void) | ((addNew: true) => void);
237
+ onDelete?: (oldData: Record<string, any>) => void;
238
+ /** hide add button */
239
+ noAdd?: boolean;
240
+ themeType?: string;
241
+ glowOnChange?: boolean;
242
+ /** only if an oid type is used */
243
+ socket?: Connection;
244
+ /** Shift in pixels for every level */
245
+ levelShift?: number;
246
+ adapterName: string;
247
+ theme: IobTheme;
248
+ }
249
+
250
+ interface TreeTableState {
251
+ opened: string[];
252
+ editMode: number | false;
253
+ deleteMode: number | false;
254
+ editData: Record<string, any> | null;
255
+ order: 'desc' | 'asc';
256
+ update: string[] | null;
257
+ orderBy: string;
258
+ showSelectColor: boolean;
259
+ selectIdValue?: string | null;
260
+ showSelectId?: boolean;
261
+ data?: Record<string, any>[];
262
+ }
263
+
264
+ class TreeTable extends Component<TreeTableProps, TreeTableState> {
265
+ private selectCallback: ((selected: string) => void) | null = null;
266
+
267
+ private updateTimeout: ReturnType<typeof setTimeout> | null = null;
268
+
269
+ constructor(props: TreeTableProps) {
270
+ super(props);
271
+
272
+ let opened =
273
+ ((window as any)._localStorage || window.localStorage).getItem(this.props.name || 'iob-table') || '[]';
274
+ try {
275
+ opened = JSON.parse(opened) || [];
276
+ } catch {
277
+ opened = [];
278
+ }
279
+ if (!Array.isArray(opened)) {
280
+ opened = [];
281
+ }
282
+
283
+ this.state = {
284
+ opened,
285
+ editMode: false,
286
+ deleteMode: false,
287
+ editData: null,
288
+ order: 'asc',
289
+ update: null,
290
+ orderBy: this.props.columns[0].field,
291
+ showSelectColor: false,
292
+ };
293
+ }
294
+
295
+ static getDerivedStateFromProps(props: TreeTableProps, state: TreeTableState): Partial<TreeTableState> {
296
+ if (props.glowOnChange) {
297
+ const update: string[] = [];
298
+ let count = 0;
299
+ if (props.data && state.data) {
300
+ props.data.forEach(line => {
301
+ count++;
302
+ const oldLine = state.data?.find(it => it.id === line.id);
303
+ if (oldLine) {
304
+ if (JSON.stringify(oldLine) !== JSON.stringify(line)) {
305
+ update.push(line.id);
306
+ }
307
+ } else {
308
+ update.push(line.id);
309
+ }
310
+ });
311
+ }
312
+
313
+ if (update.length && update.length !== count) {
314
+ return { data: props.data, update };
315
+ }
316
+ return { data: props.data };
317
+ }
318
+
319
+ return { data: props.data };
320
+ }
321
+
322
+ renderCellEdit(item: Record<string, any>, col: Column): JSX.Element | null {
323
+ let val = getAttr(item, col.field);
324
+ if (Array.isArray(val)) {
325
+ val = val[0];
326
+ }
327
+
328
+ if (col.lookup) {
329
+ return this.renderCellEditSelect(col, val);
330
+ }
331
+ if (col.editComponent) {
332
+ return this.renderCellEditCustom(col, val, item);
333
+ }
334
+ if (col.type === 'boolean' || (!col.type && typeof val === 'boolean')) {
335
+ return this.renderCellEditBoolean(col, val);
336
+ }
337
+ if (col.type === 'color') {
338
+ return this.renderCellEditColor(col, val);
339
+ }
340
+ if (col.type === 'oid') {
341
+ return this.renderCellEditObjectID(col, val);
342
+ }
343
+ if (col.type === 'numeric') {
344
+ return this.renderCellEditNumber(col, val);
345
+ }
346
+
347
+ return this.renderCellEditString(col, val);
348
+ }
349
+
350
+ onChange(col: Column, oldValue: string | number | boolean, newValue: string | number | boolean): void {
351
+ const editData = this.state.editData ? { ...this.state.editData } : {};
352
+ if (newValue === oldValue) {
353
+ delete editData[col.field];
354
+ } else {
355
+ editData[col.field] = newValue;
356
+ }
357
+ this.setState({ editData });
358
+ }
359
+
360
+ renderCellEditSelect(col: Column, val: string | number): JSX.Element {
361
+ return (
362
+ <Select
363
+ variant="standard"
364
+ onChange={e => this.onChange(col, val, e.target.value)}
365
+ value={(this.state.editData && this.state.editData[col.field]) || val}
366
+ >
367
+ {col.lookup &&
368
+ Object.keys(col.lookup).map((v, i) => (
369
+ <MenuItem
370
+ key={i}
371
+ value={v}
372
+ >
373
+ {col.lookup?.[v]}
374
+ </MenuItem>
375
+ ))}
376
+ </Select>
377
+ );
378
+ }
379
+
380
+ renderCellEditString(col: Column, val: string): JSX.Element {
381
+ return (
382
+ <TextField
383
+ variant="standard"
384
+ style={styles.fieldEdit}
385
+ fullWidth
386
+ value={
387
+ this.state.editData && this.state.editData[col.field] !== undefined
388
+ ? this.state.editData[col.field]
389
+ : val
390
+ }
391
+ onChange={e => this.onChange(col, val, e.target.value)}
392
+ />
393
+ );
394
+ }
395
+
396
+ renderCellEditNumber(col: Column, val: number): JSX.Element {
397
+ return (
398
+ <TextField
399
+ variant="standard"
400
+ style={styles.fieldEdit}
401
+ type="number"
402
+ fullWidth
403
+ value={
404
+ this.state.editData && this.state.editData[col.field] !== undefined
405
+ ? this.state.editData[col.field]
406
+ : val
407
+ }
408
+ onChange={e => this.onChange(col, val, e.target.value)}
409
+ />
410
+ );
411
+ }
412
+
413
+ renderCellEditCustom(col: Column, val: any, item: Record<string, any>): JSX.Element | null {
414
+ const EditComponent = col.editComponent;
415
+
416
+ // use new value if exists
417
+ if (this.state.editData && this.state.editData[col.field] !== undefined) {
418
+ val = this.state.editData[col.field];
419
+ item = JSON.parse(JSON.stringify(item));
420
+ item[col.field] = val;
421
+ }
422
+
423
+ return EditComponent ? (
424
+ <EditComponent
425
+ value={val}
426
+ rowData={item}
427
+ onChange={(newVal: any) => this.onChange(col, val, newVal as string | number)}
428
+ />
429
+ ) : null;
430
+ }
431
+
432
+ renderCellEditBoolean(col: Column, val: boolean): JSX.Element {
433
+ return (
434
+ <Checkbox
435
+ checked={
436
+ this.state.editData && this.state.editData[col.field] !== undefined
437
+ ? !!this.state.editData[col.field]
438
+ : !!val
439
+ }
440
+ onChange={e => this.onChange(col, !!val, e.target.checked)}
441
+ inputProps={{ 'aria-label': 'checkbox' }}
442
+ />
443
+ );
444
+ }
445
+
446
+ renderSelectColorDialog(): JSX.Element {
447
+ return (
448
+ <Dialog
449
+ sx={{
450
+ '& .MuiPaper-root': styles.root,
451
+ '& .MuiPaper-paper': styles.paper,
452
+ }}
453
+ onClose={() => {
454
+ this.selectCallback = null;
455
+ this.setState({ showSelectColor: false });
456
+ }}
457
+ open={this.state.showSelectColor}
458
+ >
459
+ <ColorPicker
460
+ color={this.state.selectIdValue as string}
461
+ onChange={color =>
462
+ this.setState({ selectIdValue: color }, () => this.selectCallback && this.selectCallback(color))
463
+ }
464
+ />
465
+ </Dialog>
466
+ );
467
+ }
468
+
469
+ renderCellEditColor(col: Column, val: string): JSX.Element {
470
+ const _val =
471
+ this.state.editData && this.state.editData[col.field] !== undefined ? this.state.editData[col.field] : val;
472
+ return (
473
+ <div style={styles.fieldEdit}>
474
+ <TextField
475
+ variant="standard"
476
+ fullWidth
477
+ style={styles.fieldEditWithButton}
478
+ value={_val}
479
+ inputProps={{ style: { backgroundColor: _val, color: Utils.isUseBright(_val) ? '#FFF' : '#000' } }}
480
+ onChange={e => this.onChange(col, !!val, e.target.value)}
481
+ />
482
+
483
+ <IconButton
484
+ style={styles.fieldButton}
485
+ onClick={() => {
486
+ this.selectCallback = newColor => this.onChange(col, val, newColor);
487
+ this.setState({ showSelectColor: true, selectIdValue: val });
488
+ }}
489
+ size="large"
490
+ >
491
+ <IconColor />
492
+ </IconButton>
493
+ </div>
494
+ );
495
+ }
496
+
497
+ renderSelectIdDialog(): JSX.Element | null {
498
+ if (this.state.showSelectId && this.props.socket) {
499
+ return (
500
+ <DialogSelectID
501
+ key="tableSelect"
502
+ imagePrefix="../.."
503
+ dialogName={this.props.adapterName}
504
+ themeType={this.props.themeType}
505
+ theme={this.props.theme}
506
+ socket={this.props.socket}
507
+ selected={this.state.selectIdValue as string}
508
+ onClose={() => this.setState({ showSelectId: false })}
509
+ onOk={(selected /* , name */) => {
510
+ this.setState({ showSelectId: false, selectIdValue: null });
511
+ this.selectCallback && this.selectCallback(selected as string);
512
+ this.selectCallback = null;
513
+ }}
514
+ />
515
+ );
516
+ }
517
+
518
+ return null;
519
+ }
520
+
521
+ renderCellEditObjectID(col: Column, val: string): JSX.Element {
522
+ return (
523
+ <div style={styles.fieldEdit}>
524
+ <TextField
525
+ variant="standard"
526
+ fullWidth
527
+ style={styles.fieldEditWithButton}
528
+ value={
529
+ this.state.editData && this.state.editData[col.field] !== undefined
530
+ ? this.state.editData[col.field]
531
+ : val
532
+ }
533
+ onChange={e => this.onChange(col, val, e.target.value)}
534
+ />
535
+
536
+ <IconButton
537
+ style={styles.fieldButton}
538
+ onClick={() => {
539
+ this.selectCallback = selected => this.onChange(col, val, selected);
540
+ this.setState({ showSelectId: true, selectIdValue: val });
541
+ }}
542
+ size="large"
543
+ >
544
+ <IconList />
545
+ </IconButton>
546
+ </div>
547
+ );
548
+ }
549
+
550
+ static renderCellNonEdit(item: Record<string, any>, col: Column): JSX.Element | string | number | null {
551
+ let val = getAttr(item, col.field, col.lookup);
552
+ if (Array.isArray(val)) {
553
+ val = val[0];
554
+ }
555
+
556
+ if (col.type === 'boolean') {
557
+ return (
558
+ <Checkbox
559
+ checked={!!val}
560
+ disabled
561
+ inputProps={{ 'aria-label': 'checkbox' }}
562
+ />
563
+ );
564
+ }
565
+
566
+ return val;
567
+ }
568
+
569
+ renderCell(item: Record<string, any>, col: Column, level: number, i: number): JSX.Element {
570
+ if (this.state.editMode === i && col.editable !== 'never' && col.editable !== false) {
571
+ return (
572
+ <TableCell
573
+ key={col.field}
574
+ style={{ ...styles.cell, ...(level ? styles.cellSecondary : undefined), ...col.cellStyle }}
575
+ component="th"
576
+ >
577
+ {this.renderCellEdit(item, col)}
578
+ </TableCell>
579
+ );
580
+ }
581
+ return (
582
+ <TableCell
583
+ key={col.field}
584
+ style={{ ...styles.cell, ...(level ? styles.cellSecondary : undefined), ...col.cellStyle }}
585
+ component="th"
586
+ >
587
+ {TreeTable.renderCellNonEdit(item, col)}
588
+ </TableCell>
589
+ );
590
+ }
591
+
592
+ static renderCellWithSubField(item: Record<string, any>, col: Column): JSX.Element {
593
+ const main = getAttr(item, col.field, col.lookup);
594
+ if (col.subField) {
595
+ const sub = getAttr(item, col.subField, col.subLookup);
596
+ return (
597
+ <div>
598
+ <div style={styles.mainText}>{main}</div>
599
+ <div style={{ ...styles.subText, ...(col.subStyle || undefined) }}>{sub}</div>
600
+ </div>
601
+ );
602
+ }
603
+ return (
604
+ <div>
605
+ <div style={styles.mainText}>{main}</div>
606
+ </div>
607
+ );
608
+ }
609
+
610
+ renderLine(item: Record<string, any>, level?: number): JSX.Element | JSX.Element[] | null {
611
+ const levelShift = this.props.levelShift === undefined ? 24 : this.props.levelShift;
612
+
613
+ level = level || 0;
614
+ const i = this.props.data.indexOf(item);
615
+ if (!item) {
616
+ return null;
617
+ }
618
+ if (!level && item.parentId) {
619
+ return null;
620
+ }
621
+ if (level && !item.parentId) {
622
+ return null; // should never happen
623
+ }
624
+ // try to find children
625
+ const opened = this.state.opened.includes(item.id);
626
+ const children = this.props.data.filter(it => it.parentId === item.id);
627
+
628
+ const row = (
629
+ <TableRow
630
+ key={item.id}
631
+ className={`table-row-${(item.id || '').toString().replace(/[.$]/g, '_')}`}
632
+ style={{
633
+ ...((this.state.update && this.state.update.includes(item.id) && styles.glow) || undefined),
634
+ ...styles.row,
635
+ ...(level ? styles.rowSecondary : undefined),
636
+ ...(!level && children.length ? styles.rowMainWithChildren : undefined),
637
+ ...(!level && !children.length ? styles.rowMainWithoutChildren : undefined),
638
+ ...(this.state.editMode !== false && this.state.editMode !== i ? styles.rowNoEdit : undefined),
639
+ ...(this.state.deleteMode !== false && this.state.deleteMode !== i ? styles.rowNoEdit : undefined),
640
+ }}
641
+ >
642
+ <TableCell
643
+ style={{ ...styles.cell, ...styles.cellExpand, ...(level ? styles.cellSecondary : undefined) }}
644
+ >
645
+ {children.length ? (
646
+ <IconButton
647
+ onClick={() => {
648
+ const _opened = [...this.state.opened];
649
+ const pos = _opened.indexOf(item.id);
650
+ if (pos === -1) {
651
+ _opened.push(item.id);
652
+ _opened.sort();
653
+ } else {
654
+ _opened.splice(pos, 1);
655
+ }
656
+
657
+ ((window as any)._localStorage || window.localStorage).setItem(
658
+ this.props.name || 'iob-table',
659
+ JSON.stringify(_opened),
660
+ );
661
+
662
+ this.setState({ opened: _opened });
663
+ }}
664
+ size="small"
665
+ >
666
+ {opened ? <IconCollapse /> : <IconExpand />}
667
+ </IconButton>
668
+ ) : null}
669
+ </TableCell>
670
+ <TableCell
671
+ scope="row"
672
+ style={{
673
+ ...styles.cell,
674
+ ...(level ? styles.cellSecondary : undefined),
675
+ ...this.props.columns[0].cellStyle,
676
+ paddingLeft: levelShift * level,
677
+ }}
678
+ >
679
+ {this.props.columns[0].subField
680
+ ? TreeTable.renderCellWithSubField(item, this.props.columns[0])
681
+ : getAttr(item, this.props.columns[0].field, this.props.columns[0].lookup)}
682
+ </TableCell>
683
+
684
+ {this.props.columns.map((col, ii) =>
685
+ !ii && !col.hidden ? null : this.renderCell(item, col, level, i),
686
+ )}
687
+
688
+ {this.props.onUpdate ? (
689
+ <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
690
+ {this.state.editMode === i || this.state.deleteMode === i ? (
691
+ <IconButton
692
+ disabled={
693
+ this.state.editMode !== false &&
694
+ (!this.state.editData || !Object.keys(this.state.editData).length)
695
+ }
696
+ onClick={() => {
697
+ if (this.state.editMode !== false) {
698
+ const newData = JSON.parse(JSON.stringify(item));
699
+ this.state.editData &&
700
+ Object.keys(this.state.editData).forEach(attr =>
701
+ setAttr(newData, attr, this.state.editData?.[attr]),
702
+ );
703
+ this.setState(
704
+ { editMode: false },
705
+ () => this.props.onUpdate && this.props.onUpdate(newData, item),
706
+ );
707
+ } else {
708
+ this.setState(
709
+ { deleteMode: false },
710
+ () => this.props.onDelete && this.props.onDelete(item),
711
+ );
712
+ }
713
+ }}
714
+ size="large"
715
+ >
716
+ <IconCheck />
717
+ </IconButton>
718
+ ) : (
719
+ <IconButton
720
+ disabled={this.state.editMode !== false}
721
+ onClick={() => this.setState({ editMode: i, editData: null })}
722
+ size="large"
723
+ >
724
+ <IconEdit />
725
+ </IconButton>
726
+ )}
727
+ </TableCell>
728
+ ) : null}
729
+
730
+ {this.props.onDelete && !this.props.onUpdate ? (
731
+ <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
732
+ {this.state.deleteMode === i ? (
733
+ <IconButton
734
+ disabled={
735
+ this.state.editMode !== false &&
736
+ (!this.state.editData || !Object.keys(this.state.editData).length)
737
+ }
738
+ onClick={() =>
739
+ this.setState(
740
+ { deleteMode: false },
741
+ () => this.props.onDelete && this.props.onDelete(item),
742
+ )
743
+ }
744
+ size="large"
745
+ >
746
+ <IconCheck />
747
+ </IconButton>
748
+ ) : null}
749
+ </TableCell>
750
+ ) : null}
751
+
752
+ {this.props.onUpdate || this.props.onDelete ? (
753
+ <TableCell style={{ ...styles.cell, ...styles.cellButton }}>
754
+ {this.state.editMode === i || this.state.deleteMode === i ? (
755
+ <IconButton
756
+ onClick={() => this.setState({ editMode: false, deleteMode: false })}
757
+ size="large"
758
+ >
759
+ <IconClose />
760
+ </IconButton>
761
+ ) : this.props.onDelete ? (
762
+ <IconButton
763
+ disabled={this.state.deleteMode !== false}
764
+ onClick={() => this.setState({ deleteMode: i })}
765
+ size="large"
766
+ >
767
+ <IconDelete />
768
+ </IconButton>
769
+ ) : null}
770
+ </TableCell>
771
+ ) : null}
772
+ </TableRow>
773
+ );
774
+
775
+ if (!level && opened) {
776
+ const items: JSX.Element[] = children.map(it => this.renderLine(it, level + 1)) as JSX.Element[];
777
+ items.unshift(row);
778
+ return items;
779
+ }
780
+ return row;
781
+ }
782
+
783
+ handleRequestSort(property: string): void {
784
+ const isAsc = this.state.orderBy === property && this.state.order === 'asc';
785
+ this.setState({ order: isAsc ? 'desc' : 'asc', orderBy: property });
786
+ }
787
+
788
+ renderHead(): JSX.Element {
789
+ return (
790
+ <TableHead>
791
+ <TableRow key="headerRow">
792
+ <TableCell
793
+ component="th"
794
+ sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellExpand)}
795
+ />
796
+ <TableCell
797
+ component="th"
798
+ sx={Utils.getStyle(
799
+ this.props.theme,
800
+ styles.cell,
801
+ styles.cellHeader,
802
+ styles[`width_${this.props.columns[0].field.replace(/\./g, '_')}`],
803
+ )}
804
+ style={this.props.columns[0].headerStyle || this.props.columns[0].cellStyle}
805
+ sortDirection={
806
+ this.props.noSort
807
+ ? false
808
+ : this.state.orderBy === this.props.columns[0].field
809
+ ? this.state.order
810
+ : false
811
+ }
812
+ >
813
+ {this.props.noSort ? null : (
814
+ <TableSortLabel
815
+ active={this.state.orderBy === this.props.columns[0].field}
816
+ direction={
817
+ this.state.orderBy === this.props.columns[0].field ? this.state.order : 'asc'
818
+ }
819
+ onClick={() => this.handleRequestSort(this.props.columns[0].field)}
820
+ >
821
+ {this.props.columns[0].title || this.props.columns[0].field}
822
+ {this.state.orderBy === this.props.columns[0].field ? (
823
+ <span style={styles.visuallyHidden}>
824
+ {this.state.order === 'desc' ? 'sorted descending' : 'sorted ascending'}
825
+ </span>
826
+ ) : null}
827
+ </TableSortLabel>
828
+ )}
829
+ </TableCell>
830
+ {this.props.columns.map((col, i) =>
831
+ !i && !col.hidden ? null : (
832
+ <TableCell
833
+ key={col.field}
834
+ sx={Utils.getStyle(
835
+ this.props.theme,
836
+ styles.cell,
837
+ styles.cellHeader,
838
+ styles[`width_${col.field.replace(/\./g, '_')}`],
839
+ )}
840
+ style={col.headerStyle || col.cellStyle}
841
+ component="th"
842
+ >
843
+ {this.props.noSort ? null : (
844
+ <TableSortLabel
845
+ active={this.state.orderBy === col.field}
846
+ direction={this.state.orderBy === col.field ? this.state.order : 'asc'}
847
+ onClick={() => this.handleRequestSort(col.field)}
848
+ >
849
+ {col.title || col.field}
850
+ {this.state.orderBy === col.field ? (
851
+ <span style={styles.visuallyHidden}>
852
+ {this.state.order === 'desc' ? 'sorted descending' : 'sorted ascending'}
853
+ </span>
854
+ ) : null}
855
+ </TableSortLabel>
856
+ )}
857
+ </TableCell>
858
+ ),
859
+ )}
860
+ {this.props.onUpdate ? (
861
+ <TableCell
862
+ component="th"
863
+ sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellButton)}
864
+ >
865
+ {!this.props.noAdd ? (
866
+ <Fab
867
+ color="primary"
868
+ size="small"
869
+ disabled={this.state.editMode !== false}
870
+ onClick={() =>
871
+ this.props.onUpdate && (this.props.onUpdate as (addNew: true) => void)(true)
872
+ }
873
+ >
874
+ <IconAdd />
875
+ </Fab>
876
+ ) : null}
877
+ </TableCell>
878
+ ) : null}
879
+ {this.props.onDelete || this.props.onUpdate ? (
880
+ <TableCell
881
+ component="th"
882
+ sx={Utils.getStyle(this.props.theme, styles.cell, styles.cellHeader, styles.cellButton)}
883
+ />
884
+ ) : null}
885
+ </TableRow>
886
+ </TableHead>
887
+ );
888
+ }
889
+
890
+ render(): JSX.Element | null {
891
+ const col = this.props.columns.find(_col => _col.field === this.state.orderBy);
892
+ if (col) {
893
+ const lookup = col.lookup;
894
+ const table = stableSort(this.props.data, getComparator(this.state.order, this.state.orderBy, lookup));
895
+
896
+ if (this.state.update && this.state.update.length) {
897
+ this.updateTimeout && clearTimeout(this.updateTimeout);
898
+ this.updateTimeout = setTimeout(() => {
899
+ this.updateTimeout = null;
900
+ this.setState({ update: null });
901
+ }, 500);
902
+ }
903
+
904
+ return (
905
+ <div
906
+ style={styles.tableContainer}
907
+ className={this.props.className}
908
+ >
909
+ <Table
910
+ style={styles.table}
911
+ aria-label="simple table"
912
+ size="small"
913
+ stickyHeader
914
+ >
915
+ {this.renderHead()}
916
+ <TableBody>{table.map(it => this.renderLine(it))}</TableBody>
917
+ </Table>
918
+ {this.renderSelectIdDialog()}
919
+ {this.renderSelectColorDialog()}
920
+ </div>
921
+ );
922
+ }
923
+
924
+ return null;
925
+ }
926
+ }
927
+ /*
928
+ const columns = [
929
+ {
930
+ title: 'Name of field', // required, else it will be "field"
931
+ field: 'fieldIdInData', // required
932
+ editable: false, // or true [default - true]
933
+ cellStyle: { // CSS style - // optional
934
+ maxWidth: '12rem',
935
+ overflow: 'hidden',
936
+ wordBreak: 'break-word'
937
+ },
938
+ lookup: { // optional => edit will be automatically "SELECT"
939
+ 'value1': 'text1',
940
+ 'value2': 'text2',
941
+ }
942
+ },
943
+ {
944
+ title: 'Type', // required, else it will be "field"
945
+ field: 'myType', // required
946
+ editable: true, // or true [default - true]
947
+ lookup: { // optional => edit will be automatically "SELECT"
948
+ 'number': 'Number',
949
+ 'string': 'String',
950
+ 'boolean': 'Boolean',
951
+ },
952
+ type: 'number/string/color/oid/icon/boolean', // oid=ObjectID,icon=base64-icon
953
+ editComponent: props =>
954
+ <div>Prefix&#123; <br/>
955
+ <textarea
956
+ rows={4}
957
+ style={{width: '100%', resize: 'vertical'}}
958
+ value={props.value}
959
+ onChange={e => props.onChange(e.target.value)}
960
+ />
961
+ Suffix
962
+ </div>,
963
+ },
964
+ ];
965
+ */
966
+ /* const data = [
967
+ {
968
+ id: 'UniqueID1' // required
969
+ fieldIdInData: 'Name1',
970
+ myType: 'number',
971
+ },
972
+ {
973
+ id: 'UniqueID2' // required
974
+ fieldIdInData: 'Name12',
975
+ myType: 'string',
976
+ },
977
+ ];
978
+ */
979
+
980
+ /*
981
+ // STYLES
982
+ const styles = theme => ({
983
+ tableDiv: {
984
+ width: '100%',
985
+ overflow: 'hidden',
986
+ height: 'calc(100% - 48px)',
987
+ },
988
+ });
989
+ // renderTable
990
+ renderTable() {
991
+ return <div style={styles.tableDiv}>
992
+ <TreeTable
993
+ columns={this.columns}
994
+ data={lines}
995
+ onUpdate={(newData, oldData) => console.log('Update: ' + JSON.stringify(newData))}
996
+ onDelete={oldData => console.log('Delete: ' + JSON.stringify(oldData))}
997
+ />
998
+ </div>;
999
+ }
1000
+ */
1001
+
1002
+ export default TreeTable;