@iobroker/gui-components 10.0.1 → 10.0.3

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 (43) hide show
  1. package/README.md +6 -2
  2. package/build/Components/FileBrowser.js +18 -13
  3. package/build/Components/FileBrowser.js.map +1 -1
  4. package/build/Components/{ObjectBrowser.d.ts → ObjectBrowser/ObjectBrowserClass.d.ts} +142 -77
  5. package/build/Components/ObjectBrowser/ObjectBrowserClass.js +2462 -0
  6. package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -0
  7. package/build/Components/ObjectBrowser/constants.d.ts +50 -0
  8. package/build/Components/ObjectBrowser/constants.js +138 -0
  9. package/build/Components/ObjectBrowser/constants.js.map +1 -0
  10. package/build/Components/ObjectBrowser/contextMenu.d.ts +13 -0
  11. package/build/Components/ObjectBrowser/contextMenu.js +354 -0
  12. package/build/Components/ObjectBrowser/contextMenu.js.map +1 -0
  13. package/build/Components/ObjectBrowser/dialogs.d.ts +31 -0
  14. package/build/Components/ObjectBrowser/dialogs.js +421 -0
  15. package/build/Components/ObjectBrowser/dialogs.js.map +1 -0
  16. package/build/Components/ObjectBrowser/index.d.ts +20 -0
  17. package/build/Components/ObjectBrowser/index.js +20 -0
  18. package/build/Components/ObjectBrowser/index.js.map +1 -0
  19. package/build/Components/ObjectBrowser/renderLeaf.d.ts +41 -0
  20. package/build/Components/ObjectBrowser/renderLeaf.js +1077 -0
  21. package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -0
  22. package/build/Components/ObjectBrowser/styles.d.ts +7 -0
  23. package/build/Components/ObjectBrowser/styles.js +662 -0
  24. package/build/Components/ObjectBrowser/styles.js.map +1 -0
  25. package/build/Components/ObjectBrowser/toolbar.d.ts +26 -0
  26. package/build/Components/ObjectBrowser/toolbar.js +327 -0
  27. package/build/Components/ObjectBrowser/toolbar.js.map +1 -0
  28. package/build/Components/{objectBrowser.types.d.ts → ObjectBrowser/types.d.ts} +15 -10
  29. package/build/Components/{objectBrowserUtils.d.ts → ObjectBrowser/utils.d.ts} +22 -2
  30. package/build/Components/{objectBrowserUtils.js → ObjectBrowser/utils.js} +46 -2
  31. package/build/Components/ObjectBrowser/utils.js.map +1 -0
  32. package/build/Components/TreeTable.d.ts +2 -2
  33. package/build/Components/TreeTable.js.map +1 -1
  34. package/build/Dialogs/SelectID.d.ts +4 -4
  35. package/build/Dialogs/SelectID.js +2 -2
  36. package/build/Dialogs/SelectID.js.map +1 -1
  37. package/build/index.d.ts +2 -2
  38. package/build/index.js +1 -1
  39. package/build/index.js.map +1 -1
  40. package/package.json +5 -5
  41. package/build/Components/ObjectBrowser.js +0 -5162
  42. package/build/Components/ObjectBrowser.js.map +0 -1
  43. package/build/Components/objectBrowserUtils.js.map +0 -1
