@iobroker/gui-components 10.0.2 → 10.0.4
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.
- package/README.md +9 -1
- package/build/Components/FileBrowser.js +18 -13
- package/build/Components/FileBrowser.js.map +1 -1
- package/build/Components/{ObjectBrowser.d.ts → ObjectBrowser/ObjectBrowserClass.d.ts} +142 -77
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js +2462 -0
- package/build/Components/ObjectBrowser/ObjectBrowserClass.js.map +1 -0
- package/build/Components/ObjectBrowser/constants.d.ts +50 -0
- package/build/Components/ObjectBrowser/constants.js +138 -0
- package/build/Components/ObjectBrowser/constants.js.map +1 -0
- package/build/Components/ObjectBrowser/contextMenu.d.ts +13 -0
- package/build/Components/ObjectBrowser/contextMenu.js +354 -0
- package/build/Components/ObjectBrowser/contextMenu.js.map +1 -0
- package/build/Components/ObjectBrowser/dialogs.d.ts +31 -0
- package/build/Components/ObjectBrowser/dialogs.js +421 -0
- package/build/Components/ObjectBrowser/dialogs.js.map +1 -0
- package/build/Components/ObjectBrowser/index.d.ts +20 -0
- package/build/Components/ObjectBrowser/index.js +20 -0
- package/build/Components/ObjectBrowser/index.js.map +1 -0
- package/build/Components/ObjectBrowser/renderLeaf.d.ts +41 -0
- package/build/Components/ObjectBrowser/renderLeaf.js +1077 -0
- package/build/Components/ObjectBrowser/renderLeaf.js.map +1 -0
- package/build/Components/ObjectBrowser/styles.d.ts +7 -0
- package/build/Components/ObjectBrowser/styles.js +662 -0
- package/build/Components/ObjectBrowser/styles.js.map +1 -0
- package/build/Components/ObjectBrowser/toolbar.d.ts +26 -0
- package/build/Components/ObjectBrowser/toolbar.js +327 -0
- package/build/Components/ObjectBrowser/toolbar.js.map +1 -0
- package/build/Components/{objectBrowser.types.d.ts → ObjectBrowser/types.d.ts} +26 -19
- package/build/Components/{objectBrowserUtils.d.ts → ObjectBrowser/utils.d.ts} +22 -2
- package/build/Components/{objectBrowserUtils.js → ObjectBrowser/utils.js} +46 -2
- package/build/Components/ObjectBrowser/utils.js.map +1 -0
- package/build/Components/TreeTable.d.ts +2 -2
- package/build/Components/TreeTable.js.map +1 -1
- package/build/Dialogs/SelectID.d.ts +4 -4
- package/build/Dialogs/SelectID.js +2 -2
- package/build/Dialogs/SelectID.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/build/Components/ObjectBrowser.js +0 -5162
- package/build/Components/ObjectBrowser.js.map +0 -1
- 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
|