@@ -0,0 +1,2462 @@
1
+ /**
2
+ * Copyright 2020-2026, Denis Haev <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ *
6
+ * The object browser itself: state, socket subscriptions, the tree and the rendering.
7
+ * The dialogs, the toolbar, the rows and the context menu live in the sibling files of this
8
+ * directory and get this instance as the first argument. Because of that, the members of this
9
+ * class are not private, even if they are not intended to be used outside of the object browser.
10
+ */
11
+ /* eslint-disable react/no-unused-class-component-methods -- the members are used by the sibling modules of this directory */
12
+ import React, { Component, createRef } from 'react';
13
+ import { Box, CircularProgress } from '@mui/material';
14
+ // own
15
+ import { Connection } from '../../Connection';
16
+ import { Utils } from '../Utils'; // @iobroker/gui-components/Components/Utils
17
+ import { TabContainer } from '../TabContainer';
18
+ import { TabContent } from '../TabContent';
19
+ import { TabHeader } from '../TabHeader';
20
+ import { applyFilter, binarySearch, buildTree, findNode, formatValue, generateFile, getName, colVar, widthFromContainer, growVar, getSelectIdIconFromObjects, setCustomValue, prepareSparkData, } from './utils';
21
+ import { styles } from './styles';
22
+ import * as dialogs from './dialogs';
23
+ import * as toolbar from './toolbar';
24
+ import * as leaf from './renderLeaf';
25
+ import * as contextMenu from './contextMenu';
26
+ import { DEFAULT_DATE_FORMAT, DEFAULT_FILTER, ITEM_LEVEL, MIN_COLUMN_WIDTHS, SCREEN_WIDTHS, } from './constants';
27
+ export { getSelectIdIconFromObjects };
28
+ let objectsAlreadyLoaded = false;
29
+ export class ObjectBrowserClass extends Component {
30
+ // do not define the type as null to save the performance, so we must check it every time
31
+ info = {
32
+ funcEnums: [],
33
+ roomEnums: [],
34
+ roles: [],
35
+ ids: [],
36
+ types: [],
37
+ objects: {},
38
+ customs: [],
39
+ enums: [],
40
+ hasSomeCustoms: false,
41
+ aliasesMap: {},
42
+ };
43
+ localStorage = window._localStorage || window.localStorage;
44
+ /**
45
+ * Own copy of the screen widths. The widths, that the user stored for THIS dialog, must not leak
46
+ * into the other instances of the object browser (the constant is a module singleton).
47
+ */
48
+ screenWidths = JSON.parse(JSON.stringify(SCREEN_WIDTHS));
49
+ tableRef;
50
+ /** Watches the width of the table container to detect the width class */
51
+ resizeObserver = null;
52
+ observedContainer = null;
53
+ /** Width class for which the columns were calculated the last time */
54
+ lastCalculatedWidth = null;
55
+ pausedSubscribes = false;
56
+ selectFirst;
57
+ /** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
58
+ lastNav = null;
59
+ /** True while applying `navigateTo`, so the derived-state watcher does not echo it back. */
60
+ applyingNav = false;
61
+ root = null;
62
+ states = {};
63
+ subscribes = [];
64
+ unsubscribeTimer = null;
65
+ statesUpdateTimer = null;
66
+ objectsUpdateTimer = null;
67
+ /** Columns that are visible in the auto mode. Depends on the width of the container */
68
+ visibleCols;
69
+ texts;
70
+ possibleCols;
71
+ imagePrefix;
72
+ adapterColumns = [];
73
+ styleTheme = '';
74
+ edit = {
75
+ id: '',
76
+ val: '',
77
+ q: 0,
78
+ ack: false,
79
+ };
80
+ levelPadding;
81
+ customWidth = false;
82
+ resizerNextName = null;
83
+ resizerActiveName = null;
84
+ resizerCurrentWidths = {};
85
+ resizeLeft = false;
86
+ resizerOldWidth = 0;
87
+ resizerMin = 0;
88
+ resizerNextMin = 0;
89
+ resizerOldWidthNext = 0;
90
+ resizerPosition = 0;
91
+ resizerActiveDiv = null;
92
+ resizerNextDiv = null;
93
+ storedWidths = null;
94
+ systemConfig = null;
95
+ objects;
96
+ defaultHistory = '';
97
+ ctrlPressed = false;
98
+ columnsVisibility = {};
99
+ changedIds = null;
100
+ contextMenu = null;
101
+ recordStates = [];
102
+ styles = {};
103
+ expertMode = false;
104
+ customColumnDialog = null;
105
+ constructor(props) {
106
+ super(props);
107
+ const lastSelectedItemStr = this.localStorage.getItem(`${props.dialogName || 'App'}.objectSelected`) || '';
108
+ this.selectFirst = '';
109
+ this.expertMode = !!this.props.expertMode;
110
+ if (lastSelectedItemStr.startsWith('[')) {
111
+ try {
112
+ const lastSelectedItems = JSON.parse(lastSelectedItemStr);
113
+ this.selectFirst = lastSelectedItems[0] || '';
114
+ }
115
+ catch {
116
+ // ignore
117
+ }
118
+ }
119
+ else {
120
+ this.selectFirst = lastSelectedItemStr;
121
+ }
122
+ let expanded;
123
+ const expandedStr = this.localStorage.getItem(`${props.dialogName || 'App'}.objectExpanded`) || '[]';
124
+ try {
125
+ expanded = JSON.parse(expandedStr);
126
+ }
127
+ catch {
128
+ expanded = [];
129
+ }
130
+ let filter;
131
+ const filterStr = props.defaultFilters
132
+ ? ''
133
+ : this.localStorage.getItem(`${props.dialogName || 'App'}.objectFilter`) || '';
134
+ if (filterStr) {
135
+ try {
136
+ filter = JSON.parse(filterStr);
137
+ }
138
+ catch {
139
+ filter = { ...DEFAULT_FILTER };
140
+ }
141
+ }
142
+ else if (props.defaultFilters && typeof props.defaultFilters === 'object') {
143
+ filter = { ...props.defaultFilters };
144
+ }
145
+ else {
146
+ filter = { ...DEFAULT_FILTER };
147
+ }
148
+ // Migrate old filters to new one
149
+ if (typeof filter.room === 'string' && filter.room) {
150
+ filter.room = [filter.room].filter(s => s);
151
+ if (!filter.room.length) {
152
+ delete filter.room;
153
+ }
154
+ }
155
+ if (typeof filter.func === 'string' && filter.func) {
156
+ filter.func = [filter.func].filter(s => s);
157
+ if (!filter.func.length) {
158
+ delete filter.func;
159
+ }
160
+ }
161
+ if (typeof filter.role === 'string' && filter.role) {
162
+ filter.role = [filter.role].filter(s => s);
163
+ if (!filter.role.length) {
164
+ delete filter.role;
165
+ }
166
+ }
167
+ if (typeof filter.type === 'string') {
168
+ filter.type = [filter.type].filter(s => s);
169
+ if (!filter.type.length) {
170
+ delete filter.type;
171
+ }
172
+ }
173
+ if (typeof filter.custom === 'string') {
174
+ filter.custom = [filter.custom].filter(s => s);
175
+ if (!filter.custom.length) {
176
+ delete filter.custom;
177
+ }
178
+ }
179
+ filter.expertMode =
180
+ props.expertMode !== undefined
181
+ ? props.expertMode
182
+ : (window._sessionStorage || window.sessionStorage).getItem('App.expertMode') ===
183
+ 'true';
184
+ this.tableRef = createRef();
185
+ this.visibleCols = props.columns || this.screenWidths[props.width || 'lg'].fields;
186
+ // remove type column if only one type must be selected
187
+ if (props.types && props.types.length === 1) {
188
+ const pos = this.visibleCols.indexOf('type');
189
+ if (pos !== -1) {
190
+ this.visibleCols.splice(pos, 1);
191
+ }
192
+ }
193
+ this.possibleCols = this.screenWidths.xl.fields;
194
+ let customDialog = null;
195
+ if (props.router) {
196
+ const location = props.router.getLocation();
197
+ if (location.id && location.dialog === 'customs') {
198
+ customDialog = [location.id];
199
+ this.pauseSubscribe(true);
200
+ }
201
+ }
202
+ let selected;
203
+ if (!Array.isArray(props.selected)) {
204
+ selected = [props.selected || ''];
205
+ }
206
+ else {
207
+ selected = props.selected;
208
+ }
209
+ selected = selected.map(id => id.replace(/["']/g, '')).filter(id => id);
210
+ this.selectFirst = selected.length && selected[0] ? selected[0] : this.selectFirst;
211
+ const columnsStr = this.localStorage.getItem(`${props.dialogName || 'App'}.columns`);
212
+ let columns;
213
+ try {
214
+ columns = columnsStr ? JSON.parse(columnsStr) : null;
215
+ }
216
+ catch {
217
+ columns = null;
218
+ }
219
+ let columnsWidths = null; // this.localStorage.getItem(`${props.dialogName || 'App'}.columnsWidths`);
220
+ try {
221
+ columnsWidths = columnsWidths ? JSON.parse(columnsWidths) : {};
222
+ }
223
+ catch {
224
+ columnsWidths = {};
225
+ }
226
+ this.imagePrefix = props.imagePrefix || '.';
227
+ let foldersFirst;
228
+ const foldersFirstStr = this.localStorage.getItem(`${props.dialogName || 'App'}.foldersFirst`);
229
+ if (foldersFirstStr === 'false') {
230
+ foldersFirst = false;
231
+ }
232
+ else if (foldersFirstStr === 'true') {
233
+ foldersFirst = true;
234
+ }
235
+ else {
236
+ foldersFirst = props.foldersFirst === undefined ? true : props.foldersFirst;
237
+ }
238
+ let statesView = false;
239
+ try {
240
+ statesView = this.props.objectStatesView
241
+ ? JSON.parse(this.localStorage.getItem(`${props.dialogName || 'App'}.objectStatesView`) || '') || false
242
+ : false;
243
+ }
244
+ catch {
245
+ // ignore
246
+ }
247
+ this.state = {
248
+ aliasMenu: '',
249
+ beautifyJsonExport: true,
250
+ columns,
251
+ columnsAuto: this.localStorage.getItem(`${props.dialogName || 'App'}.columnsAuto`) !== 'false',
252
+ columnsDialogTransparent: 100,
253
+ columnsEditCustomDialog: null,
254
+ columnsForAdmin: null,
255
+ columnsSelectorShow: false,
256
+ columnsWidths,
257
+ customColumnDialogValueChanged: false,
258
+ customDialog,
259
+ depth: 0,
260
+ editObjectAlias: false, // open the edit object dialog on alias tab
261
+ editObjectDialog: '',
262
+ enumDialog: null,
263
+ excludeSystemRepositoriesFromExport: true,
264
+ excludeTranslations: false,
265
+ expandAllVisible: false,
266
+ expanded,
267
+ filter,
268
+ filterKey: 0,
269
+ containerWidth: props.width || 'lg',
270
+ focused: this.localStorage.getItem(`${props.dialogName || 'App'}.focused`) || '',
271
+ foldersFirst,
272
+ linesEnabled: this.localStorage.getItem(`${props.dialogName || 'App'}.lines`) === 'true',
273
+ loaded: false,
274
+ noStatesByExportImport: false,
275
+ roleDialog: null,
276
+ selected,
277
+ selectedNonObject: this.localStorage.getItem(`${props.dialogName || 'App'}.selectedNonObject`) || '',
278
+ showAliasEditor: '',
279
+ showAllExportOptions: false,
280
+ showContextMenu: null,
281
+ showDescription: this.localStorage.getItem(`${props.dialogName || 'App'}.desc`) !== 'false',
282
+ showExportDialog: false,
283
+ showImportDialog: false,
284
+ showImportMenu: null,
285
+ showRenameDialog: null,
286
+ statesView,
287
+ toast: '',
288
+ tooltipInfo: null,
289
+ viewFileDialog: '',
290
+ };
291
+ this.texts = {
292
+ name: props.t('ra_Name'),
293
+ categories: props.t('ra_Categories'),
294
+ value: props.t('ra_tooltip_value'),
295
+ ack: props.t('ra_tooltip_ack'),
296
+ ts: props.t('ra_tooltip_ts'),
297
+ lc: props.t('ra_tooltip_lc'),
298
+ from: props.t('ra_tooltip_from'),
299
+ user: props.t('ra_tooltip_user'),
300
+ c: props.t('ra_tooltip_comment'),
301
+ quality: props.t('ra_tooltip_quality'),
302
+ editObject: props.t('ra_tooltip_editObject'),
303
+ deleteObject: props.t('ra_tooltip_deleteObject'),
304
+ customConfig: props.t('ra_tooltip_customConfig'),
305
+ copyState: props.t('ra_tooltip_copyState'),
306
+ editState: props.t('ra_tooltip_editState'),
307
+ ctrlForLink: props.t('ra_tooltip_ctrlForLink'),
308
+ close: props.t('ra_Close'),
309
+ filter_id: props.t('ra_filter_id'),
310
+ filter_name: props.t('ra_filter_name'),
311
+ filter_type: props.t('ra_filter_type'),
312
+ filter_role: props.t('ra_filter_role'),
313
+ filter_room: props.t('ra_filter_room'),
314
+ filter_func: props.t('ra_filter_func'),
315
+ filter_custom: props.t('ra_filter_customs'), //
316
+ filterCustomsWithout: props.t('ra_filter_customs_without'), //
317
+ objectChangedByUser: props.t('ra_object_changed_by_user'), // Object last changed at
318
+ objectChangedBy: props.t('ra_object_changed_by'), // Object changed by
319
+ objectChangedFrom: props.t('ra_state_changed_from'), // Object changed from
320
+ stateChangedBy: props.t('ra_state_changed_by'), // State changed by
321
+ stateChangedFrom: props.t('ra_state_changed_from'), // State changed from
322
+ ownerGroup: props.t('ra_Owner group'),
323
+ ownerUser: props.t('ra_Owner user'),
324
+ showAll: props.t('ra_show_all'),
325
+ deviceError: props.t('ra_Error'),
326
+ deviceDisconnected: props.t('ra_Disconnected'),
327
+ deviceConnected: props.t('ra_Connected'),
328
+ aclOwner_read_object: props.t('ra_aclOwner_read_object'),
329
+ aclOwner_read_state: props.t('ra_aclOwner_read_state'),
330
+ aclOwner_write_object: props.t('ra_aclOwner_write_object'),
331
+ aclOwner_write_state: props.t('ra_aclOwner_write_state'),
332
+ aclGroup_read_object: props.t('ra_aclGroup_read_object'),
333
+ aclGroup_read_state: props.t('ra_aclGroup_read_state'),
334
+ aclGroup_write_object: props.t('ra_aclGroup_write_object'),
335
+ aclGroup_write_state: props.t('ra_aclGroup_write_state'),
336
+ aclEveryone_read_object: props.t('ra_aclEveryone_read_object'),
337
+ aclEveryone_read_state: props.t('ra_aclEveryone_read_state'),
338
+ aclEveryone_write_object: props.t('ra_aclEveryone_write_object'),
339
+ aclEveryone_write_state: props.t('ra_aclEveryone_write_state'),
340
+ create: props.t('ra_Create'),
341
+ createBooleanState: props.t('ra_create_boolean_state'),
342
+ createNumberState: props.t('ra_create_number_state'),
343
+ createStringState: props.t('ra_create_string_state'),
344
+ createState: props.t('ra_create_state'),
345
+ createChannel: props.t('ra_create_channel'),
346
+ createDevice: props.t('ra_create_device'),
347
+ createFolder: props.t('ra_Create folder'),
348
+ };
349
+ this.levelPadding = props.levelPadding || ITEM_LEVEL;
350
+ const resizerCurrentWidthsStr = this.localStorage.getItem(`${this.props.dialogName || 'App'}.table`);
351
+ if (resizerCurrentWidthsStr) {
352
+ try {
353
+ const resizerCurrentWidths = JSON.parse(resizerCurrentWidthsStr);
354
+ const width = this.width || 'lg';
355
+ this.storedWidths = JSON.parse(JSON.stringify(this.screenWidths[width]));
356
+ Object.keys(resizerCurrentWidths).forEach(id => {
357
+ if (id === 'id') {
358
+ this.screenWidths[width].idWidth = resizerCurrentWidths.id;
359
+ }
360
+ else if (id === 'nameHeader') {
361
+ // widths stored by an older version, where the header had an own width
362
+ this.screenWidths[width].widths.name = resizerCurrentWidths[id];
363
+ }
364
+ else if (this.screenWidths[width].widths[id] !== undefined) {
365
+ this.screenWidths[width].widths[id] = resizerCurrentWidths[id];
366
+ }
367
+ });
368
+ this.customWidth = true;
369
+ }
370
+ catch {
371
+ // ignore
372
+ }
373
+ }
374
+ this.calculateColumnsVisibility();
375
+ }
376
+ async loadAllObjects(update) {
377
+ const props = this.props;
378
+ try {
379
+ await new Promise(resolve => {
380
+ this.setState({ updating: true }, () => resolve());
381
+ });
382
+ const objects = (props.objectsWorker
383
+ ? await props.objectsWorker.getObjects(update)
384
+ : await props.socket.getObjects(update, true)) || {};
385
+ if (props.types && Connection.isWeb()) {
386
+ for (let i = 0; i < props.types.length; i++) {
387
+ // admin has ALL objects
388
+ // web has only state, channel, device, enum, and system.config
389
+ if (props.types[i] === 'state' ||
390
+ props.types[i] === 'channel' ||
391
+ props.types[i] === 'device' ||
392
+ props.types[i] === 'enum') {
393
+ continue;
394
+ }
395
+ const moreObjects = await props.socket.getObjectViewSystem(props.types[i]);
396
+ Object.assign(objects || {}, moreObjects);
397
+ }
398
+ }
399
+ this.systemConfig ||=
400
+ objects?.['system.config'] ||
401
+ (await props.socket.getObject('system.config'));
402
+ this.systemConfig.common ||= {};
403
+ this.systemConfig.common.defaultNewAcl ||= {
404
+ object: 0,
405
+ state: 0,
406
+ file: 0,
407
+ owner: 'system.user.admin',
408
+ ownerGroup: 'system.group.administrator',
409
+ };
410
+ this.systemConfig.common.defaultNewAcl.owner ||= 'system.user.admin';
411
+ this.systemConfig.common.defaultNewAcl.ownerGroup ||= 'system.group.administrator';
412
+ if (typeof this.systemConfig.common.defaultNewAcl.state !== 'number') {
413
+ // TODO: may be convert here from string
414
+ this.systemConfig.common.defaultNewAcl.state = 0x664;
415
+ }
416
+ if (typeof this.systemConfig.common.defaultNewAcl.object !== 'number') {
417
+ // TODO: may be convert here from string
418
+ this.systemConfig.common.defaultNewAcl.state = 0x664;
419
+ }
420
+ if (typeof props.filterFunc === 'function') {
421
+ this.objects = {};
422
+ const filterFunc = props.filterFunc;
423
+ Object.keys(objects).forEach(id => {
424
+ try {
425
+ if (filterFunc(objects[id])) {
426
+ this.objects[id] = objects[id];
427
+ }
428
+ else {
429
+ const type = objects[id] && objects[id].type;
430
+ // include "folder" types too for icons and names of nodes
431
+ if (type &&
432
+ (type === 'channel' ||
433
+ type === 'device' ||
434
+ type === 'folder' ||
435
+ type === 'adapter' ||
436
+ type === 'instance')) {
437
+ this.objects[id] = objects[id];
438
+ }
439
+ }
440
+ }
441
+ catch (e) {
442
+ console.log(`Error by filtering of "${id}": ${e}`);
443
+ }
444
+ });
445
+ }
446
+ else if (props.types) {
447
+ this.objects = {};
448
+ const propsTypes = props.types;
449
+ Object.keys(objects).forEach(id => {
450
+ const type = objects[id]?.type;
451
+ // include "folder" types too
452
+ if (type &&
453
+ (type === 'channel' ||
454
+ type === 'device' ||
455
+ type === 'enum' ||
456
+ type === 'folder' ||
457
+ type === 'adapter' ||
458
+ type === 'instance' ||
459
+ propsTypes.includes(type))) {
460
+ this.objects[id] = objects[id];
461
+ }
462
+ });
463
+ }
464
+ else {
465
+ this.objects = objects;
466
+ }
467
+ if (props.setObjectsReference) {
468
+ props.setObjectsReference(this.objects);
469
+ }
470
+ // read default history
471
+ this.defaultHistory = this.systemConfig.common.defaultHistory;
472
+ if (this.defaultHistory) {
473
+ props.socket
474
+ .getState(`system.adapter.${this.defaultHistory}.alive`)
475
+ .then(state => {
476
+ if (!state?.val) {
477
+ this.defaultHistory = '';
478
+ }
479
+ })
480
+ .catch(e => window.alert(`Cannot get state: ${e}`));
481
+ }
482
+ const columnsForAdmin = await this.getAdditionalColumns();
483
+ this.calculateColumnsVisibility(null, null, columnsForAdmin);
484
+ const { info, root } = buildTree(this.objects, {
485
+ imagePrefix: props.imagePrefix,
486
+ root: props.root,
487
+ lang: props.lang,
488
+ themeType: props.themeType,
489
+ });
490
+ this.root = root;
491
+ this.info = info;
492
+ // Show first selected item
493
+ const node = this.state.selected?.length && findNode(this.root, this.state.selected[0]);
494
+ // If the selected ID is not visible, reset filter
495
+ if (node &&
496
+ !applyFilter(node, this.state.filter, props.lang, this.objects, undefined, undefined, props.customFilter, props.types)) {
497
+ // reset filter
498
+ this.setState({ filter: { ...DEFAULT_FILTER }, columnsForAdmin }, () => {
499
+ this.doFilter();
500
+ this.setState({ loaded: true, updating: false }, () => this.expandAllSelected(() => {
501
+ this.onAfterSelect();
502
+ this.applyInitialNavigateTo();
503
+ }));
504
+ });
505
+ }
506
+ else {
507
+ this.doFilter();
508
+ this.setState({ loaded: true, updating: false, columnsForAdmin }, () => this.expandAllSelected(() => {
509
+ this.onAfterSelect();
510
+ this.applyInitialNavigateTo();
511
+ }));
512
+ }
513
+ }
514
+ catch (error) {
515
+ this.showError(error);
516
+ }
517
+ }
518
+ expandAllSelected(cb) {
519
+ const expanded = [...this.state.expanded];
520
+ let changed = false;
521
+ this.state.selected.forEach(id => {
522
+ const parts = id.split('.');
523
+ const path = [];
524
+ for (let i = 0; i < parts.length - 1; i++) {
525
+ path.push(parts[i]);
526
+ if (!expanded.includes(path.join('.'))) {
527
+ expanded.push(path.join('.'));
528
+ changed = true;
529
+ }
530
+ }
531
+ });
532
+ if (changed) {
533
+ expanded.sort();
534
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
535
+ this.setState({ expanded }, cb);
536
+ }
537
+ else if (cb) {
538
+ cb();
539
+ }
540
+ }
541
+ /**
542
+ * @param isDouble is double click
543
+ */
544
+ onAfterSelect(isDouble) {
545
+ if (this.state.selected?.length && this.state.selected[0]) {
546
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectSelected`, this.state.selected[0]);
547
+ // remove a task to select the pre-selected item if now we want to see another object
548
+ if (this.selectFirst && this.selectFirst !== this.state.selected[0]) {
549
+ this.selectFirst = '';
550
+ }
551
+ // If the this.state.selected[0] filtered out, disable the filter
552
+ const item = this.findItem(this.state.selected[0]);
553
+ if (item?.data && !item.data.visible && !item.data.hasVisibleChildren) {
554
+ // If the selected ID is not visible, reset filter
555
+ this.clearFilter();
556
+ }
557
+ if (this.state.selected.length === 1 && this.objects[this.state.selected[0]]) {
558
+ const name = Utils.getObjectName(this.objects, this.state.selected[0], null, {
559
+ language: this.props.lang,
560
+ });
561
+ this.props.onSelect?.(this.state.selected, name, isDouble);
562
+ }
563
+ else if (this.state.selected.length === 1 && this.props.allowNonObjects) {
564
+ this.props.onSelect?.(this.state.selected, null, isDouble);
565
+ }
566
+ else {
567
+ // we have more than one state
568
+ // Check if all IDs are objects
569
+ if (!this.props.allowNonObjects || !this.state.selected.find(id => !this.objects[id])) {
570
+ this.props.onSelect?.(this.state.selected, null, isDouble);
571
+ }
572
+ }
573
+ }
574
+ else {
575
+ this.localStorage.removeItem(`${this.props.dialogName || 'App'}.objectSelected`);
576
+ if (this.state.selected.length) {
577
+ this.setState({ selected: [] }, () => {
578
+ if (this.props.onSelect) {
579
+ if (this.state.focused && this.props.allowNonObjects) {
580
+ // remove a task to select the pre-selected item if now we want to see another object
581
+ if (this.selectFirst && this.selectFirst !== this.state.selected[0]) {
582
+ this.selectFirst = '';
583
+ }
584
+ this.props.onSelect([this.state.focused], null, isDouble);
585
+ }
586
+ else {
587
+ this.props.onSelect([], '');
588
+ }
589
+ }
590
+ });
591
+ }
592
+ else if (this.props.onSelect) {
593
+ if (this.state.focused && this.props.allowNonObjects) {
594
+ // remove a task to select the pre-selected item if now we want to see another object
595
+ if (this.selectFirst && this.selectFirst !== this.state.selected[0]) {
596
+ this.selectFirst = '';
597
+ }
598
+ this.props.onSelect([this.state.focused], null, isDouble);
599
+ }
600
+ else {
601
+ this.props.onSelect([], '');
602
+ }
603
+ }
604
+ }
605
+ }
606
+ // This function is used
607
+ static getDerivedStateFromProps(props, state) {
608
+ const newState = {};
609
+ let changed = false;
610
+ if (props.expertMode !== undefined && props.expertMode !== state.filter.expertMode) {
611
+ changed = true;
612
+ newState.filter = { ...state.filter };
613
+ newState.filter.expertMode = props.expertMode;
614
+ }
615
+ return changed ? newState : null;
616
+ }
617
+ /**
618
+ * Called when component is mounted.
619
+ */
620
+ async componentDidMount() {
621
+ await this.loadAllObjects(!objectsAlreadyLoaded);
622
+ if (this.props.objectsWorker) {
623
+ this.props.objectsWorker.registerHandler(this.onObjectChangeFromWorker);
624
+ }
625
+ else {
626
+ await this.props.socket.subscribeObject('*', this.onObjectChange);
627
+ }
628
+ objectsAlreadyLoaded = true;
629
+ window.addEventListener('contextmenu', this.onContextMenu, true);
630
+ window.addEventListener('keydown', this.onKeyPress, true);
631
+ window.addEventListener('keyup', this.onKeyPress, true);
632
+ this.observeContainerWidth();
633
+ // Inform dialog that all objects are loaded
634
+ if (this.props.onAllLoaded) {
635
+ setTimeout(() => {
636
+ this.props.onAllLoaded?.();
637
+ }, 100);
638
+ }
639
+ }
640
+ onKeyPress = (event) => {
641
+ if (event.type === 'keydown' && event.ctrlKey && !this.ctrlPressed) {
642
+ this.ctrlPressed = true;
643
+ if (this.tableRef.current) {
644
+ this.tableRef.current.className = 'highlight-link';
645
+ }
646
+ }
647
+ else if (event.type === 'keyup' && !event.ctrlKey && this.ctrlPressed) {
648
+ this.ctrlPressed = false;
649
+ if (this.tableRef.current) {
650
+ this.tableRef.current.className = '';
651
+ }
652
+ }
653
+ };
654
+ /**
655
+ * Called when component is unmounted.
656
+ */
657
+ componentWillUnmount() {
658
+ window.removeEventListener('contextmenu', this.onContextMenu, true);
659
+ window.removeEventListener('keydown', this.onKeyPress, true);
660
+ window.removeEventListener('keyup', this.onKeyPress, true);
661
+ this.resizeObserver?.disconnect();
662
+ this.resizeObserver = null;
663
+ this.observedContainer = null;
664
+ // the timers must not fire after the component was destroyed
665
+ if (this.unsubscribeTimer) {
666
+ clearTimeout(this.unsubscribeTimer);
667
+ this.unsubscribeTimer = null;
668
+ }
669
+ if (this.statesUpdateTimer) {
670
+ clearTimeout(this.statesUpdateTimer);
671
+ this.statesUpdateTimer = null;
672
+ }
673
+ if (this.objectsUpdateTimer) {
674
+ clearTimeout(this.objectsUpdateTimer);
675
+ this.objectsUpdateTimer = null;
676
+ }
677
+ if (this.props.objectsWorker) {
678
+ this.props.objectsWorker.unregisterHandler(this.onObjectChangeFromWorker, true);
679
+ }
680
+ else {
681
+ void this.props.socket
682
+ .unsubscribeObject('*', this.onObjectChange)
683
+ .catch(e => console.error(`Cannot unsubscribe *: ${e}`));
684
+ }
685
+ // remove all subscribes
686
+ this.subscribes.forEach(pattern => {
687
+ // console.log(`- unsubscribe ${pattern}`);
688
+ this.props.socket.unsubscribeState(pattern, this.onStateChange);
689
+ });
690
+ this.subscribes = [];
691
+ this.objects = {};
692
+ }
693
+ /**
694
+ * Show the deletion dialog for a given object
695
+ */
696
+ showDeleteDialog(options) {
697
+ const { id, obj, item } = options;
698
+ // calculate the number of children
699
+ const keys = Object.keys(this.objects);
700
+ keys.sort();
701
+ let count = 0;
702
+ const start = `${id}.`;
703
+ for (let i = 0; i < keys.length; i++) {
704
+ if (keys[i].startsWith(start)) {
705
+ count++;
706
+ }
707
+ else if (keys[i] > start) {
708
+ break;
709
+ }
710
+ }
711
+ this.props.onObjectDelete?.(id, !!item.children?.length, !obj.common?.dontDelete, count + 1);
712
+ }
713
+ /**
714
+ * Context menu handler.
715
+ */
716
+ onContextMenu = (e) => {
717
+ // console.log(`CONTEXT MENU: ${this.contextMenu ? Date.now() - this.contextMenu.ts : 'false'}`);
718
+ if (this.contextMenu && Date.now() - this.contextMenu.ts < 2000) {
719
+ e.preventDefault();
720
+ this.setState({
721
+ showContextMenu: {
722
+ item: this.contextMenu.item,
723
+ position: { left: e.clientX + 2, top: e.clientY - 6 },
724
+ },
725
+ });
726
+ }
727
+ else if (this.state.showContextMenu) {
728
+ e.preventDefault();
729
+ this.setState({ showContextMenu: null });
730
+ }
731
+ this.contextMenu = null;
732
+ };
733
+ /**
734
+ * Called when component is mounted.
735
+ */
736
+ refreshComponent() {
737
+ // remove all subscribes
738
+ this.subscribes.forEach(pattern => {
739
+ // console.log(`- unsubscribe ${pattern}`);
740
+ this.props.socket.unsubscribeState(pattern, this.onStateChange);
741
+ });
742
+ this.subscribes = [];
743
+ this.loadAllObjects(true)
744
+ .then(() => console.log('updated!'))
745
+ .catch(e => this.showError(e));
746
+ }
747
+ /**
748
+ * Renders the error dialog.
749
+ */
750
+ renderErrorDialog() {
751
+ return dialogs.renderErrorDialog(this);
752
+ }
753
+ /**
754
+ * Show the error dialog.
755
+ */
756
+ showError(error) {
757
+ this.setState({
758
+ error: typeof error === 'object'
759
+ ? error && typeof error.toString === 'function'
760
+ ? error.toString()
761
+ : JSON.stringify(error)
762
+ : error,
763
+ });
764
+ }
765
+ /**
766
+ * Called when an item is selected/deselected.
767
+ */
768
+ onSelect(toggleItem, isDouble, cb) {
769
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.focused`, toggleItem);
770
+ if (!this.props.multiSelect) {
771
+ if (this.objects[toggleItem] &&
772
+ (!this.props.types || this.props.types.includes(this.objects[toggleItem].type))) {
773
+ this.localStorage.removeItem(`${this.props.dialogName || 'App'}.selectedNonObject`);
774
+ if (this.state.selected[0] !== toggleItem) {
775
+ this.setState({ selected: [toggleItem], selectedNonObject: '', focused: toggleItem }, () => {
776
+ this.onAfterSelect(isDouble);
777
+ if (cb) {
778
+ cb();
779
+ }
780
+ });
781
+ }
782
+ else if (isDouble && this.props.onSelect) {
783
+ this.onAfterSelect(isDouble);
784
+ }
785
+ }
786
+ else {
787
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.selectedNonObject`, toggleItem);
788
+ this.setState({ selected: [], selectedNonObject: toggleItem, focused: toggleItem }, () => {
789
+ this.onAfterSelect();
790
+ if (cb) {
791
+ cb();
792
+ }
793
+ });
794
+ }
795
+ }
796
+ else if (this.objects[toggleItem] &&
797
+ (!this.props.types || this.props.types.includes(this.objects[toggleItem].type))) {
798
+ this.localStorage.removeItem(`${this.props.dialogName || 'App'}.selectedNonObject`);
799
+ const selected = [...this.state.selected];
800
+ const pos = selected.indexOf(toggleItem);
801
+ if (pos === -1) {
802
+ selected.push(toggleItem);
803
+ selected.sort();
804
+ }
805
+ else if (!isDouble) {
806
+ selected.splice(pos, 1);
807
+ }
808
+ this.setState({ selected, selectedNonObject: '', focused: toggleItem }, () => {
809
+ this.onAfterSelect(isDouble);
810
+ if (cb) {
811
+ cb();
812
+ }
813
+ });
814
+ }
815
+ }
816
+ /**
817
+ * Renders the columns' selector.
818
+ */
819
+ renderColumnsSelectorDialog() {
820
+ return dialogs.renderColumnsSelectorDialog(this);
821
+ }
822
+ async getAdditionalColumns() {
823
+ try {
824
+ const instances = await this.props.socket.getAdapters();
825
+ let columnsForAdmin = null;
826
+ // find all additional columns
827
+ instances.forEach(obj => (columnsForAdmin = this.parseObjectForAdmins(columnsForAdmin, obj)));
828
+ return columnsForAdmin;
829
+ }
830
+ catch (err) {
831
+ // window.alert('Cannot get adapters: ' + e);
832
+ // Object browser in Web has no additional columns
833
+ console.error(`Cannot get adapters: ${err}`);
834
+ return null;
835
+ }
836
+ }
837
+ checkUnsubscribes() {
838
+ // Remove unused subscriptions
839
+ for (let i = this.subscribes.length - 1; i >= 0; i--) {
840
+ if (!this.recordStates.includes(this.subscribes[i])) {
841
+ this.unsubscribe(this.subscribes[i]);
842
+ }
843
+ }
844
+ this.recordStates = [];
845
+ }
846
+ /**
847
+ * Find an item.
848
+ */
849
+ findItem(id, _parts, _root, _partyId) {
850
+ _parts ||= id.split('.');
851
+ _root ||= this.root;
852
+ if (!_root || !_parts.length) {
853
+ return null;
854
+ }
855
+ _partyId = (_partyId ? `${_partyId}.` : '') + _parts.shift();
856
+ if (_root.children) {
857
+ const item = _root.children.find(i => i.data.id === _partyId);
858
+ if (item) {
859
+ if (item.data.id === id) {
860
+ return item;
861
+ }
862
+ if (_parts.length) {
863
+ return this.findItem(id, _parts, item, _partyId);
864
+ }
865
+ }
866
+ else {
867
+ return null;
868
+ }
869
+ }
870
+ return null;
871
+ }
872
+ /**
873
+ * Called when a state changes.
874
+ */
875
+ onStateChange = (id, state) => {
876
+ // console.log(`> stateChange ${id}`);
877
+ if (this.states[id]) {
878
+ const item = this.findItem(id);
879
+ if (item?.data.state) {
880
+ item.data.state = undefined;
881
+ }
882
+ }
883
+ if (state) {
884
+ this.states[id] = state;
885
+ }
886
+ else {
887
+ delete this.states[id];
888
+ }
889
+ if (!this.pausedSubscribes) {
890
+ if (!this.statesUpdateTimer) {
891
+ this.statesUpdateTimer = setTimeout(() => {
892
+ this.statesUpdateTimer = null;
893
+ this.forceUpdate();
894
+ }, 300);
895
+ }
896
+ }
897
+ else if (this.statesUpdateTimer) {
898
+ clearTimeout(this.statesUpdateTimer);
899
+ this.statesUpdateTimer = null;
900
+ }
901
+ };
902
+ parseObjectForAdmins(columnsForAdmin, obj) {
903
+ if (obj.common?.adminColumns && obj.common.name) {
904
+ const columns = obj.common.adminColumns;
905
+ let aColumns;
906
+ if (columns && typeof columns !== 'object') {
907
+ aColumns = [columns];
908
+ }
909
+ else if (columns) {
910
+ aColumns = columns;
911
+ }
912
+ let cColumns;
913
+ if (columns) {
914
+ cColumns = aColumns
915
+ ?.map((_item) => {
916
+ if (typeof _item !== 'object') {
917
+ return { path: _item, name: _item.split('.').pop() };
918
+ }
919
+ const item = _item;
920
+ // string => array
921
+ if (item.objTypes && typeof item.objTypes !== 'object') {
922
+ item.objTypes = [item.objTypes];
923
+ }
924
+ else if (!item.objTypes) {
925
+ item.objTypes = undefined;
926
+ }
927
+ if (!item.name && item.path) {
928
+ return {
929
+ path: item.path,
930
+ name: item.path.split('.').pop(),
931
+ width: item.width,
932
+ edit: !!item.edit,
933
+ type: item.type,
934
+ objTypes: item.objTypes,
935
+ };
936
+ }
937
+ if (!item.path) {
938
+ console.warn(`Admin columns for ${obj._id} ignored, because path not found`);
939
+ return null;
940
+ }
941
+ return {
942
+ path: item.path,
943
+ name: getName(item.name || '', this.props.lang),
944
+ width: item.width,
945
+ edit: !!item.edit,
946
+ type: item.type,
947
+ objTypes: item.objTypes,
948
+ };
949
+ })
950
+ .filter((item) => item);
951
+ }
952
+ else {
953
+ cColumns = null;
954
+ }
955
+ if (cColumns && cColumns.length) {
956
+ columnsForAdmin ||= {};
957
+ columnsForAdmin[obj.common.name] = cColumns.sort((a, b) => a.path > b.path ? -1 : a.path < b.path ? 1 : 0);
958
+ }
959
+ }
960
+ else if (obj.common && obj.common.name && columnsForAdmin && columnsForAdmin[obj.common.name]) {
961
+ delete columnsForAdmin[obj.common.name];
962
+ }
963
+ return columnsForAdmin;
964
+ }
965
+ onObjectChangeFromWorker = (events) => {
966
+ if (Array.isArray(events)) {
967
+ let newState = null;
968
+ events.forEach(event => {
969
+ const { newInnerState, filtered } = this.processOnObjectChangeElement(event.id, event.obj);
970
+ if (filtered) {
971
+ return;
972
+ }
973
+ if (newInnerState && newState) {
974
+ Object.assign(newState, newInnerState);
975
+ }
976
+ else {
977
+ newState = newInnerState;
978
+ }
979
+ });
980
+ if (newState) {
981
+ this.setState(newState);
982
+ }
983
+ this.afterObjectUpdated();
984
+ }
985
+ };
986
+ onObjectChange = (id, obj) => {
987
+ const { newInnerState, filtered } = this.processOnObjectChangeElement(id, obj);
988
+ if (filtered) {
989
+ return;
990
+ }
991
+ if (newInnerState) {
992
+ this.setState(newInnerState);
993
+ }
994
+ this.afterObjectUpdated();
995
+ };
996
+ afterObjectUpdated() {
997
+ if (!this.objectsUpdateTimer && this.objects) {
998
+ this.objectsUpdateTimer = setTimeout(() => {
999
+ this.objectsUpdateTimer = null;
1000
+ const { info, root } = buildTree(this.objects, {
1001
+ imagePrefix: this.props.imagePrefix,
1002
+ root: this.props.root,
1003
+ lang: this.props.lang,
1004
+ themeType: this.props.themeType,
1005
+ });
1006
+ this.root = root;
1007
+ this.info = info;
1008
+ if (!this.pausedSubscribes) {
1009
+ this.doFilter();
1010
+ }
1011
+ // else it will be re-rendered when the dialog will be closed
1012
+ }, 500);
1013
+ }
1014
+ }
1015
+ // This function is called when the user changes the alias of an object.
1016
+ // It updates the aliasMap and returns true if the aliasMap has changed.
1017
+ updateAliases(aliasId) {
1018
+ if (!this.objects || !this.info?.aliasesMap || !aliasId?.startsWith('alias.')) {
1019
+ return;
1020
+ }
1021
+ // Rebuild aliases map
1022
+ const aliasesIds = Object.keys(this.objects).filter(id => id.startsWith('alias.0'));
1023
+ this.info.aliasesMap = {};
1024
+ for (const id of aliasesIds) {
1025
+ const obj = this.objects[id];
1026
+ if (obj?.common?.alias?.id) {
1027
+ if (typeof obj.common.alias.id === 'string') {
1028
+ const usedId = obj.common.alias.id;
1029
+ if (!this.info.aliasesMap[usedId]) {
1030
+ this.info.aliasesMap[usedId] = [id];
1031
+ }
1032
+ else if (!this.info.aliasesMap[usedId].includes(id)) {
1033
+ this.info.aliasesMap[usedId].push(id);
1034
+ }
1035
+ }
1036
+ else {
1037
+ const readId = obj.common.alias.id.read;
1038
+ if (readId) {
1039
+ if (!this.info.aliasesMap[readId]) {
1040
+ this.info.aliasesMap[readId] = [id];
1041
+ }
1042
+ else if (!this.info.aliasesMap[readId].includes(id)) {
1043
+ this.info.aliasesMap[readId].push(id);
1044
+ }
1045
+ }
1046
+ const writeId = obj.common.alias.id.write;
1047
+ if (writeId) {
1048
+ if (!this.info.aliasesMap[writeId]) {
1049
+ this.info.aliasesMap[writeId] = [id];
1050
+ }
1051
+ else if (!this.info.aliasesMap[writeId].includes(id)) {
1052
+ this.info.aliasesMap[writeId].push(id);
1053
+ }
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Processes a single element in regard to certain filters, columns for admin and updates object dict
1061
+ *
1062
+ * @param id The id of the object
1063
+ * @param obj The object itself
1064
+ * @returns Returns an object containing the new state (if any) and whether the object was filtered.
1065
+ */
1066
+ processOnObjectChangeElement(id, obj) {
1067
+ // console.log(`> objectChange ${id}`);
1068
+ const type = obj?.type;
1069
+ // If the object is filtered out, we don't need to update the React state
1070
+ if (obj &&
1071
+ typeof this.props.filterFunc === 'function' &&
1072
+ !this.props.filterFunc(obj) &&
1073
+ type !== 'channel' &&
1074
+ type !== 'device' &&
1075
+ type !== 'folder' &&
1076
+ type !== 'adapter' &&
1077
+ type !== 'instance') {
1078
+ return { newInnerState: null, filtered: true };
1079
+ }
1080
+ let newInnerState = null;
1081
+ if (id.startsWith('system.adapter.') && obj?.type === 'adapter') {
1082
+ const columnsForAdmin = JSON.parse(JSON.stringify(this.state.columnsForAdmin));
1083
+ this.parseObjectForAdmins(columnsForAdmin, obj);
1084
+ if (JSON.stringify(this.state.columnsForAdmin) !== JSON.stringify(columnsForAdmin)) {
1085
+ newInnerState = { columnsForAdmin };
1086
+ }
1087
+ }
1088
+ this.objects ||= {};
1089
+ if (obj) {
1090
+ this.objects[id] = obj;
1091
+ }
1092
+ else if (this.objects[id]) {
1093
+ delete this.objects[id];
1094
+ }
1095
+ this.updateAliases(id);
1096
+ return { newInnerState, filtered: false };
1097
+ }
1098
+ subscribe(id) {
1099
+ if (!this.subscribes.includes(id)) {
1100
+ this.subscribes.push(id);
1101
+ // console.log(`+ subscribe ${id}`);
1102
+ if (!this.pausedSubscribes) {
1103
+ this.props.socket
1104
+ .subscribeState(id, this.onStateChange)
1105
+ .catch(e => console.error(`Cannot subscribe on state ${id}: ${e}`));
1106
+ }
1107
+ }
1108
+ }
1109
+ unsubscribe(id) {
1110
+ const pos = this.subscribes.indexOf(id);
1111
+ if (pos !== -1) {
1112
+ this.subscribes.splice(pos, 1);
1113
+ if (this.states[id]) {
1114
+ delete this.states[id];
1115
+ }
1116
+ // console.log(`- unsubscribe ${id}`);
1117
+ this.props.socket.unsubscribeState(id, this.onStateChange);
1118
+ if (this.pausedSubscribes) {
1119
+ console.warn('Unsubscribe during pause?');
1120
+ }
1121
+ }
1122
+ }
1123
+ pauseSubscribe(isPause) {
1124
+ if (!this.pausedSubscribes && isPause) {
1125
+ this.pausedSubscribes = true;
1126
+ this.subscribes.forEach(id => this.props.socket.unsubscribeState(id, this.onStateChange));
1127
+ }
1128
+ else if (this.pausedSubscribes && !isPause) {
1129
+ this.pausedSubscribes = false;
1130
+ this.subscribes.forEach(id => this.props.socket.subscribeState(id, this.onStateChange));
1131
+ }
1132
+ }
1133
+ clearFilter() {
1134
+ if (JSON.stringify(this.state.filter) !== JSON.stringify(DEFAULT_FILTER)) {
1135
+ this.setState({ filter: { ...DEFAULT_FILTER }, filterKey: this.state.filterKey + 1 }, () => {
1136
+ this.doFilter();
1137
+ this.props.onFilterChanged?.({ ...DEFAULT_FILTER });
1138
+ });
1139
+ }
1140
+ }
1141
+ isFilterEmpty() {
1142
+ return (!!this.state.filter.id ||
1143
+ !!this.state.filter.name ||
1144
+ !!this.state.filter.room?.length ||
1145
+ !!this.state.filter.func?.length ||
1146
+ !!this.state.filter.role?.length ||
1147
+ !!this.state.filter.type?.length ||
1148
+ !!this.state.filter.custom?.length);
1149
+ }
1150
+ onExpandAll(root, expanded) {
1151
+ const _root = root || this.root;
1152
+ expanded ||= [];
1153
+ _root?.children?.forEach((item) => {
1154
+ if (item.data.sumVisibility) {
1155
+ expanded.push(item.data.id);
1156
+ this.onExpandAll(item, expanded);
1157
+ }
1158
+ });
1159
+ if (_root === this.root) {
1160
+ expanded.sort();
1161
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
1162
+ this.setState({ expanded });
1163
+ }
1164
+ }
1165
+ onCollapseAll() {
1166
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify([]));
1167
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectSelected`, '[]');
1168
+ this.setState({ expanded: [], depth: 0, selected: [] }, () => this.onAfterSelect());
1169
+ }
1170
+ expandDepth(root, depth, expanded) {
1171
+ if (!this.root) {
1172
+ throw new Error('No root');
1173
+ }
1174
+ root ||= this.root;
1175
+ if (depth > 0) {
1176
+ root.children?.forEach(item => {
1177
+ if (item.data.sumVisibility) {
1178
+ if (!binarySearch(expanded, item.data.id)) {
1179
+ expanded.push(item.data.id);
1180
+ expanded.sort();
1181
+ }
1182
+ if (depth - 1 > 0) {
1183
+ this.expandDepth(item, depth - 1, expanded);
1184
+ }
1185
+ }
1186
+ });
1187
+ }
1188
+ }
1189
+ static collapseDepth(depth, expanded) {
1190
+ return expanded.filter(id => id.split('.').length <= depth);
1191
+ }
1192
+ onExpandVisible() {
1193
+ if (this.state.depth < 9) {
1194
+ const depth = this.state.depth + 1;
1195
+ const expanded = [...this.state.expanded];
1196
+ if (this.root) {
1197
+ this.expandDepth(this.root, depth, expanded);
1198
+ }
1199
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
1200
+ this.setState({ depth, expanded });
1201
+ }
1202
+ }
1203
+ onStatesViewVisible() {
1204
+ const statesView = !this.state.statesView;
1205
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectStatesView`, JSON.stringify(statesView));
1206
+ this.setState({ statesView });
1207
+ }
1208
+ onCollapseVisible() {
1209
+ if (this.state.depth > 0) {
1210
+ const depth = this.state.depth - 1;
1211
+ const expanded = ObjectBrowserClass.collapseDepth(depth, this.state.expanded);
1212
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
1213
+ this.setState({ depth, expanded });
1214
+ }
1215
+ }
1216
+ getEnumsForId = (id) => {
1217
+ const result = [];
1218
+ this.info.enums.forEach(_id => {
1219
+ if (this.objects[_id]?.common?.members?.includes(id)) {
1220
+ const enumItem = {
1221
+ _id: this.objects[_id]._id,
1222
+ common: JSON.parse(JSON.stringify(this.objects[_id].common)),
1223
+ native: this.objects[_id].native,
1224
+ type: 'enum',
1225
+ };
1226
+ if (enumItem.common) {
1227
+ delete enumItem.common.members;
1228
+ delete enumItem.common.custom;
1229
+ // @ts-expect-error deprecated attribute
1230
+ delete enumItem.common.mobile;
1231
+ }
1232
+ result.push(enumItem);
1233
+ }
1234
+ });
1235
+ return result.length ? result : undefined;
1236
+ };
1237
+ _createAllEnums = async (enums, objId) => {
1238
+ for (let e = 0; e < enums.length; e++) {
1239
+ const item = enums[e];
1240
+ let id;
1241
+ let newObj;
1242
+ // some admin version delivered enums as string
1243
+ if (typeof item === 'object') {
1244
+ newObj = item;
1245
+ id = newObj._id;
1246
+ }
1247
+ else {
1248
+ id = item;
1249
+ }
1250
+ let oldObj = this.objects[id];
1251
+ // if enum does not exist
1252
+ if (!oldObj) {
1253
+ // create a new one
1254
+ oldObj =
1255
+ newObj ||
1256
+ {
1257
+ _id: id,
1258
+ common: {
1259
+ name: id.split('.').pop(),
1260
+ members: [],
1261
+ },
1262
+ native: {},
1263
+ type: 'enum',
1264
+ };
1265
+ oldObj.common ||= {};
1266
+ oldObj.common.members = [objId];
1267
+ oldObj.type = 'enum';
1268
+ await this.props.socket.setObject(id, oldObj);
1269
+ }
1270
+ else if (!oldObj.common?.members?.includes(objId)) {
1271
+ oldObj.common ||= {};
1272
+ oldObj.type = 'enum';
1273
+ oldObj.common.members ||= [];
1274
+ // add the missing object
1275
+ oldObj.common.members.push(objId);
1276
+ oldObj.common.members.sort();
1277
+ await this.props.socket.setObject(id, oldObj);
1278
+ }
1279
+ }
1280
+ };
1281
+ async loadObjects(objs) {
1282
+ if (objs) {
1283
+ for (const id in objs) {
1284
+ if (!Object.prototype.hasOwnProperty.call(objs, id) || !objs[id]) {
1285
+ continue;
1286
+ }
1287
+ const obj = objs[id];
1288
+ let enums = null;
1289
+ let val;
1290
+ let ack;
1291
+ if (obj?.common?.enums) {
1292
+ enums = obj.common.enums;
1293
+ delete obj.common.enums;
1294
+ }
1295
+ else {
1296
+ enums = null;
1297
+ }
1298
+ if (obj.val || obj.val === 0) {
1299
+ val = obj.val;
1300
+ delete obj.val;
1301
+ }
1302
+ if (obj.ack !== undefined) {
1303
+ ack = obj.ack;
1304
+ delete obj.ack;
1305
+ }
1306
+ try {
1307
+ await this.props.socket.setObject(id, obj);
1308
+ if (enums) {
1309
+ await this._createAllEnums(enums, obj._id);
1310
+ }
1311
+ if (obj.type === 'state') {
1312
+ if (val !== undefined && val !== null) {
1313
+ try {
1314
+ await this.props.socket.setState(obj._id, val, ack !== undefined ? ack : true);
1315
+ }
1316
+ catch (e) {
1317
+ window.alert(`Cannot set state "${obj._id} with ${val}": ${e}`);
1318
+ }
1319
+ }
1320
+ else {
1321
+ try {
1322
+ const state = await this.props.socket.getState(obj._id);
1323
+ if (!state || state.val === null) {
1324
+ try {
1325
+ await this.props.socket.setState(obj._id, !obj.common || obj.common.def === undefined ? null : obj.common.def, true);
1326
+ }
1327
+ catch (e) {
1328
+ window.alert(`Cannot set state "${obj._id}": ${e}`);
1329
+ }
1330
+ }
1331
+ }
1332
+ catch (e) {
1333
+ window.alert(`Cannot read state "${obj._id}": ${e}`);
1334
+ }
1335
+ }
1336
+ }
1337
+ }
1338
+ catch (error) {
1339
+ window.alert(error);
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ _getSelectedIdsForExport() {
1345
+ if (this.state.selected.length || this.state.selectedNonObject) {
1346
+ const result = [];
1347
+ const keys = Object.keys(this.objects);
1348
+ keys.sort();
1349
+ const id = this.state.selected[0] || this.state.selectedNonObject;
1350
+ const idDot = `${id}.`;
1351
+ const idLen = idDot.length;
1352
+ for (let k = 0; k < keys.length; k++) {
1353
+ const key = keys[k];
1354
+ if (id === key || key.startsWith(idDot)) {
1355
+ result.push(key);
1356
+ }
1357
+ if (key.substring(0, idLen) > idDot) {
1358
+ break;
1359
+ }
1360
+ }
1361
+ return result;
1362
+ }
1363
+ return [];
1364
+ }
1365
+ /**
1366
+ * Exports the selected objects based on the given options and triggers file generation
1367
+ */
1368
+ async _exportObjects(
1369
+ /** Options to filter/reduce the output */
1370
+ options) {
1371
+ if (options.isAll) {
1372
+ generateFile('allObjects.json', this.objects, options);
1373
+ return;
1374
+ }
1375
+ if (!(this.state.selected.length || this.state.selectedNonObject)) {
1376
+ window.alert(this.props.t('ra_Save of objects-tree is not possible'));
1377
+ return;
1378
+ }
1379
+ const result = {};
1380
+ const id = this.state.selected[0] || this.state.selectedNonObject;
1381
+ const ids = this._getSelectedIdsForExport();
1382
+ for (const key of ids) {
1383
+ result[key] = JSON.parse(JSON.stringify(this.objects[key]));
1384
+ // read states values
1385
+ if (result[key]?.type === 'state' && !options.noStatesByExportImport) {
1386
+ const state = await this.props.socket.getState(key);
1387
+ if (state) {
1388
+ result[key].val = state.val;
1389
+ result[key].ack = state.ack;
1390
+ }
1391
+ }
1392
+ // add enum information
1393
+ if (result[key].common) {
1394
+ const enums = this.getEnumsForId(key);
1395
+ if (enums) {
1396
+ result[key].common.enums = enums;
1397
+ }
1398
+ }
1399
+ }
1400
+ generateFile(`${id}.json`, result, options);
1401
+ }
1402
+ renderExportDialog() {
1403
+ return dialogs.renderExportDialog(this);
1404
+ }
1405
+ renderRenameDialog() {
1406
+ return dialogs.renderRenameDialog(this);
1407
+ }
1408
+ async parseJsonFile(contents) {
1409
+ try {
1410
+ const json = JSON.parse(contents);
1411
+ const len = Object.keys(json).length;
1412
+ const id = json._id;
1413
+ // it could be a single object or many objects
1414
+ if (id === undefined && len) {
1415
+ // many objects
1416
+ await this.loadObjects(json);
1417
+ window.alert(this.props.t('ra_%s object(s) processed', len));
1418
+ }
1419
+ else {
1420
+ // it is only one object in form
1421
+ // {
1422
+ // "_id": "xxx",
1423
+ // "common": "yyy",
1424
+ // "native": "zzz"
1425
+ // "val": JSON.stringify(value)
1426
+ // "ack": true
1427
+ // }
1428
+ if (!id) {
1429
+ return window.alert(this.props.t('ra_Invalid structure'));
1430
+ }
1431
+ try {
1432
+ let enums;
1433
+ let val;
1434
+ let ack;
1435
+ if (json.common.enums) {
1436
+ enums = json.common.enums;
1437
+ delete json.common.enums;
1438
+ }
1439
+ if (json.val) {
1440
+ val = json.val;
1441
+ delete json.val;
1442
+ }
1443
+ if (json.ack !== undefined) {
1444
+ ack = json.ack;
1445
+ delete json.ack;
1446
+ }
1447
+ await this.props.socket.setObject(json._id, json);
1448
+ if (json.type === 'state') {
1449
+ if (val !== undefined && val !== null) {
1450
+ await this.props.socket.setState(json._id, val, ack === undefined ? true : ack);
1451
+ }
1452
+ else {
1453
+ const state = await this.props.socket.getState(json._id);
1454
+ if (!state || state.val === null || state.val === undefined) {
1455
+ await this.props.socket.setState(json._id, json.common.def === undefined ? null : json.common.def, true);
1456
+ }
1457
+ }
1458
+ }
1459
+ if (enums) {
1460
+ await this._createAllEnums(enums, json._id);
1461
+ }
1462
+ window.alert(this.props.t('ra_%s was imported', json._id));
1463
+ }
1464
+ catch (err) {
1465
+ window.alert(err);
1466
+ }
1467
+ }
1468
+ }
1469
+ catch (err) {
1470
+ window.alert(err);
1471
+ }
1472
+ }
1473
+ handleJsonUpload(evt) {
1474
+ const target = evt.target;
1475
+ const f = target.files?.length && target.files[0];
1476
+ if (f) {
1477
+ const r = new FileReader();
1478
+ r.onload = (e) => {
1479
+ this.parseJsonFile(e.target?.result).catch(e => console.log(`Cannot parse file: ${e}`));
1480
+ };
1481
+ r.readAsText(f);
1482
+ }
1483
+ else {
1484
+ window.alert(this.props.t('ra_Failed to open JSON File'));
1485
+ }
1486
+ }
1487
+ toolTipObjectCreating = () => {
1488
+ const { t } = this.props;
1489
+ let value = [
1490
+ React.createElement("div", { key: 1 }, t('ra_Only following structures of objects are available:')),
1491
+ React.createElement("div", { key: 2 }, t('ra_Folder → State')),
1492
+ React.createElement("div", { key: 3 }, t('ra_Folder → Channel → State')),
1493
+ React.createElement("div", { key: 4 }, t('ra_Folder → Device → Channel → State')),
1494
+ React.createElement("div", { key: 5 }, t('ra_Device → Channel → State')),
1495
+ React.createElement("div", { key: 6 }, t('ra_Channel → State')),
1496
+ React.createElement("div", { key: 7, style: { height: 10 } }),
1497
+ React.createElement("div", { key: 8 }, t('ra_Non-experts may create new objects only in "0_userdata.0" or "alias.0".')),
1498
+ React.createElement("div", { key: 9 }, t('ra_The experts may create objects everywhere but from second level (e.g. "vis.0" or "javascript.0").')),
1499
+ ];
1500
+ if (this.state.selected.length || this.state.selectedNonObject) {
1501
+ const id = this.state.selected[0] || this.state.selectedNonObject;
1502
+ if (id.split('.').length < 2 || (this.objects[id] && this.objects[id]?.type === 'state')) {
1503
+ // show default tooltip
1504
+ }
1505
+ else if (this.state.filter.expertMode) {
1506
+ switch (this.objects[id]?.type) {
1507
+ case 'device':
1508
+ value = [
1509
+ React.createElement("div", { key: 1 }, t('ra_Only following structures of objects are available:')),
1510
+ React.createElement("div", { key: 5 }, t('ra_Device → Channel → State')),
1511
+ React.createElement("div", { key: 7, style: { height: 10 } }),
1512
+ React.createElement("div", { key: 8 }, t('ra_Non-experts may create new objects only in "0_userdata.0" or "alias.0".')),
1513
+ React.createElement("div", { key: 9 }, t('ra_The experts may create objects everywhere but from second level (e.g. "vis.0" or "javascript.0").')),
1514
+ ];
1515
+ break;
1516
+ case 'folder':
1517
+ value = [
1518
+ React.createElement("div", { key: 1 }, t('ra_Only following structures of objects are available:')),
1519
+ React.createElement("div", { key: 2 }, t('ra_Folder → State')),
1520
+ React.createElement("div", { key: 3 }, t('ra_Folder → Channel → State')),
1521
+ React.createElement("div", { key: 4 }, t('ra_Folder → Device → Channel → State')),
1522
+ React.createElement("div", { key: 7, style: { height: 10 } }),
1523
+ React.createElement("div", { key: 8 }, t('ra_Non-experts may create new objects only in "0_userdata.0" or "alias.0".')),
1524
+ React.createElement("div", { key: 9 }, t('ra_The experts may create objects everywhere but from second level (e.g. "vis.0" or "javascript.0").')),
1525
+ ];
1526
+ break;
1527
+ case 'channel':
1528
+ value = [
1529
+ React.createElement("div", { key: 1 }, t('ra_Only following structures of objects are available:')),
1530
+ React.createElement("div", { key: 1 }, t('ra_Channel → State')),
1531
+ React.createElement("div", { key: 7, style: { height: 10 } }),
1532
+ React.createElement("div", { key: 8 }, t('ra_Non-experts may create new objects only in "0_userdata.0" or "alias.0".')),
1533
+ React.createElement("div", { key: 9 }, t('ra_The experts may create objects everywhere but from second level (e.g. "vis.0" or "javascript.0").')),
1534
+ ];
1535
+ break;
1536
+ default:
1537
+ break;
1538
+ }
1539
+ }
1540
+ else if (id.startsWith('alias.0') || id.startsWith('0_userdata')) {
1541
+ value = [
1542
+ React.createElement("div", { key: 1 }, t('ra_Only following structures of objects are available:')),
1543
+ React.createElement("div", { key: 2 }, t('ra_Folder → State')),
1544
+ React.createElement("div", { key: 3 }, t('ra_Folder → Channel → State')),
1545
+ React.createElement("div", { key: 4 }, t('ra_Folder → Device → Channel → State')),
1546
+ React.createElement("div", { key: 5 }, t('ra_Device → Channel → State')),
1547
+ React.createElement("div", { key: 6 }, t('ra_Channel → State')),
1548
+ React.createElement("div", { key: 7, style: { height: 10 } }),
1549
+ React.createElement("div", { key: 7 }, t('ra_Non-experts may create new objects only in "0_userdata.0" or "alias.0".')),
1550
+ React.createElement("div", { key: 8 }, t('ra_The experts may create objects everywhere but from second level (e.g. "vis.0" or "javascript.0").')),
1551
+ ];
1552
+ }
1553
+ }
1554
+ return value.length ? value : t('ra_Add new child object to selected parent');
1555
+ };
1556
+ onOpenFile() {
1557
+ const input = document.createElement('input');
1558
+ input.setAttribute('type', 'file');
1559
+ input.setAttribute('id', 'files');
1560
+ input.setAttribute('opacity', '0');
1561
+ input.addEventListener('change', (e) => this.handleJsonUpload(e), false);
1562
+ input.click();
1563
+ }
1564
+ renderInputJsonDialog() {
1565
+ return dialogs.renderInputJsonDialog(this);
1566
+ }
1567
+ /**
1568
+ * Renders the toolbar.
1569
+ */
1570
+ getToolbar() {
1571
+ return toolbar.getToolbar(this);
1572
+ }
1573
+ toggleExpanded(id) {
1574
+ const expanded = JSON.parse(JSON.stringify(this.state.expanded));
1575
+ const pos = expanded.indexOf(id);
1576
+ if (pos === -1) {
1577
+ expanded.push(id);
1578
+ expanded.sort();
1579
+ }
1580
+ else {
1581
+ expanded.splice(pos, 1);
1582
+ }
1583
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectExpanded`, JSON.stringify(expanded));
1584
+ this.setState({ expanded });
1585
+ }
1586
+ onCopy(e, text) {
1587
+ e.stopPropagation();
1588
+ e.preventDefault();
1589
+ if (text) {
1590
+ Utils.copyToClipboard(text);
1591
+ if (text.length < 50) {
1592
+ this.setState({ toast: this.props.t('ra_Copied %s', text) });
1593
+ }
1594
+ else {
1595
+ this.setState({ toast: this.props.t('ra_Copied') });
1596
+ }
1597
+ }
1598
+ }
1599
+ renderTooltipAccessControl(acl) {
1600
+ return leaf.renderTooltipAccessControl(this, acl);
1601
+ }
1602
+ renderColumnButtons(id, item) {
1603
+ return leaf.renderColumnButtons(this, id, item);
1604
+ }
1605
+ readHistory(id) {
1606
+ /* interface GetHistoryOptions {
1607
+ instance?: string;
1608
+ start?: number;
1609
+ end?: number;
1610
+ step?: number;
1611
+ count?: number;
1612
+ from?: boolean;
1613
+ ack?: boolean;
1614
+ q?: boolean;
1615
+ addID?: boolean;
1616
+ limit?: number;
1617
+ ignoreNull?: boolean;
1618
+ sessionId?: any;
1619
+ aggregate?: 'minmax' | 'min' | 'max' | 'average' | 'total' | 'count' | 'none';
1620
+ } */
1621
+ if (window.sparkline &&
1622
+ this.defaultHistory &&
1623
+ this.objects[id]?.common?.custom &&
1624
+ this.objects[id].common.custom[this.defaultHistory]) {
1625
+ const now = new Date();
1626
+ now.setHours(now.getHours() - 24);
1627
+ now.setMinutes(0);
1628
+ now.setSeconds(0);
1629
+ now.setMilliseconds(0);
1630
+ const nowMs = now.getTime();
1631
+ this.props.socket
1632
+ .getHistory(id, {
1633
+ instance: this.defaultHistory,
1634
+ start: nowMs,
1635
+ end: Date.now(),
1636
+ step: 3600000,
1637
+ from: false,
1638
+ ack: false,
1639
+ q: false,
1640
+ addId: false,
1641
+ aggregate: 'minmax',
1642
+ })
1643
+ .then(values => {
1644
+ const sparks = window.document.getElementsByClassName('sparkline');
1645
+ for (let s = 0; s < sparks.length; s++) {
1646
+ if (sparks[s].dataset.id === id) {
1647
+ const v = prepareSparkData(values, nowMs);
1648
+ window.sparkline.sparkline(sparks[s], v);
1649
+ break;
1650
+ }
1651
+ }
1652
+ })
1653
+ .catch(e => console.warn(`Cannot read history: ${e}`));
1654
+ }
1655
+ }
1656
+ getTooltipInfo(id, cb) {
1657
+ const obj = this.objects[id];
1658
+ const state = this.states[id];
1659
+ const { valFull, fileViewer } = formatValue({
1660
+ state,
1661
+ obj: obj,
1662
+ texts: this.texts,
1663
+ dateFormat: this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT,
1664
+ isFloatComma: this.props.isFloatComma === undefined
1665
+ ? (this.systemConfig?.common.isFloatComma ?? true)
1666
+ : this.props.isFloatComma,
1667
+ full: true,
1668
+ });
1669
+ const valFullRx = [];
1670
+ valFull?.forEach(_item => {
1671
+ if (_item.t === this.texts.quality && state.q) {
1672
+ valFullRx.push(React.createElement("div", { style: styles.cellValueTooltipBoth, key: _item.t },
1673
+ _item.t,
1674
+ ":\u00A0",
1675
+ _item.v));
1676
+ // <div style={styles.cellValueTooltipValue} key={item.t + '_v'}>{item.v}</div>,
1677
+ if (!_item.nbr) {
1678
+ valFullRx.push(React.createElement("br", { key: `${_item.t}_br` }));
1679
+ }
1680
+ }
1681
+ else {
1682
+ valFullRx.push(React.createElement("div", { style: styles.cellValueTooltipTitle, key: _item.t },
1683
+ _item.t,
1684
+ ":\u00A0"));
1685
+ valFullRx.push(React.createElement("div", { style: styles.cellValueTooltipValue, key: `${_item.t}_v` }, _item.v));
1686
+ if (!_item.nbr) {
1687
+ valFullRx.push(React.createElement("br", { key: `${_item.t}_br` }));
1688
+ }
1689
+ }
1690
+ });
1691
+ const role = obj?.common?.role || '';
1692
+ if (fileViewer === 'image') {
1693
+ valFullRx.push(React.createElement("img", { style: styles.cellValueTooltipImage, src: state.val, alt: id }));
1694
+ }
1695
+ else if (role === 'url' || role === 'url.self' || role === 'url.blank') {
1696
+ // Show comment about "Hold Ctrl/⌘ key to open the link"
1697
+ valFullRx.unshift(React.createElement("div", { key: "ctrl", style: { textDecoration: 'underline', fontWeight: 'bold' } }, this.texts.ctrlForLink));
1698
+ }
1699
+ else if (this.defaultHistory && obj?.common?.custom?.[this.defaultHistory]) {
1700
+ valFullRx.push(React.createElement("svg", { key: "sparkline", className: "sparkline", "data-id": id, style: { fill: '#3d85de' }, width: "200", height: "30", strokeWidth: "3" }));
1701
+ }
1702
+ this.setState({ tooltipInfo: { el: valFullRx, id } }, () => cb && cb());
1703
+ }
1704
+ _syncEnum(id, enumIds, newArray, cb) {
1705
+ if (!enumIds?.length) {
1706
+ if (cb) {
1707
+ cb();
1708
+ }
1709
+ return;
1710
+ }
1711
+ const enumId = enumIds.pop() || '';
1712
+ const promises = [];
1713
+ if (this.info.objects[enumId]?.common) {
1714
+ if (this.info.objects[enumId].common.members?.length) {
1715
+ const pos = this.info.objects[enumId].common.members.indexOf(id);
1716
+ if (pos !== -1 && !newArray.includes(enumId)) {
1717
+ // delete it from members
1718
+ const obj = JSON.parse(JSON.stringify(this.info.objects[enumId]));
1719
+ obj.common.members.splice(pos, 1);
1720
+ promises.push(this.props.socket
1721
+ .setObject(enumId, obj)
1722
+ .then(() => (this.info.objects[enumId] = obj))
1723
+ .catch(e => this.showError(e)));
1724
+ }
1725
+ }
1726
+ // add to it
1727
+ if (newArray.includes(enumId) && !this.info.objects[enumId].common.members?.includes(id)) {
1728
+ // add to object
1729
+ const obj = JSON.parse(JSON.stringify(this.info.objects[enumId]));
1730
+ obj.common.members ||= [];
1731
+ obj.common.members.push(id);
1732
+ obj.common.members.sort();
1733
+ promises.push(this.props.socket
1734
+ .setObject(enumId, obj)
1735
+ .then(() => (this.info.objects[enumId] = obj))
1736
+ .catch(e => this.showError(e)));
1737
+ }
1738
+ }
1739
+ void Promise.all(promises).then(() => {
1740
+ setTimeout(() => this._syncEnum(id, enumIds, newArray, cb), 0);
1741
+ });
1742
+ }
1743
+ syncEnum(id, enumName, newArray) {
1744
+ const toCheck = [...this.info[enumName === 'func' ? 'funcEnums' : 'roomEnums']];
1745
+ return new Promise(resolve => {
1746
+ this._syncEnum(id, toCheck, newArray, () => {
1747
+ // force update of an object
1748
+ resolve();
1749
+ });
1750
+ });
1751
+ }
1752
+ onColumnsEditCustomDialogClose(isSave) {
1753
+ // cannot be null
1754
+ const customColumnDialog = this.customColumnDialog;
1755
+ if (isSave) {
1756
+ let value = customColumnDialog.value;
1757
+ if (customColumnDialog.type === 'boolean') {
1758
+ value = value === 'true' || value === true;
1759
+ }
1760
+ else if (customColumnDialog.type === 'number') {
1761
+ value = parseFloat(value);
1762
+ }
1763
+ const it = this.state.columnsEditCustomDialog?.it;
1764
+ this.customColumnDialog = null;
1765
+ this.props.socket
1766
+ .getObject(this.state.columnsEditCustomDialog?.obj?._id || '')
1767
+ .then(obj => {
1768
+ if (obj && it && setCustomValue(obj, it, value)) {
1769
+ return this.props.socket.setObject(obj._id, obj);
1770
+ }
1771
+ throw new Error(this.props.t('ra_Cannot update attribute, because not found in the object'));
1772
+ })
1773
+ .then(() => this.setState({ columnsEditCustomDialog: null }))
1774
+ .catch(e => this.showError(e));
1775
+ }
1776
+ else {
1777
+ this.customColumnDialog = null;
1778
+ this.setState({ columnsEditCustomDialog: null });
1779
+ }
1780
+ }
1781
+ /**
1782
+ * Renders a custom value.
1783
+ */
1784
+ renderCustomValue(obj, it, item) {
1785
+ return leaf.renderCustomValue(this, obj, it, item);
1786
+ }
1787
+ renderAliasLink(id, index, customStyle) {
1788
+ return leaf.renderAliasLink(this, id, index, customStyle);
1789
+ }
1790
+ /**
1791
+ * Renders one row of the table.
1792
+ */
1793
+ renderLeaf(item, isExpanded, counter) {
1794
+ return leaf.renderLeaf(this, item, isExpanded, counter);
1795
+ }
1796
+ /**
1797
+ * Renders an item.
1798
+ */
1799
+ renderItem(root, isExpanded, counter) {
1800
+ return leaf.renderItem(this, root, isExpanded, counter);
1801
+ }
1802
+ calculateColumnsVisibility(aColumnsAuto, aColumns, aColumnsForAdmin, aColumnsWidths) {
1803
+ let columnsWidths = aColumnsWidths || this.state.columnsWidths;
1804
+ const columnsForAdmin = aColumnsForAdmin || this.state.columnsForAdmin;
1805
+ const columns = aColumns || this.state.columns || [];
1806
+ const columnsAuto = typeof aColumnsAuto !== 'boolean' ? this.state.columnsAuto : aColumnsAuto;
1807
+ columnsWidths = JSON.parse(JSON.stringify(columnsWidths));
1808
+ Object.keys(columnsWidths).forEach(name => {
1809
+ if (columnsWidths[name]) {
1810
+ columnsWidths[name] = parseInt(columnsWidths[name], 10) || 0;
1811
+ }
1812
+ });
1813
+ this.adapterColumns = [];
1814
+ // In the auto mode the visible columns depend on the width of the container
1815
+ if (!this.props.columns) {
1816
+ this.visibleCols = [...this.screenWidths[this.width].fields];
1817
+ // remove type column if only one type must be selected
1818
+ if (this.props.types && this.props.types.length === 1) {
1819
+ const pos = this.visibleCols.indexOf('type');
1820
+ if (pos !== -1) {
1821
+ this.visibleCols.splice(pos, 1);
1822
+ }
1823
+ }
1824
+ }
1825
+ const WIDTHS = this.screenWidths[this.width].widths;
1826
+ if (columnsAuto) {
1827
+ this.columnsVisibility = {
1828
+ id: this.screenWidths[this.width || 'lg'].idWidth,
1829
+ name: this.visibleCols.includes('name') ? WIDTHS.name || 0 : 0,
1830
+ type: this.visibleCols.includes('type') ? WIDTHS.type || 0 : 0,
1831
+ role: this.visibleCols.includes('role') ? WIDTHS.role || 0 : 0,
1832
+ room: this.visibleCols.includes('room') ? WIDTHS.room || 0 : 0,
1833
+ func: this.visibleCols.includes('func') ? WIDTHS.func || 0 : 0,
1834
+ changedFrom: this.visibleCols.includes('changedFrom') ? WIDTHS.changedFrom || 0 : 0,
1835
+ qualityCode: this.visibleCols.includes('qualityCode') ? WIDTHS.qualityCode || 0 : 0,
1836
+ timestamp: this.visibleCols.includes('timestamp') ? WIDTHS.timestamp || 0 : 0,
1837
+ lastChange: this.visibleCols.includes('lastChange') ? WIDTHS.lastChange || 0 : 0,
1838
+ val: this.visibleCols.includes('val') ? WIDTHS.val || 0 : 0,
1839
+ buttons: this.visibleCols.includes('buttons') ? WIDTHS.buttons || 0 : 0,
1840
+ };
1841
+ // in xs name is not visible
1842
+ if (this.columnsVisibility.name && !this.customWidth) {
1843
+ let widthSum = this.columnsVisibility.id || 0; // id is always visible
1844
+ if (this.state.statesView) {
1845
+ widthSum += this.columnsVisibility.changedFrom || 0;
1846
+ widthSum += this.columnsVisibility.qualityCode || 0;
1847
+ widthSum += this.columnsVisibility.timestamp || 0;
1848
+ widthSum += this.columnsVisibility.lastChange || 0;
1849
+ }
1850
+ else {
1851
+ widthSum += this.columnsVisibility.type || 0;
1852
+ widthSum += this.columnsVisibility.role || 0;
1853
+ widthSum += this.columnsVisibility.room || 0;
1854
+ widthSum += this.columnsVisibility.func || 0;
1855
+ }
1856
+ widthSum += this.columnsVisibility.val || 0;
1857
+ widthSum += this.columnsVisibility.buttons || 0;
1858
+ this.columnsVisibility.name = `calc(100% - ${widthSum + 5}px)`;
1859
+ }
1860
+ else if (!this.customWidth) {
1861
+ // Calculate the width of ID
1862
+ let widthSum = 0; // id is always visible
1863
+ if (this.state.statesView) {
1864
+ widthSum += this.columnsVisibility.changedFrom || 0;
1865
+ widthSum += this.columnsVisibility.qualityCode || 0;
1866
+ widthSum += this.columnsVisibility.timestamp || 0;
1867
+ widthSum += this.columnsVisibility.lastChange || 0;
1868
+ }
1869
+ else {
1870
+ widthSum += this.columnsVisibility.type || 0;
1871
+ widthSum += this.columnsVisibility.role || 0;
1872
+ widthSum += this.columnsVisibility.room || 0;
1873
+ widthSum += this.columnsVisibility.func || 0;
1874
+ }
1875
+ widthSum += this.columnsVisibility.val || 0;
1876
+ widthSum += this.columnsVisibility.buttons || 0;
1877
+ this.columnsVisibility.id = `calc(100% - ${widthSum + 5}px)`;
1878
+ }
1879
+ }
1880
+ else {
1881
+ const width = this.width || 'lg';
1882
+ this.columnsVisibility = {
1883
+ id: columnsWidths.id || this.screenWidths[width].idWidth,
1884
+ name: columns.includes('name')
1885
+ ? columnsWidths.name || WIDTHS.name || this.screenWidths[width].widths.name || 0
1886
+ : 0,
1887
+ type: columns.includes('type')
1888
+ ? columnsWidths.type || WIDTHS.type || this.screenWidths[width].widths.type || 0
1889
+ : 0,
1890
+ role: columns.includes('role')
1891
+ ? columnsWidths.role || WIDTHS.role || this.screenWidths[width].widths.role || 0
1892
+ : 0,
1893
+ room: columns.includes('room')
1894
+ ? columnsWidths.room || WIDTHS.room || this.screenWidths[width].widths.room || 0
1895
+ : 0,
1896
+ func: columns.includes('func')
1897
+ ? columnsWidths.func || WIDTHS.func || this.screenWidths[width].widths.func || 0
1898
+ : 0,
1899
+ };
1900
+ let widthSum = this.columnsVisibility.id; // id is always visible
1901
+ if (this.columnsVisibility.name) {
1902
+ widthSum += this.columnsVisibility.type || 0;
1903
+ widthSum += this.columnsVisibility.role || 0;
1904
+ widthSum += this.columnsVisibility.room || 0;
1905
+ widthSum += this.columnsVisibility.func || 0;
1906
+ }
1907
+ if (columnsForAdmin && columns) {
1908
+ Object.keys(columnsForAdmin)
1909
+ .sort()
1910
+ .forEach(adapter => columnsForAdmin[adapter].forEach(column => {
1911
+ const id = `_${adapter}_${column.path}`;
1912
+ if (columns.includes(id)) {
1913
+ const item = {
1914
+ adapter,
1915
+ id: `_${adapter}_${column.path}`,
1916
+ name: column.name,
1917
+ path: column.path.split('.'),
1918
+ pathText: column.path,
1919
+ };
1920
+ if (column.edit) {
1921
+ item.edit = true;
1922
+ if (column.type) {
1923
+ item.type = column.type;
1924
+ }
1925
+ if (column.objTypes) {
1926
+ item.objTypes = column.objTypes;
1927
+ }
1928
+ }
1929
+ this.adapterColumns.push(item);
1930
+ this.columnsVisibility[id] =
1931
+ columnsWidths[item.id] ||
1932
+ column.width ||
1933
+ this.screenWidths[width].widths.func ||
1934
+ this.screenWidths.xl.widths.func ||
1935
+ 0;
1936
+ widthSum += this.columnsVisibility[id];
1937
+ }
1938
+ else {
1939
+ this.columnsVisibility[id] = 0;
1940
+ }
1941
+ }));
1942
+ }
1943
+ this.adapterColumns.sort((a, b) => (a.id > b.id ? -1 : a.id < b.id ? 1 : 0));
1944
+ this.columnsVisibility.val = columns.includes('val')
1945
+ ? columnsWidths.val || WIDTHS.val || this.screenWidths.xl.widths.val
1946
+ : 0;
1947
+ // do not show buttons if not desired
1948
+ if (!this.props.columns || this.props.columns.includes('buttons')) {
1949
+ this.columnsVisibility.buttons = columns.includes('buttons')
1950
+ ? columnsWidths.buttons || WIDTHS.buttons || this.screenWidths.xl.widths.buttons
1951
+ : 0;
1952
+ widthSum += this.columnsVisibility.buttons || 0;
1953
+ }
1954
+ if (this.columnsVisibility.name && !columnsWidths.name) {
1955
+ widthSum += this.columnsVisibility.val || 0;
1956
+ this.columnsVisibility.name = `calc(100% - ${widthSum}px)`;
1957
+ }
1958
+ else {
1959
+ const newWidth = Object.keys(this.columnsVisibility).reduce((accumulator, name) => {
1960
+ // do not summarize strings
1961
+ if (name === 'id' ||
1962
+ typeof this.columnsVisibility[name] === 'string' ||
1963
+ !this.columnsVisibility[name]) {
1964
+ return accumulator;
1965
+ }
1966
+ return accumulator + this.columnsVisibility[name];
1967
+ }, 0);
1968
+ this.columnsVisibility.id = `calc(100% - ${newWidth}px)`;
1969
+ }
1970
+ }
1971
+ }
1972
+ /**
1973
+ * The width class of the object browser. It is measured from the container and not from the
1974
+ * window, so the browser shows the right columns if it is used in a narrow panel or dialog too.
1975
+ * `props.width` overwrites the measured value.
1976
+ */
1977
+ get width() {
1978
+ return this.props.width || this.state.containerWidth;
1979
+ }
1980
+ /** Watch the width of the table container to detect the width class (see `width`) */
1981
+ observeContainerWidth() {
1982
+ const container = this.tableRef.current;
1983
+ if (!container || this.observedContainer === container) {
1984
+ return;
1985
+ }
1986
+ this.resizeObserver?.disconnect();
1987
+ this.observedContainer = container;
1988
+ this.resizeObserver = new ResizeObserver(entries => this.setContainerWidth(entries[0].contentRect.width));
1989
+ this.resizeObserver.observe(container);
1990
+ // measure immediately, so the first render after mounting shows the right columns
1991
+ this.setContainerWidth(container.clientWidth);
1992
+ }
1993
+ /** Store the width class of the container if it changed */
1994
+ setContainerWidth(containerWidth) {
1995
+ const width = widthFromContainer(containerWidth, this.props.theme);
1996
+ if (width !== this.state.containerWidth) {
1997
+ // the columns are recalculated in `render`, as soon as the width class changed
1998
+ this.setState({ containerWidth: width });
1999
+ }
2000
+ }
2001
+ /**
2002
+ * All column widths as CSS variables of the table container. The header and every row read the
2003
+ * widths from here (see `colWidth`), so they can never get out of sync, and the resizer can
2004
+ * change a width by writing one variable instead of re-rendering all rows.
2005
+ */
2006
+ getColumnWidthVariables() {
2007
+ const variables = {};
2008
+ Object.keys(this.columnsVisibility).forEach(name => {
2009
+ const width = this.columnsVisibility[name];
2010
+ if (typeof width === 'string') {
2011
+ // `calc(100% - 500px)`: this column takes the remaining place. A percentage cannot be
2012
+ // used here, because the width of the row depends on the columns and would be
2013
+ // circular - the column grows instead and keeps its minimal width if the place is
2014
+ // not sufficient (then the table scrolls horizontally).
2015
+ // `100%` (mobile view) must not reserve any place, `calc(100% - 500px)` keeps a minimum
2016
+ variables[colVar(name)] = width === '100%' ? '0px' : `${MIN_COLUMN_WIDTHS[name] || 100}px`;
2017
+ variables[growVar(name)] = '1';
2018
+ }
2019
+ else {
2020
+ variables[colVar(name)] = `${width || 0}px`;
2021
+ variables[growVar(name)] = '0';
2022
+ }
2023
+ });
2024
+ return variables;
2025
+ }
2026
+ /** Set the width of one column without re-rendering the rows */
2027
+ setColumnWidth(name, width) {
2028
+ this.columnsVisibility[name] = width;
2029
+ this.tableRef.current?.style.setProperty(colVar(name), `${width}px`);
2030
+ }
2031
+ resizerMouseMove = (e) => {
2032
+ if (this.resizerActiveDiv) {
2033
+ let width;
2034
+ let widthNext;
2035
+ if (this.resizeLeft) {
2036
+ width = this.resizerOldWidth - e.clientX + this.resizerPosition;
2037
+ widthNext = this.resizerOldWidthNext + e.clientX - this.resizerPosition;
2038
+ }
2039
+ else {
2040
+ width = this.resizerOldWidth + e.clientX - this.resizerPosition;
2041
+ widthNext = this.resizerOldWidthNext - e.clientX + this.resizerPosition;
2042
+ }
2043
+ if (this.resizerActiveName &&
2044
+ this.resizerNextName &&
2045
+ (!this.resizerMin || width > this.resizerMin) &&
2046
+ (!this.resizerNextMin || widthNext > this.resizerNextMin)) {
2047
+ this.resizerCurrentWidths[this.resizerActiveName] = width;
2048
+ this.resizerCurrentWidths[this.resizerNextName] = widthNext;
2049
+ // Only two CSS variables are written - the header and all rows follow immediately,
2050
+ // without a re-render of the table
2051
+ this.setColumnWidth(this.resizerActiveName, width);
2052
+ this.setColumnWidth(this.resizerNextName, widthNext);
2053
+ this.customWidth = true;
2054
+ }
2055
+ }
2056
+ };
2057
+ resizerMouseUp = () => {
2058
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.table`, JSON.stringify(this.resizerCurrentWidths));
2059
+ this.resizerActiveName = null;
2060
+ this.resizerNextName = null;
2061
+ this.resizerActiveDiv = null;
2062
+ this.resizerNextDiv = null;
2063
+ window.removeEventListener('mousemove', this.resizerMouseMove);
2064
+ window.removeEventListener('mouseup', this.resizerMouseUp);
2065
+ };
2066
+ resizerMouseDown = (e) => {
2067
+ this.storedWidths ||= JSON.parse(JSON.stringify(this.screenWidths[this.width || 'lg']));
2068
+ this.resizerCurrentWidths ||= {};
2069
+ this.resizerActiveDiv = e.target.parentNode;
2070
+ this.resizerActiveName = this.resizerActiveDiv.dataset.name || null;
2071
+ if (this.resizerActiveName) {
2072
+ let i = 0;
2073
+ if (e.target.dataset.left === 'true') {
2074
+ this.resizeLeft = true;
2075
+ this.resizerNextDiv = this.resizerActiveDiv.previousElementSibling;
2076
+ let handle = this.resizerNextDiv.querySelector('.iob-ob-resize-handler');
2077
+ while (this.resizerNextDiv && !handle && i < 10) {
2078
+ this.resizerNextDiv = this.resizerNextDiv.previousElementSibling;
2079
+ handle = this.resizerNextDiv.querySelector('.iob-ob-resize-handler');
2080
+ i++;
2081
+ }
2082
+ if (handle?.dataset.left !== 'true') {
2083
+ this.resizerNextDiv = this.resizerNextDiv.nextElementSibling;
2084
+ }
2085
+ }
2086
+ else {
2087
+ this.resizeLeft = false;
2088
+ this.resizerNextDiv = this.resizerActiveDiv.nextElementSibling;
2089
+ /* while (this.resizerNextDiv && !this.resizerNextDiv.querySelector('.iob-ob-resize-handler') && i < 10) {
2090
+ this.resizerNextDiv = this.resizerNextDiv.nextElementSibling;
2091
+ i++;
2092
+ } */
2093
+ }
2094
+ this.resizerNextName = this.resizerNextDiv.dataset.name || null;
2095
+ this.resizerMin = parseInt(this.resizerActiveDiv.dataset.min, 10) || 0;
2096
+ this.resizerNextMin = parseInt(this.resizerNextDiv.dataset.min, 10) || 0;
2097
+ this.resizerPosition = e.clientX;
2098
+ this.resizerCurrentWidths[this.resizerActiveName] = this.resizerActiveDiv.offsetWidth;
2099
+ this.resizerOldWidth = this.resizerCurrentWidths[this.resizerActiveName];
2100
+ if (this.resizerNextName) {
2101
+ this.resizerCurrentWidths[this.resizerNextName] = this.resizerNextDiv.offsetWidth;
2102
+ this.resizerOldWidthNext = this.resizerCurrentWidths[this.resizerNextName];
2103
+ }
2104
+ window.addEventListener('mousemove', this.resizerMouseMove);
2105
+ window.addEventListener('mouseup', this.resizerMouseUp);
2106
+ }
2107
+ };
2108
+ /**
2109
+ * Handle keyboard events for navigation
2110
+ */
2111
+ navigateKeyPress(event) {
2112
+ const selectedId = this.state.selectedNonObject || this.state.selected[0];
2113
+ if (!selectedId) {
2114
+ return;
2115
+ }
2116
+ if (event.code === 'ArrowUp' || event.code === 'ArrowDown') {
2117
+ event.preventDefault();
2118
+ const ids = [];
2119
+ // the first child is the header, it has no ID
2120
+ this.tableRef.current?.childNodes.forEach((node) => {
2121
+ const nodeId = node.id;
2122
+ if (nodeId) {
2123
+ ids.push(nodeId);
2124
+ }
2125
+ });
2126
+ const idx = ids.indexOf(selectedId);
2127
+ const newIdx = event.code === 'ArrowDown' ? idx + 1 : idx - 1;
2128
+ const newId = ids[newIdx] || selectedId;
2129
+ this.onSelect(newId);
2130
+ this.scrollToItem(newId);
2131
+ }
2132
+ if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
2133
+ this.toggleExpanded(selectedId);
2134
+ }
2135
+ if (event.code === 'Delete' && this.root && selectedId) {
2136
+ const item = ObjectBrowserClass.getItemFromRoot(this.root, selectedId);
2137
+ if (item) {
2138
+ const { obj } = item.data;
2139
+ if (obj && !obj.common?.dontDelete) {
2140
+ this.showDeleteDialog({ id: selectedId, obj, item });
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ /**
2146
+ * Find the id from the root
2147
+ *
2148
+ * @param root The current root
2149
+ * @param id The object id to find
2150
+ */
2151
+ static getItemFromRoot(root, id) {
2152
+ const idArr = id.split('.');
2153
+ let currId = '';
2154
+ let _root = root;
2155
+ for (let i = 0; i < idArr.length; i++) {
2156
+ const idEntry = idArr[i];
2157
+ currId = currId ? `${currId}.${idEntry}` : idEntry;
2158
+ let found = false;
2159
+ if (_root.children) {
2160
+ for (let j = 0; j < _root.children.length; j++) {
2161
+ if (_root.children[j].data.id === currId) {
2162
+ _root = _root.children[j];
2163
+ found = true;
2164
+ break;
2165
+ }
2166
+ }
2167
+ }
2168
+ if (!found) {
2169
+ return null;
2170
+ }
2171
+ }
2172
+ return _root || null;
2173
+ }
2174
+ resizerReset = () => {
2175
+ this.customWidth = false;
2176
+ this.screenWidths[this.width || 'lg'] = JSON.parse(JSON.stringify(this.storedWidths));
2177
+ this.calculateColumnsVisibility();
2178
+ this.localStorage.removeItem(`${this.props.dialogName || 'App'}.table`);
2179
+ this.forceUpdate();
2180
+ };
2181
+ /**
2182
+ * Render the right handle for resizing
2183
+ */
2184
+ renderHandleRight() {
2185
+ return toolbar.renderHandleRight(this);
2186
+ }
2187
+ // --- Routing (navigateTo / onNavigateTo) ---
2188
+ // The browser never reads the URL itself; the parent drives it via `navigateTo` and is informed
2189
+ // of user navigation via `onNavigateTo`. All URL parsing/writing lives in the parent component.
2190
+ /** Derive the current navigation target from the dialog/selection state. */
2191
+ getStateNav() {
2192
+ if (this.state.editObjectDialog) {
2193
+ return { mode: 'edit', id: this.state.editObjectDialog };
2194
+ }
2195
+ if (this.state.customDialog && this.state.customDialog.length === 1 && !this.state.customDialogAll) {
2196
+ return { mode: 'settings', id: this.state.customDialog[0] };
2197
+ }
2198
+ if (this.state.viewFileDialog) {
2199
+ return { mode: 'viewFile', id: this.state.viewFileDialog };
2200
+ }
2201
+ if (this.state.selected.length === 1 && this.state.selected[0]) {
2202
+ return { mode: 'select', id: this.state.selected[0] };
2203
+ }
2204
+ return null;
2205
+ }
2206
+ static navEqual(a, b) {
2207
+ if (!a || !b) {
2208
+ return !a && !b;
2209
+ }
2210
+ return a.mode === b.mode && a.id === b.id;
2211
+ }
2212
+ /** Apply a navigation target coming from the parent (`navigateTo`): select + open the dialog. */
2213
+ applyNavigateTo(nav) {
2214
+ this.applyingNav = true;
2215
+ const done = () => {
2216
+ this.applyingNav = false;
2217
+ };
2218
+ if (!nav?.id) {
2219
+ // No target: just close any open dialog (keep the current selection).
2220
+ this.setState({ editObjectDialog: '', customDialog: null, viewFileDialog: '' }, done);
2221
+ return;
2222
+ }
2223
+ const { id } = nav;
2224
+ const open = () => {
2225
+ if (nav.mode === 'edit') {
2226
+ this.setState({ editObjectDialog: id, editObjectAlias: false, customDialog: null, viewFileDialog: '' }, done);
2227
+ }
2228
+ else if (nav.mode === 'settings') {
2229
+ this.setState({ customDialog: [id], customDialogAll: false, editObjectDialog: '', viewFileDialog: '' }, done);
2230
+ }
2231
+ else if (nav.mode === 'viewFile') {
2232
+ this.setState({ viewFileDialog: id, editObjectDialog: '', customDialog: null }, done);
2233
+ }
2234
+ else {
2235
+ this.setState({ editObjectDialog: '', customDialog: null, viewFileDialog: '' }, done);
2236
+ }
2237
+ };
2238
+ // Select the target (if needed), expand to it and scroll into view, then open the dialog.
2239
+ if (this.state.selected.length === 1 && this.state.selected[0] === id) {
2240
+ open();
2241
+ }
2242
+ else {
2243
+ this.onSelect(id, false, () => this.expandAllSelected(() => {
2244
+ this.scrollToItem(id);
2245
+ open();
2246
+ }));
2247
+ }
2248
+ }
2249
+ /** Apply the initial `navigateTo` after the tree has loaded (called from componentDidMount). */
2250
+ applyInitialNavigateTo() {
2251
+ const nav = this.props.navigateTo ?? null;
2252
+ if (nav?.id) {
2253
+ this.lastNav = nav;
2254
+ this.applyNavigateTo(nav);
2255
+ }
2256
+ else {
2257
+ // Don't push the restored-from-localStorage selection into the URL on load.
2258
+ this.lastNav = this.getStateNav();
2259
+ }
2260
+ }
2261
+ /** Reconcile `navigateTo` (parent/URL) with the browser's selection/dialog state. */
2262
+ reconcileNavigation(prevProps) {
2263
+ if (this.props.navigateTo === undefined && !this.props.onNavigateTo) {
2264
+ return; // routing not used by this consumer
2265
+ }
2266
+ if (this.applyingNav) {
2267
+ return; // currently applying a target; ignore the intermediate state
2268
+ }
2269
+ const propNav = this.props.navigateTo ?? null;
2270
+ const stateNav = this.getStateNav();
2271
+ if (ObjectBrowserClass.navEqual(propNav, stateNav)) {
2272
+ this.lastNav = stateNav;
2273
+ return;
2274
+ }
2275
+ if (!ObjectBrowserClass.navEqual(propNav, prevProps.navigateTo ?? null)) {
2276
+ // The parent (URL) drove a change → apply it to the browser.
2277
+ this.lastNav = propNav;
2278
+ this.applyNavigateTo(propNav);
2279
+ }
2280
+ else if (!ObjectBrowserClass.navEqual(stateNav, this.lastNav)) {
2281
+ // The user changed selection/dialog → report it so the parent can update the URL.
2282
+ this.lastNav = stateNav;
2283
+ this.props.onNavigateTo?.(stateNav);
2284
+ }
2285
+ }
2286
+ /**
2287
+ * Called when component is updated.
2288
+ */
2289
+ componentDidUpdate(prevProps) {
2290
+ // the table is rendered only after the objects are loaded
2291
+ this.observeContainerWidth();
2292
+ if (this.tableRef.current && this.selectFirst) {
2293
+ this.scrollToItem(this.selectFirst);
2294
+ }
2295
+ this.reconcileNavigation(prevProps);
2296
+ }
2297
+ scrollToItem(id) {
2298
+ this.selectFirst = '';
2299
+ const node = window.document.getElementById(id);
2300
+ node?.scrollIntoView({
2301
+ behavior: 'auto',
2302
+ block: 'center',
2303
+ inline: 'center',
2304
+ });
2305
+ }
2306
+ onUpdate(valAck) {
2307
+ this.props.socket
2308
+ .setState(this.edit.id, {
2309
+ val: valAck.val,
2310
+ ack: valAck.ack,
2311
+ q: valAck.q || 0,
2312
+ expire: valAck.expire || undefined,
2313
+ })
2314
+ .catch(e => this.showError(`Cannot write value: ${e}`));
2315
+ }
2316
+ showAddDataPointDialog(id, initialType, initialStateType) {
2317
+ this.setState({
2318
+ showContextMenu: null,
2319
+ modalNewObj: {
2320
+ id,
2321
+ initialType,
2322
+ initialStateType,
2323
+ },
2324
+ });
2325
+ }
2326
+ /** Renders the aliases list for one state (if more than 2) */
2327
+ doFilter(doNotStore) {
2328
+ if (!this.objects || !this.root) {
2329
+ return;
2330
+ }
2331
+ if (!doNotStore) {
2332
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(this.state.filter));
2333
+ }
2334
+ const counter = { count: 0 };
2335
+ applyFilter(this.root, this.state.filter, this.props.lang, this.objects, undefined, counter, this.props.customFilter, this.props.types);
2336
+ if (counter.count < 500 && !this.state.expandAllVisible) {
2337
+ setTimeout(() => this.setState({ expandAllVisible: true }));
2338
+ }
2339
+ else if (counter.count >= 500 && this.state.expandAllVisible) {
2340
+ setTimeout(() => this.setState({ expandAllVisible: false }));
2341
+ }
2342
+ else {
2343
+ this.forceUpdate();
2344
+ }
2345
+ }
2346
+ /**
2347
+ * The rendering method of this component.
2348
+ */
2349
+ render() {
2350
+ this.recordStates = [];
2351
+ if (this.unsubscribeTimer) {
2352
+ clearTimeout(this.unsubscribeTimer);
2353
+ }
2354
+ if (this.styleTheme !== this.props.themeType) {
2355
+ this.styles = {
2356
+ cellIdIconFolder: Utils.getStyle(this.props.theme, styles.cellIdIconFolder),
2357
+ cellIdIconDocument: Utils.getStyle(this.props.theme, styles.cellIdIconDocument),
2358
+ iconDeviceError: Utils.getStyle(this.props.theme, styles.iconDeviceError),
2359
+ iconDeviceConnected: Utils.getStyle(this.props.theme, styles.iconDeviceConnected),
2360
+ iconDeviceDisconnected: Utils.getStyle(this.props.theme, styles.iconDeviceDisconnected),
2361
+ cellButtonsButtonWithCustoms: Utils.getStyle(this.props.theme, styles.cellButtonsButtonWithCustoms),
2362
+ invertedBackground: Utils.getStyle(this.props.theme, styles.invertedBackground),
2363
+ invertedBackgroundFlex: Utils.getStyle(this.props.theme, styles.invertedBackgroundFlex),
2364
+ contextMenuEdit: Utils.getStyle(this.props.theme, styles.contextMenuEdit),
2365
+ contextMenuEditValue: Utils.getStyle(this.props.theme, styles.contextMenuEditValue),
2366
+ contextMenuView: Utils.getStyle(this.props.theme, styles.contextMenuView),
2367
+ contextMenuCustom: Utils.getStyle(this.props.theme, styles.contextMenuCustom),
2368
+ contextMenuACL: Utils.getStyle(this.props.theme, styles.contextMenuACL),
2369
+ contextMenuRoom: Utils.getStyle(this.props.theme, styles.contextMenuRoom),
2370
+ contextMenuRole: Utils.getStyle(this.props.theme, styles.contextMenuRole),
2371
+ contextMenuDelete: Utils.getStyle(this.props.theme, styles.contextMenuDelete),
2372
+ filterInput: Utils.getStyle(this.props.theme, styles.headerCellInput, styles.filterInput),
2373
+ iconCopy: Utils.getStyle(this.props.theme, styles.cellButtonsValueButton, styles.cellButtonsValueButtonCopy),
2374
+ aliasReadWrite: Utils.getStyle(this.props.theme, styles.cellIdAlias, styles.cellIdAliasReadWrite),
2375
+ aliasAlone: Utils.getStyle(this.props.theme, styles.cellIdAlias, styles.cellIdAliasAlone),
2376
+ };
2377
+ this.styleTheme = this.props.themeType;
2378
+ }
2379
+ this.unsubscribeTimer = setTimeout(() => {
2380
+ this.unsubscribeTimer = null;
2381
+ this.checkUnsubscribes();
2382
+ }, 200);
2383
+ if (this.expertMode !== !!this.state.filter.expertMode) {
2384
+ this.expertMode = !!this.state.filter.expertMode;
2385
+ this.doFilter(true);
2386
+ }
2387
+ if (!this.state.loaded) {
2388
+ return React.createElement(CircularProgress, { key: `${this.props.dialogName}_c` });
2389
+ }
2390
+ // the container was resized into another width class => other columns are visible
2391
+ if (this.lastCalculatedWidth !== this.width) {
2392
+ this.lastCalculatedWidth = this.width;
2393
+ this.calculateColumnsVisibility();
2394
+ }
2395
+ const items = this.root ? this.renderItem(this.root, undefined) : null;
2396
+ return (React.createElement(TabContainer, { key: this.props.dialogName, styles: { root: this.props.style } },
2397
+ React.createElement("style", null, `
2398
+ @keyframes newValueAnimation-light {
2399
+ 0% {
2400
+ color: #00f900;
2401
+ }
2402
+ 80% {
2403
+ color: #008000;
2404
+ }
2405
+ 100% {
2406
+ color: #000;
2407
+ }
2408
+ }
2409
+ @keyframes newValueAnimation-dark {
2410
+ 0% {
2411
+ color: #00f900;
2412
+ }
2413
+ 80% {
2414
+ color: #008000;
2415
+ }
2416
+ 100% {
2417
+ color: #fff;
2418
+ }
2419
+ }
2420
+ .newValueBrowser-dark {
2421
+ animation: newValueAnimation-dark 2s ease-in-out;
2422
+ }
2423
+ .newValueBrowser-light {
2424
+ animation: newValueAnimation-light 2s ease-in-out;
2425
+ }
2426
+ .highlight-link .iob-link {
2427
+ text-decoration: underline;
2428
+ cursor: pointer;
2429
+ }
2430
+ `),
2431
+ React.createElement(TabHeader, null, this.getToolbar()),
2432
+ React.createElement(TabContent, null,
2433
+ React.createElement(Box, { style: { ...styles.tableDiv, ...this.getColumnWidthVariables() }, ref: this.tableRef, onKeyDown: event => this.navigateKeyPress(event) },
2434
+ toolbar.renderHeader(this),
2435
+ items)),
2436
+ contextMenu.renderContextMenu(this),
2437
+ dialogs.renderAliasMenu(this),
2438
+ dialogs.renderToast(this),
2439
+ dialogs.renderColumnsEditCustomDialog(this),
2440
+ this.renderColumnsSelectorDialog(),
2441
+ dialogs.renderCustomDialog(this),
2442
+ dialogs.renderEditValueDialog(this),
2443
+ dialogs.renderEditObjectDialog(this),
2444
+ dialogs.renderViewObjectFileDialog(this),
2445
+ dialogs.renderAliasEditorDialog(this),
2446
+ dialogs.renderEditRoleDialog(this),
2447
+ dialogs.renderEnumDialog(this),
2448
+ this.renderErrorDialog(),
2449
+ this.renderExportDialog(),
2450
+ this.renderRenameDialog(),
2451
+ this.renderInputJsonDialog(),
2452
+ this.state.modalNewObj && this.props.modalNewObject && this.props.modalNewObject(this),
2453
+ this.state.modalEditOfAccess &&
2454
+ this.state.modalEditOfAccessObjData &&
2455
+ this.props.modalEditOfAccessControl &&
2456
+ this.props.modalEditOfAccessControl(this, this.state.modalEditOfAccessObjData)));
2457
+ }
2458
+ }
2459
+ // The width class is measured from the container (see `ObjectBrowserClass.width`), so this component
2460
+ // does not need the `withWidth` HOC (which measures the window) anymore.
2461
+ export const ObjectBrowser = ObjectBrowserClass;
2462
+ //# sourceMappingURL=ObjectBrowserClass.js.map