@iobroker/gui-components 8.0.2 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +834 -543
  3. package/build/AdminConnection.js.map +1 -1
  4. package/build/Components/404.js +13 -13
  5. package/build/Components/404.js.map +1 -1
  6. package/build/Components/ColorPicker.js.map +1 -1
  7. package/build/Components/ComplexCron.js +2 -2
  8. package/build/Components/ComplexCron.js.map +1 -1
  9. package/build/Components/CopyToClipboard.js.map +1 -1
  10. package/build/Components/CustomModal.js +1 -1
  11. package/build/Components/CustomModal.js.map +1 -1
  12. package/build/Components/DeviceType/DeviceTypeIcon.js +1 -0
  13. package/build/Components/DeviceType/DeviceTypeIcon.js.map +1 -1
  14. package/build/Components/DeviceType/DeviceTypeSelector.js +2 -2
  15. package/build/Components/DeviceType/DeviceTypeSelector.js.map +1 -1
  16. package/build/Components/DeviceType/i18n/de.json +1 -0
  17. package/build/Components/DeviceType/i18n/en.json +1 -0
  18. package/build/Components/DeviceType/i18n/fr.json +1 -0
  19. package/build/Components/DeviceType/i18n/it.json +1 -0
  20. package/build/Components/DeviceType/i18n/nl.json +1 -0
  21. package/build/Components/DeviceType/i18n/pl.json +1 -0
  22. package/build/Components/DeviceType/i18n/pt.json +1 -0
  23. package/build/Components/DeviceType/i18n/ru.json +1 -0
  24. package/build/Components/DeviceType/i18n/uk.json +1 -0
  25. package/build/Components/DeviceType/i18n/zh-cn.json +1 -0
  26. package/build/Components/FileBrowser.d.ts +39 -4
  27. package/build/Components/FileBrowser.js +104 -11
  28. package/build/Components/FileBrowser.js.map +1 -1
  29. package/build/Components/FileViewer.js +2 -4
  30. package/build/Components/FileViewer.js.map +1 -1
  31. package/build/Components/Icon.d.ts +1 -3
  32. package/build/Components/Icon.js +11 -11
  33. package/build/Components/Icon.js.map +1 -1
  34. package/build/Components/Image.js +8 -4
  35. package/build/Components/Image.js.map +1 -1
  36. package/build/Components/InfoBox.js +1 -1
  37. package/build/Components/InfoBox.js.map +1 -1
  38. package/build/Components/IobUri.js +5 -5
  39. package/build/Components/IobUri.js.map +1 -1
  40. package/build/Components/Loader.d.ts +2 -3
  41. package/build/Components/Loader.js +25 -257
  42. package/build/Components/Loader.js.map +1 -1
  43. package/build/Components/Loaders/Community.d.ts +31 -0
  44. package/build/Components/Loaders/Community.js +265 -0
  45. package/build/Components/Loaders/Community.js.map +1 -0
  46. package/build/Components/Loaders/HA.d.ts +17 -0
  47. package/build/Components/Loaders/HA.js +99 -0
  48. package/build/Components/Loaders/HA.js.map +1 -0
  49. package/build/Components/Loaders/MV.js.map +1 -1
  50. package/build/Components/Loaders/NW.d.ts +17 -0
  51. package/build/Components/Loaders/NW.js +70 -0
  52. package/build/Components/Loaders/NW.js.map +1 -0
  53. package/build/Components/Loaders/PT.js +103 -103
  54. package/build/Components/Loaders/PT.js.map +1 -1
  55. package/build/Components/Loaders/Vendor.js +7 -7
  56. package/build/Components/Loaders/Vendor.js.map +1 -1
  57. package/build/Components/MDUtils.js.map +1 -1
  58. package/build/Components/ObjectBrowser.d.ts +25 -406
  59. package/build/Components/ObjectBrowser.js +460 -1543
  60. package/build/Components/ObjectBrowser.js.map +1 -1
  61. package/build/Components/Router.js.map +1 -1
  62. package/build/Components/SaveCloseButtons.js.map +1 -1
  63. package/build/Components/Schedule.d.ts +27 -1
  64. package/build/Components/Schedule.js +60 -50
  65. package/build/Components/Schedule.js.map +1 -1
  66. package/build/Components/SelectWithIcon.js +1 -1
  67. package/build/Components/SelectWithIcon.js.map +1 -1
  68. package/build/Components/SimpleCron/cronText.js.map +1 -1
  69. package/build/Components/SimpleCron/index.js +3 -3
  70. package/build/Components/SimpleCron/index.js.map +1 -1
  71. package/build/Components/TabContainer.js +7 -2
  72. package/build/Components/TabContainer.js.map +1 -1
  73. package/build/Components/TabContent.d.ts +1 -1
  74. package/build/Components/TabContent.js +5 -5
  75. package/build/Components/TabContent.js.map +1 -1
  76. package/build/Components/TabHeader.js +2 -2
  77. package/build/Components/TabHeader.js.map +1 -1
  78. package/build/Components/TableResize.js.map +1 -1
  79. package/build/Components/TextWithIcon.js.map +1 -1
  80. package/build/Components/TreeTable.d.ts +1 -0
  81. package/build/Components/TreeTable.js +16 -6
  82. package/build/Components/TreeTable.js.map +1 -1
  83. package/build/Components/UploadImage.js +305 -305
  84. package/build/Components/UploadImage.js.map +1 -1
  85. package/build/Components/Utils.d.ts +17 -3
  86. package/build/Components/Utils.js +167 -9
  87. package/build/Components/Utils.js.map +1 -1
  88. package/build/Components/objectBrowser.types.d.ts +496 -0
  89. package/build/Components/objectBrowserUtils.d.ts +170 -0
  90. package/build/Components/objectBrowserUtils.js +1375 -0
  91. package/build/Components/objectBrowserUtils.js.map +1 -0
  92. package/build/Connection.js.map +1 -1
  93. package/build/Dialogs/ComplexCron.js.map +1 -1
  94. package/build/Dialogs/Confirm.js.map +1 -1
  95. package/build/Dialogs/Cron.js.map +1 -1
  96. package/build/Dialogs/Error.js.map +1 -1
  97. package/build/Dialogs/Message.js.map +1 -1
  98. package/build/Dialogs/SelectFile.d.ts +1 -1
  99. package/build/Dialogs/SelectFile.js +1 -1
  100. package/build/Dialogs/SelectFile.js.map +1 -1
  101. package/build/Dialogs/SelectID.d.ts +1 -1
  102. package/build/Dialogs/SelectID.js +1 -1
  103. package/build/Dialogs/SelectID.js.map +1 -1
  104. package/build/Dialogs/SimpleCron.js.map +1 -1
  105. package/build/Dialogs/TextInput.js +1 -1
  106. package/build/Dialogs/TextInput.js.map +1 -1
  107. package/build/GenericApp.d.ts +6 -4
  108. package/build/GenericApp.js +22 -16
  109. package/build/GenericApp.js.map +1 -1
  110. package/build/Prompt.js +7 -7
  111. package/build/Prompt.js.map +1 -1
  112. package/build/Theme.js +57 -11
  113. package/build/Theme.js.map +1 -1
  114. package/build/assets/devices/parseNames.js +1 -0
  115. package/build/assets/devices/parseNames.js.map +1 -1
  116. package/build/assets/rooms/parseNames.js +1 -0
  117. package/build/assets/rooms/parseNames.js.map +1 -1
  118. package/build/dictionary.js.map +1 -1
  119. package/build/globals.d.ts +2 -0
  120. package/build/i18n/de.json +5 -66
  121. package/build/i18n/en.json +5 -66
  122. package/build/i18n/es.json +5 -66
  123. package/build/i18n/fr.json +5 -66
  124. package/build/i18n/it.json +5 -61
  125. package/build/i18n/nl.json +5 -61
  126. package/build/i18n/pl.json +5 -61
  127. package/build/i18n/pt.json +5 -61
  128. package/build/i18n/ru.json +5 -61
  129. package/build/i18n/uk.json +5 -61
  130. package/build/i18n/zh-cn.json +5 -61
  131. package/build/i18n.js +12 -15
  132. package/build/i18n.js.map +1 -1
  133. package/build/index.css +56 -56
  134. package/build/index.d.ts +9 -4
  135. package/build/index.js +5 -2
  136. package/build/index.js.map +1 -1
  137. package/build/modulefederation.admin.config.d.ts +1 -0
  138. package/build/modulefederation.admin.config.js +1 -1
  139. package/build/modulefederation.admin.config.js.map +1 -1
  140. package/build/types.d.ts +4 -84
  141. package/i18n/de.json +5 -66
  142. package/i18n/en.json +5 -66
  143. package/i18n/es.json +5 -66
  144. package/i18n/fr.json +5 -66
  145. package/i18n/it.json +5 -61
  146. package/i18n/nl.json +5 -61
  147. package/i18n/pl.json +5 -61
  148. package/i18n/pt.json +5 -61
  149. package/i18n/ru.json +5 -61
  150. package/i18n/uk.json +5 -61
  151. package/i18n/zh-cn.json +5 -61
  152. package/index.css +56 -56
  153. package/modulefederation.admin.config.d.ts +1 -0
  154. package/modulefederation.admin.config.js +1 -2
  155. package/package.json +50 -35
  156. package/tasksExample.ts +93 -0
  157. package/build/LegacyConnection.d.ts +0 -938
  158. package/build/LegacyConnection.js +0 -2849
  159. package/build/LegacyConnection.js.map +0 -1
  160. package/tasksExample.js +0 -91
@@ -1,14 +1,13 @@
1
1
  /**
2
- * Copyright 2020-2025, Denis Haev <dogafox@gmail.com>
2
+ * Copyright 2020-2026, Denis Haev <dogafox@gmail.com>
3
3
  *
4
4
  * MIT License
5
5
  *
6
6
  */
7
7
  import React, { Component, createRef } from 'react';
8
- import SVG from 'react-inlinesvg';
9
- import { Badge, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fab, FormControl, FormControlLabel, Grid2, IconButton, Input, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, Select, Snackbar, Switch, TextField, Tooltip, } from '@mui/material';
8
+ import { Badge, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fab, FormControlLabel, Grid, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, Snackbar, Switch, TextField, Tooltip, } from '@mui/material';
10
9
  // Icons
11
- import { Add as AddIcon, ArrowRight as ArrowRightIcon, BedroomParent, BorderColor, Build as BuildIcon, CalendarToday as IconSchedule, Check as IconCheck, Close as IconClose, Code as IconScript, Construction, CreateNewFolder as IconFolder, Delete as IconDelete, Description as IconMeta, Edit as IconEdit, Error as IconError, FindInPage, FormatItalic as IconValueEdit, Info as IconInfo, Link as IconLink, ListAlt as IconEnum, LooksOne as LooksOneIcon, PersonOutlined as IconUser, Publish as PublishIcon, Refresh as RefreshIcon, Router as IconHost, Settings as IconConfig, SettingsApplications as IconSystem, DataObject as IconData, ShowChart as IconChart, SupervisedUserCircle as IconGroup, TextFields as TextFieldsIcon, ViewColumn as IconColumns, Wifi as IconConnection, WifiOff as IconDisconnected, DriveFileRenameOutline, } from '@mui/icons-material';
10
+ import { Add as AddIcon, ArrowRight as ArrowRightIcon, BedroomParent, BorderColor, Build as BuildIcon, CalendarToday as IconSchedule, Check as IconCheck, Close as IconClose, Code as IconScript, Construction, CreateNewFolder as IconFolder, Delete as IconDelete, Description as IconMeta, Edit as IconEdit, Error as IconError, FindInPage, FormatItalic as IconValueEdit, Link as IconLink, ListAlt as IconEnum, LooksOne as LooksOneIcon, PersonOutlined as IconUser, Publish as PublishIcon, Refresh as RefreshIcon, Router as IconHost, Settings as IconConfig, ShowChart as IconChart, SupervisedUserCircle as IconGroup, TextFields as TextFieldsIcon, ViewColumn as IconColumns, Wifi as IconConnection, WifiOff as IconDisconnected, DriveFileRenameOutline, ContentPaste, UploadFile, } from '@mui/icons-material';
12
11
  import { IconExpert } from '../icons/IconExpert';
13
12
  import { IconAdapter } from '../icons/IconAdapter';
14
13
  import { IconChannel } from '../icons/IconChannel';
@@ -24,25 +23,21 @@ import { IconClearFilter } from '../icons/IconClearFilter';
24
23
  import { Connection } from '../Connection';
25
24
  import { Icon } from './Icon';
26
25
  import { withWidth } from './withWidth';
27
- import { Utils } from './Utils'; // @iobroker/adapter-react-v5/Components/Utils
26
+ import { Utils } from './Utils'; // @iobroker/gui-components/Components/Utils
28
27
  import { TabContainer } from './TabContainer';
29
28
  import { TabContent } from './TabContent';
30
29
  import { TabHeader } from './TabHeader';
31
- const ICON_SIZE = 24;
32
- const ROW_HEIGHT = 32;
30
+ import { CustomFilterInput, CustomFilterSelect, ButtonIcon, applyFilter, binarySearch, buildTree, findEnumsForObjectAsIds, findFunctionsForObject, findNode, findRoomsForObject, formatValue, generateFile, getCustomValue, getIdFieldTooltip, getName, getObjectTooltip, getSelectIdIconFromObjects, getValueStyle, getVisibleItems, isNonExpertId, setCustomValue, prepareSparkData, COLOR_NAME_USERDATA, COLOR_NAME_ALIAS, COLOR_NAME_JAVASCRIPT, COLOR_NAME_SYSTEM, COLOR_NAME_SYSTEM_ADAPTER, ICON_SIZE, ROW_HEIGHT, styles as utilStyles, } from './objectBrowserUtils';
31
+ export { getSelectIdIconFromObjects };
33
32
  const ITEM_LEVEL = 16;
34
33
  const SMALL_BUTTON_SIZE = 20;
35
- const COLOR_NAME_USERDATA = (themeType) => (themeType === 'dark' ? '#62ff25' : '#37c400');
36
- const COLOR_NAME_ALIAS = (themeType) => (themeType === 'dark' ? '#ee56ff' : '#a204b4');
37
- const COLOR_NAME_JAVASCRIPT = (themeType) => (themeType === 'dark' ? '#fff46e' : '#b89101');
38
- const COLOR_NAME_SYSTEM = (themeType) => (themeType === 'dark' ? '#ff6d69' : '#ff6d69');
39
- const COLOR_NAME_SYSTEM_ADAPTER = (themeType) => (themeType === 'dark' ? '#5773ff' : '#5773ff');
40
34
  const COLOR_NAME_ERROR_DARK = '#ff413c';
41
35
  const COLOR_NAME_ERROR_LIGHT = '#86211f';
42
36
  const COLOR_NAME_CONNECTED_DARK = '#57ff45';
43
37
  const COLOR_NAME_CONNECTED_LIGHT = '#098c04';
44
38
  const COLOR_NAME_DISCONNECTED_DARK = '#f3ad11';
45
39
  const COLOR_NAME_DISCONNECTED_LIGHT = '#6c5008';
40
+ const DEFAULT_DATE_FORMAT = 'YYYY.MM.DD';
46
41
  const styles = {
47
42
  toolbar: {
48
43
  minHeight: 38, // Theme.toolbar.height,
@@ -232,18 +227,6 @@ const styles = {
232
227
  height: SMALL_BUTTON_SIZE,
233
228
  },
234
229
  cellIdIconOwn: {},
235
- cellIdTooltip: {
236
- fontSize: 14,
237
- },
238
- cellIdTooltipLink: {
239
- color: '#7ec2fd',
240
- '&:hover': {
241
- color: '#7ec2fd',
242
- },
243
- '&:visited': {
244
- color: '#7ec2fd',
245
- },
246
- },
247
230
  cellCopyButton: {
248
231
  width: SMALL_BUTTON_SIZE,
249
232
  height: SMALL_BUTTON_SIZE,
@@ -508,9 +491,6 @@ const styles = {
508
491
  height: 24,
509
492
  marginRight: 4,
510
493
  },
511
- selectNone: {
512
- opacity: 0.5,
513
- },
514
494
  itemSelected: (theme) => ({
515
495
  background: `${theme.palette.primary.main} !important`,
516
496
  color: `${Utils.invertColor(theme.palette.primary.main, true)} !important`,
@@ -527,25 +507,6 @@ const styles = {
527
507
  // paddingLeft: 5,
528
508
  fontSize: 16,
529
509
  },
530
- headerCellInput: {
531
- width: 'calc(100% - 5px)',
532
- height: ROW_HEIGHT,
533
- pt: 0,
534
- '& .itemIcon': {
535
- verticalAlign: 'middle',
536
- width: ICON_SIZE,
537
- height: ICON_SIZE,
538
- display: 'inline-block',
539
- },
540
- },
541
- headerCellSelectItem: {
542
- '& .itemIcon': {
543
- width: ICON_SIZE,
544
- height: ICON_SIZE,
545
- mr: '5px',
546
- display: 'inline-block',
547
- },
548
- },
549
510
  visibleButtons: {
550
511
  color: '#2196f3',
551
512
  opacity: 0.7,
@@ -615,13 +576,6 @@ const styles = {
615
576
  nonDraggable: {
616
577
  cursor: 'no-drop',
617
578
  },
618
- selectClearButton: {
619
- position: 'absolute',
620
- top: 0,
621
- right: 0,
622
- borderRadius: 5,
623
- backgroundColor: 'background.default',
624
- },
625
579
  iconDeviceConnected: (theme) => ({
626
580
  color: theme.palette.mode === 'dark' ? COLOR_NAME_CONNECTED_DARK : COLOR_NAME_CONNECTED_LIGHT,
627
581
  opacity: 0.8,
@@ -689,1077 +643,27 @@ const styles = {
689
643
  color: theme.palette.mode === 'dark' ? '#42eaff' : '#01bbc2',
690
644
  }),
691
645
  contextMenuACL: (theme) => ({
692
- color: theme.palette.mode === 'dark' ? '#e079ff' : '#500070',
693
- }),
694
- contextMenuRoom: (theme) => ({
695
- color: theme.palette.mode === 'dark' ? '#ff9a33' : '#642a00',
696
- }),
697
- contextMenuRole: (theme) => ({
698
- color: theme.palette.mode === 'dark' ? '#ffdb43' : '#562d00',
699
- }),
700
- contextMenuDelete: (theme) => ({
701
- color: theme.palette.mode === 'dark' ? '#ff4f4f' : '#cf0000',
702
- }),
703
- contextMenuKeys: {
704
- marginLeft: 8,
705
- opacity: 0.7,
706
- fontSize: 'smaller',
707
- },
708
- contextMenuWithSubMenu: {
709
- display: 'flex',
710
- },
711
- };
712
- function ButtonIcon(props) {
713
- return (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 436 436", style: props?.style, width: "24", height: "24", className: "admin-button" },
714
- React.createElement("g", { fill: "currentColor" },
715
- React.createElement("path", { d: "m195.23077,24.30769c-36,3 -67,12 -96,26c-49,24 -82,61 -93,104l-3,11l-1,50c0,46 0,49 2,59l5,20c21,58 84,103 165,116c16,3 53,4 70,2c60,-6 111,-28 147,-64c21,-21 36,-49 40,-74a866,866 0 0 0 1,-104c-3,-18 -6,-28 -13,-43c-26,-52 -87,-90 -162,-101c-16,-2 -48,-3 -63,-2l1,0zm60,23c36,5 70,18 95,35c31,20 51,47 59,77c2,7 2,11 2,25c1,15 0,18 -2,26c-19,69 -104,117 -200,114c-47,-2 -90,-15 -124,-38c-31,-20 -51,-47 -59,-77c-3,-11 -4,-32 -2,-43c8,-42 41,-78 91,-101a260,260 0 0 1 140,-19l0,1zm-221,222c21,26 57,49 95,62c81,27 174,14 239,-32c14,-10 31,-27 41,-41c2,-2 2,-2 2,7c-1,23 -16,50 -38,72c-78,74 -233,74 -311,-1a121,121 0 0 1 -39,-76l0,-6l3,4l8,11z" }),
716
- React.createElement("path", { d: "m201.23077,47.30769c-40,3 -79,19 -104,44c-55,55 -38,133 37,171c52,26 122,24 172,-5c30,-17 51,-42 58,-71c3,-11 3,-34 0,-45c-6,-23 -21,-44 -40,-60l-27,-16a184,184 0 0 0 -96,-18zm30,21c56,5 100,35 112,75c4,11 4,30 0,41c-8,25 -26,45 -54,59a166,166 0 0 1 -160,-8a98,98 0 0 1 -41,-53c-5,-18 -2,-39 8,-57c23,-39 79,-62 135,-57z" }))));
717
- }
718
- /** Converts ioB pattern into regex */
719
- export function pattern2RegEx(pattern) {
720
- pattern = (pattern || '').toString();
721
- const startsWithWildcard = pattern[0] === '*';
722
- const endsWithWildcard = pattern[pattern.length - 1] === '*';
723
- pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*');
724
- return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$');
725
- }
726
- /**
727
- * Function that walks through all keys of an object or array and applies a function to each key.
728
- */
729
- function walkThroughArray(object, iteratee) {
730
- const copiedObject = [];
731
- for (let index = 0; index < object.length; index++) {
732
- iteratee(copiedObject, object[index], index);
733
- }
734
- return copiedObject;
735
- }
736
- /**
737
- * Function that walks through all keys of an object or array and applies a function to each key.
738
- */
739
- function walkThroughObject(object, iteratee) {
740
- const copiedObject = {};
741
- for (const key in object) {
742
- if (Object.prototype.hasOwnProperty.call(object, key)) {
743
- iteratee(copiedObject, object[key], key);
744
- }
745
- }
746
- return copiedObject;
747
- }
748
- /**
749
- * Function to reduce an object primarily by a given list of keys
750
- */
751
- function filterObject(
752
- /** The objects which should be filtered */
753
- obj,
754
- /** The keys which should be excluded */
755
- filterKeys,
756
- /** Whether translations should be reduced to only the english value */
757
- excludeTranslations) {
758
- if (Array.isArray(obj)) {
759
- return walkThroughArray(obj, (result, value, key) => {
760
- if (value === undefined || value === null) {
761
- return;
762
- }
763
- // if the key is an object, run it through the inner function - omitFromObject
764
- const isObject = typeof value === 'object';
765
- if (excludeTranslations && isObject) {
766
- if (typeof value.en === 'string' && typeof value.de === 'string') {
767
- result[key] = value.en;
768
- return;
769
- }
770
- }
771
- result[key] = isObject ? filterObject(value, filterKeys, excludeTranslations) : value;
772
- });
773
- }
774
- return walkThroughObject(obj, (result, value, key) => {
775
- if (value === undefined || value === null) {
776
- return;
777
- }
778
- if (filterKeys.includes(key)) {
779
- return;
780
- }
781
- // if the key is an object, run it through the inner function - omitFromObject
782
- const isObject = typeof value === 'object';
783
- if (excludeTranslations && isObject) {
784
- if (typeof value.en === 'string' && typeof value.de === 'string') {
785
- result[key] = value.en;
786
- return;
787
- }
788
- }
789
- result[key] = isObject ? filterObject(value, filterKeys, excludeTranslations) : value;
790
- });
791
- }
792
- export function filterRoles(roleArray, type, defaultRoles) {
793
- const bigRoleArray = [];
794
- roleArray.forEach(role => (role.type === 'mixed' || role.type) === type &&
795
- !bigRoleArray.includes(role.role) &&
796
- bigRoleArray.push(role.role));
797
- defaultRoles.forEach(role => (role.type === 'mixed' || role.type) === type &&
798
- !bigRoleArray.includes(role.role) &&
799
- bigRoleArray.push(role.role));
800
- bigRoleArray.sort();
801
- return bigRoleArray;
802
- }
803
- /**
804
- * Function to generate a json-file for an object and trigger download it
805
- */
806
- function generateFile(
807
- /** The desired filename */
808
- fileName,
809
- /** The objects which should be downloaded */
810
- obj,
811
- /** Options to filter/reduce the output */
812
- options) {
813
- const el = document.createElement('a');
814
- const filterKeys = [];
815
- if (options.excludeSystemRepositories) {
816
- filterKeys.push('system.repositories');
817
- }
818
- const filteredObject = filterKeys.length > 0 || options.excludeTranslations
819
- ? filterObject(obj, filterKeys, options.excludeTranslations)
820
- : obj;
821
- const data = options.beautify ? JSON.stringify(filteredObject, null, 2) : JSON.stringify(filteredObject);
822
- el.setAttribute('href', `data:application/json;charset=utf-8,${encodeURIComponent(data)}`);
823
- el.setAttribute('download', fileName);
824
- el.style.display = 'none';
825
- document.body.appendChild(el);
826
- el.click();
827
- document.body.removeChild(el);
828
- }
829
- // d=data, t=target, s=start, e=end, m=middle
830
- function binarySearch(list, find, _start, _end) {
831
- _start ||= 0;
832
- if (_end === undefined) {
833
- _end = list.length - 1;
834
- if (!_end) {
835
- return list[0] === find;
836
- }
837
- }
838
- const middle = Math.floor((_start + _end) / 2);
839
- if (find === list[middle]) {
840
- return true;
841
- }
842
- if (_end - 1 === _start) {
843
- return list[_start] === find || list[_end] === find;
844
- }
845
- if (find > list[middle]) {
846
- return binarySearch(list, find, middle, _end);
847
- }
848
- if (find < list[middle]) {
849
- return binarySearch(list, find, _start, middle);
850
- }
851
- return false;
852
- }
853
- function getName(name, lang) {
854
- if (typeof name === 'object') {
855
- if (!name) {
856
- return '';
857
- }
858
- return (name[lang] || name.en || '').toString();
859
- }
860
- return name ? name.toString() : '';
861
- }
862
- export function getSelectIdIconFromObjects(objects, id, lang, imagePrefix) {
863
- // `admin` has prefix '.' and `web` has '../..'
864
- imagePrefix ||= '.'; // http://localhost:8081';
865
- let src = '';
866
- const _id_ = `system.adapter.${id}`;
867
- const aIcon = id && objects[_id_]?.common?.icon;
868
- if (aIcon) {
869
- // if not BASE64
870
- if (!aIcon.startsWith('data:image/')) {
871
- if (aIcon.includes('.')) {
872
- const name = objects[_id_].common.name;
873
- if (typeof name === 'object') {
874
- src = `${imagePrefix}/adapter/${name[lang] || name.en}/${aIcon}`;
875
- }
876
- else {
877
- src = `${imagePrefix}/adapter/${name}/${aIcon}`;
878
- }
879
- }
880
- else if (aIcon && aIcon.length < 3) {
881
- return aIcon; // utf-8
882
- }
883
- else {
884
- return null; // '<i class="material-icons iob-list-icon">' + objects[_id_].common.icon + '</i>';
885
- }
886
- }
887
- else if (aIcon.startsWith('data:image/svg')) {
888
- src = (React.createElement(SVG, { className: "iconOwn", src: aIcon, width: 28, height: 28 }));
889
- }
890
- else {
891
- src = aIcon;
892
- }
893
- }
894
- else {
895
- const common = objects[id] && objects[id].common;
896
- if (common) {
897
- const cIcon = common.icon;
898
- if (cIcon) {
899
- if (!cIcon.startsWith('data:image/')) {
900
- if (cIcon.includes('.')) {
901
- let instance;
902
- if (objects[id].type === 'instance' || objects[id].type === 'adapter') {
903
- if (typeof common.name === 'object') {
904
- src = `${imagePrefix}/adapter/${common.name[lang] || common.name.en}/${cIcon}`;
905
- }
906
- else {
907
- src = `${imagePrefix}/adapter/${common.name}/${cIcon}`;
908
- }
909
- }
910
- else if (id && id.startsWith('system.adapter.')) {
911
- instance = id.split('.', 3);
912
- if (cIcon[0] === '/') {
913
- instance[2] += cIcon;
914
- }
915
- else {
916
- instance[2] += `/${cIcon}`;
917
- }
918
- src = `${imagePrefix}/adapter/${instance[2]}`;
919
- }
920
- else {
921
- instance = id.split('.', 2);
922
- if (cIcon[0] === '/') {
923
- instance[0] += cIcon;
924
- }
925
- else {
926
- instance[0] += `/${cIcon}`;
927
- }
928
- src = `${imagePrefix}/adapter/${instance[0]}`;
929
- }
930
- }
931
- else if (aIcon && aIcon.length < 3) {
932
- return aIcon; // utf-8
933
- }
934
- else {
935
- return null;
936
- }
937
- }
938
- else if (cIcon.startsWith('data:image/svg')) {
939
- // if base 64 image
940
- src = (React.createElement(SVG, { className: "iconOwn", src: cIcon, width: 28, height: 28 }));
941
- }
942
- else {
943
- src = cIcon;
944
- }
945
- }
946
- }
947
- }
948
- return src || null;
949
- }
950
- function applyFilter(item, filters, lang, objects, context, counter, customFilter, selectedTypes, _depth) {
951
- _depth ||= 0;
952
- let filteredOut = false;
953
- if (!context) {
954
- context = {};
955
- if (filters.id) {
956
- const id = filters.id.toLowerCase();
957
- if (id.includes('*')) {
958
- context.idRx = new RegExp(pattern2RegEx(filters.id), 'i');
959
- }
960
- else {
961
- context.id = id;
962
- }
963
- }
964
- if (filters.name) {
965
- const name = filters.name.toLowerCase();
966
- if (name.includes('*')) {
967
- context.nameRx = new RegExp(pattern2RegEx(name), 'i');
968
- }
969
- else {
970
- context.name = name;
971
- }
972
- }
973
- if (filters.type?.length) {
974
- context.type = filters.type.map(f => f.toLowerCase());
975
- }
976
- if (filters.custom?.length) {
977
- context.custom = filters.custom.map(c => c.toLowerCase());
978
- }
979
- if (filters.role?.length) {
980
- context.role = filters.role.map(r => r.toLowerCase());
981
- }
982
- if (filters.room?.length) {
983
- context.room = [];
984
- filters.room.forEach(room => {
985
- context.room = context.room.concat(objects[room]?.common?.members || []);
986
- });
987
- }
988
- if (filters.func?.length) {
989
- context.func = [];
990
- filters.func.forEach(func => {
991
- context.func = context.func.concat(objects[func]?.common?.members || []);
992
- });
993
- }
994
- }
995
- const data = item.data;
996
- if (data?.id) {
997
- const common = data.obj?.common;
998
- if (customFilter) {
999
- if (customFilter.type) {
1000
- if (typeof customFilter.type === 'string') {
1001
- if (!data.obj || customFilter.type !== data.obj.type) {
1002
- filteredOut = true;
1003
- }
1004
- }
1005
- else if (Array.isArray(customFilter.type)) {
1006
- if (!data.obj || !customFilter.type.includes(data.obj.type)) {
1007
- filteredOut = true;
1008
- }
1009
- }
1010
- }
1011
- if (!filteredOut && customFilter.common?.type) {
1012
- if (!common?.type) {
1013
- filteredOut = true;
1014
- }
1015
- else if (typeof customFilter.common.type === 'string') {
1016
- if (customFilter.common.type !== common.type) {
1017
- filteredOut = true;
1018
- }
1019
- }
1020
- else if (Array.isArray(customFilter.common.type)) {
1021
- if (!customFilter.common.type.includes(common.type)) {
1022
- filteredOut = true;
1023
- }
1024
- }
1025
- }
1026
- if (!filteredOut && customFilter.common?.role) {
1027
- if (!common?.role) {
1028
- filteredOut = true;
1029
- }
1030
- else if (typeof customFilter.common.role === 'string') {
1031
- if (common.role.startsWith(customFilter.common.role)) {
1032
- filteredOut = true;
1033
- }
1034
- }
1035
- else if (Array.isArray(customFilter.common.role)) {
1036
- if (!customFilter.common.role.find(role => common.role.startsWith(role))) {
1037
- filteredOut = true;
1038
- }
1039
- }
1040
- }
1041
- if (!filteredOut && customFilter.common?.custom === '_' && common?.custom) {
1042
- filteredOut = true;
1043
- }
1044
- else if (!filteredOut && customFilter.common?.custom && customFilter.common?.custom !== '_') {
1045
- const filterOfCustom = customFilter.common.custom;
1046
- if (!common?.custom) {
1047
- filteredOut = true;
1048
- }
1049
- else if (filterOfCustom === '_dataSources') {
1050
- // TODO: make it configurable
1051
- if (!Object.keys(common.custom).find(id => id.startsWith('history.') || id.startsWith('sql.') || id.startsWith('influxdb.'))) {
1052
- filteredOut = true;
1053
- }
1054
- }
1055
- else if (Array.isArray(filterOfCustom)) {
1056
- // here are ['influxdb.', 'telegram.']
1057
- const customs = Object.keys(common.custom); // here are ['influxdb.0', 'telegram.2']
1058
- if (filterOfCustom.find(cst => customs.find(id => id.startsWith(cst)))) {
1059
- filteredOut = true;
1060
- }
1061
- }
1062
- else if (filterOfCustom !== true &&
1063
- !Object.keys(common.custom).find(id => id.startsWith(filterOfCustom))) {
1064
- filteredOut = true;
1065
- }
1066
- }
1067
- }
1068
- if (!filteredOut && !filters.expertMode) {
1069
- filteredOut =
1070
- data.id === 'system' ||
1071
- data.id === 'enum' ||
1072
- // (data.obj && data.obj.type === 'meta') ||
1073
- data.id.startsWith('system.') ||
1074
- data.id.startsWith('enum.') ||
1075
- data.id.startsWith('_design/') ||
1076
- data.id.endsWith('.admin') ||
1077
- !!common?.expert;
1078
- }
1079
- if (!filteredOut && context.id) {
1080
- if (data.fID === undefined) {
1081
- data.fID = data.id.toLowerCase();
1082
- }
1083
- filteredOut = !data.fID.includes(context.id);
1084
- }
1085
- if (!filteredOut && context.idRx) {
1086
- filteredOut = !context.idRx.test(data.id);
1087
- }
1088
- if (!filteredOut && context.name) {
1089
- if (common) {
1090
- if (data.fName === undefined) {
1091
- data.fName = getName(common.name, lang) || '';
1092
- data.fName = data.fName.toLowerCase();
1093
- }
1094
- filteredOut = !data.fName.includes(context.name);
1095
- }
1096
- else {
1097
- filteredOut = true;
1098
- }
1099
- }
1100
- if (!filteredOut && context.nameRx) {
1101
- if (common) {
1102
- if (data.fName === undefined) {
1103
- data.fName = getName(common.name, lang) || '';
1104
- data.fName = data.fName.toLowerCase();
1105
- }
1106
- filteredOut = !context.nameRx.test(data.fName);
1107
- }
1108
- }
1109
- if (!filteredOut && filters.role?.length && common) {
1110
- filteredOut = !(typeof common.role === 'string' && context.role.find(role => common.role.startsWith(role)));
1111
- }
1112
- if (!filteredOut && context.room?.length) {
1113
- filteredOut = !context.room.find(id => id === data.id || data.id.startsWith(`${id}.`));
1114
- }
1115
- if (!filteredOut && context.func?.length) {
1116
- filteredOut = !context.func.find(id => id === data.id || data.id.startsWith(`${id}.`));
1117
- }
1118
- if (!filteredOut && context.type?.length) {
1119
- filteredOut = !(data.obj?.type && context.type.includes(data.obj.type));
1120
- }
1121
- if (!filteredOut && selectedTypes) {
1122
- filteredOut = !(data.obj?.type && selectedTypes.includes(data.obj.type));
1123
- }
1124
- if (!filteredOut && context.custom?.length) {
1125
- if (common) {
1126
- if (context.custom.includes('_')) {
1127
- filteredOut = !!common.custom;
1128
- }
1129
- else if (common.custom) {
1130
- filteredOut = !context.custom.find(custom => common.custom[custom]);
1131
- }
1132
- }
1133
- else {
1134
- filteredOut = true;
1135
- }
1136
- }
1137
- }
1138
- data.visible = !filteredOut;
1139
- data.hasVisibleChildren = false;
1140
- if (item.children && _depth < 20) {
1141
- item.children.forEach(_item => {
1142
- const visible = applyFilter(_item, filters, lang, objects, context, counter, customFilter, selectedTypes, _depth + 1);
1143
- if (visible) {
1144
- data.hasVisibleChildren = true;
1145
- }
1146
- });
1147
- }
1148
- // const visible = data.visible || data.hasVisibleChildren;
1149
- data.sumVisibility = data.visible || data.hasVisibleChildren; // || data.hasVisibleParent;
1150
- if (counter && data.sumVisibility) {
1151
- counter.count++;
1152
- }
1153
- // show all children of visible object with opacity 0.5
1154
- if (data.id && data.sumVisibility && item.children) {
1155
- item.children.forEach(_item => (_item.data.hasVisibleParent = true));
1156
- }
1157
- return data.visible || data.hasVisibleChildren;
1158
- }
1159
- function getVisibleItems(item, type, objects, _result) {
1160
- _result ||= [];
1161
- const data = item.data;
1162
- if (data.sumVisibility) {
1163
- if (data.id && objects[data.id] && (!type || objects[data.id].type === type)) {
1164
- _result.push(data.id);
1165
- }
1166
- item.children?.forEach(_item => getVisibleItems(_item, type, objects, _result));
1167
- }
1168
- return _result;
1169
- }
1170
- function getSystemIcon(objects, id, level, themeType, lang, imagePrefix) {
1171
- let icon;
1172
- // system or design has special icons
1173
- if (id === 'alias' || id === 'alias.0') {
1174
- icon = (React.createElement(IconLink, { className: "iconOwn", style: { color: COLOR_NAME_ALIAS(themeType) } }));
1175
- }
1176
- else if (id === '0_userdata' || id === '0_userdata.0') {
1177
- icon = (React.createElement(IconData, { className: "iconOwn", style: { color: COLOR_NAME_USERDATA(themeType) } }));
1178
- }
1179
- else if (id.startsWith('_design/') || id === 'system') {
1180
- icon = (React.createElement(IconSystem, { className: "iconOwn", style: { color: COLOR_NAME_SYSTEM(themeType) } }));
1181
- }
1182
- else if (id === 'system.adapter') {
1183
- icon = (React.createElement(IconSystem, { className: "iconOwn", style: { color: COLOR_NAME_SYSTEM_ADAPTER(themeType) } }));
1184
- }
1185
- else if (id === 'system.group') {
1186
- icon = React.createElement(IconGroup, { className: "iconOwn" });
1187
- }
1188
- else if (id === 'system.user') {
1189
- icon = React.createElement(IconUser, { className: "iconOwn" });
1190
- }
1191
- else if (id === 'system.host') {
1192
- icon = React.createElement(IconHost, { className: "iconOwn" });
1193
- }
1194
- else if (id.endsWith('.connection') || id.endsWith('.connected')) {
1195
- icon = React.createElement(IconConnection, { className: "iconOwn" });
1196
- }
1197
- else if (id.endsWith('.info')) {
1198
- icon = React.createElement(IconInfo, { className: "iconOwn" });
1199
- }
1200
- else if (objects[id] && objects[id].type === 'meta') {
1201
- icon = React.createElement(IconMeta, { className: "iconOwn" });
1202
- }
1203
- else if (level < 2) {
1204
- // detect "cloud.0"
1205
- if (objects[`system.adapter.${id}`]) {
1206
- icon = getSelectIdIconFromObjects(objects, `system.adapter.${id}`, lang, imagePrefix);
1207
- }
1208
- }
1209
- return icon || null;
1210
- }
1211
- function getObjectTooltip(data, lang) {
1212
- if (data?.obj?.common?.desc) {
1213
- return getName(data.obj.common.desc, lang) || null;
1214
- }
1215
- return null;
1216
- }
1217
- function getIdFieldTooltip(data, lang) {
1218
- const tooltip = getObjectTooltip(data, lang);
1219
- if (tooltip?.startsWith('http')) {
1220
- return (React.createElement(Box, { component: "a", sx: styles.cellIdTooltipLink, href: tooltip, target: "_blank", rel: "noreferrer" }, tooltip));
1221
- }
1222
- return React.createElement("span", { style: styles.cellIdTooltip }, tooltip || data.id || '');
1223
- }
1224
- function buildTree(objects, options) {
1225
- const imagePrefix = options.imagePrefix || '.';
1226
- let ids = Object.keys(objects);
1227
- ids.sort((a, b) => {
1228
- if (a === b) {
1229
- return 0;
1230
- }
1231
- a = a.replace(/\./g, '!!!');
1232
- b = b.replace(/\./g, '!!!');
1233
- if (a > b) {
1234
- return 1;
1235
- }
1236
- return -1;
1237
- });
1238
- if (options.root) {
1239
- ids = ids.filter(id => id === options.root || id.startsWith(`${options.root}.`));
1240
- }
1241
- // find empty nodes and create names for it
1242
- let currentPathArr = [];
1243
- let currentPath = '';
1244
- let currentPathLen = 0;
1245
- const root = {
1246
- data: {
1247
- name: '',
1248
- id: '',
1249
- },
1250
- children: [],
1251
- };
1252
- const info = {
1253
- funcEnums: [],
1254
- roomEnums: [],
1255
- roles: [],
1256
- ids: [],
1257
- types: [],
1258
- objects,
1259
- customs: ['_'],
1260
- enums: [],
1261
- hasSomeCustoms: false,
1262
- aliasesMap: {},
1263
- };
1264
- let cRoot = root;
1265
- for (let i = 0; i < ids.length; i++) {
1266
- const id = ids[i];
1267
- if (!id) {
1268
- continue;
1269
- }
1270
- const obj = objects[id];
1271
- const parts = id.split('.');
1272
- if (obj.type && !info.types.includes(obj.type)) {
1273
- info.types.push(obj.type);
1274
- }
1275
- if (obj) {
1276
- const common = obj.common;
1277
- const role = common?.role;
1278
- if (role && !info.roles.find(it => it.role === role)) {
1279
- if (typeof role !== 'string') {
1280
- console.warn(`Invalid role type "${typeof role}" in "${obj._id}"`);
1281
- }
1282
- else {
1283
- info.roles.push({ role, type: common.type });
1284
- }
1285
- }
1286
- else if (id.startsWith('enum.rooms.')) {
1287
- info.roomEnums.push(id);
1288
- info.enums.push(id);
1289
- }
1290
- else if (id.startsWith('enum.functions.')) {
1291
- info.funcEnums.push(id);
1292
- info.enums.push(id);
1293
- }
1294
- else if (obj.type === 'enum') {
1295
- info.enums.push(id);
1296
- }
1297
- else if (obj.type === 'instance' && common && (common.supportCustoms || common.adminUI?.custom)) {
1298
- info.hasSomeCustoms = true;
1299
- info.customs.push(id.substring('system.adapter.'.length));
1300
- }
1301
- // Build a map of aliases
1302
- if (id.startsWith('alias.') && obj.common.alias?.id) {
1303
- if (typeof obj.common.alias.id === 'string') {
1304
- const usedId = obj.common.alias.id;
1305
- if (!info.aliasesMap[usedId]) {
1306
- info.aliasesMap[usedId] = [id];
1307
- }
1308
- else if (!info.aliasesMap[usedId].includes(id)) {
1309
- info.aliasesMap[usedId].push(id);
1310
- }
1311
- }
1312
- else {
1313
- const readId = obj.common.alias.id.read;
1314
- if (readId) {
1315
- if (!info.aliasesMap[readId]) {
1316
- info.aliasesMap[readId] = [id];
1317
- }
1318
- else if (!info.aliasesMap[readId].includes(id)) {
1319
- info.aliasesMap[readId].push(id);
1320
- }
1321
- }
1322
- const writeId = obj.common.alias.id.write;
1323
- if (writeId) {
1324
- if (!info.aliasesMap[writeId]) {
1325
- info.aliasesMap[writeId] = [id];
1326
- }
1327
- else if (!info.aliasesMap[writeId].includes(id)) {
1328
- info.aliasesMap[writeId].push(id);
1329
- }
1330
- }
1331
- }
1332
- }
1333
- }
1334
- info.ids.push(id);
1335
- let repeat;
1336
- // if next level
1337
- do {
1338
- repeat = false;
1339
- // If the current level is still OK, and we can add ID to children
1340
- if (!currentPath || id.startsWith(`${currentPath}.`)) {
1341
- // if more than one level added
1342
- if (parts.length - currentPathLen > 1) {
1343
- let curPath = currentPath;
1344
- // generate missing levels
1345
- for (let k = currentPathLen; k < parts.length - 1; k++) {
1346
- curPath += (curPath ? '.' : '') + parts[k];
1347
- // level does not exist
1348
- if (!binarySearch(info.ids, curPath)) {
1349
- const _cRoot = {
1350
- data: {
1351
- name: parts[k],
1352
- parent: cRoot,
1353
- id: curPath,
1354
- obj: objects[curPath],
1355
- level: k,
1356
- icon: getSystemIcon(objects, curPath, k, options.themeType, options.lang, imagePrefix),
1357
- generated: true,
1358
- },
1359
- };
1360
- cRoot.children ||= [];
1361
- cRoot.children.push(_cRoot);
1362
- cRoot = _cRoot;
1363
- info.ids.push(curPath); // IDs will be added by alphabet
1364
- }
1365
- else if (cRoot.children) {
1366
- cRoot = cRoot.children.find(item => item.data.name === parts[k]);
1367
- }
1368
- }
1369
- }
1370
- const _cRoot = {
1371
- data: {
1372
- name: parts[parts.length - 1],
1373
- title: getName(obj?.common?.name, options.lang),
1374
- obj,
1375
- parent: cRoot,
1376
- icon: getSelectIdIconFromObjects(objects, id, options.lang, imagePrefix) ||
1377
- getSystemIcon(objects, id, 0, options.themeType, options.lang, imagePrefix),
1378
- id,
1379
- hasCustoms: !!(obj.common?.custom && Object.keys(obj.common.custom).length),
1380
- level: parts.length - 1,
1381
- generated: false,
1382
- button: obj.type === 'state' &&
1383
- !!obj.common?.role &&
1384
- typeof obj.common.role === 'string' &&
1385
- obj.common.role.startsWith('button') &&
1386
- obj.common?.write !== false,
1387
- switch: obj.type === 'state' &&
1388
- obj.common?.type === 'boolean' &&
1389
- obj.common?.write !== false &&
1390
- obj.common?.read !== false,
1391
- url: !!obj.common?.role &&
1392
- typeof obj.common.role === 'string' &&
1393
- obj.common.role.startsWith('url'),
1394
- },
1395
- };
1396
- cRoot.children ||= [];
1397
- cRoot.children.push(_cRoot);
1398
- cRoot = _cRoot;
1399
- currentPathLen = parts.length;
1400
- currentPathArr = parts;
1401
- currentPath = id;
1402
- }
1403
- else {
1404
- let u = 0;
1405
- while (currentPathArr[u] === parts[u]) {
1406
- u++;
1407
- }
1408
- if (u > 0) {
1409
- let move = currentPathArr.length;
1410
- currentPathArr = currentPathArr.splice(0, u);
1411
- currentPathLen = u;
1412
- currentPath = currentPathArr.join('.');
1413
- while (move > u) {
1414
- if (cRoot.data.parent) {
1415
- cRoot = cRoot.data.parent;
1416
- }
1417
- else {
1418
- console.error(`Parent is null for ${id} ${currentPath} ${currentPathArr.join('.')}`);
1419
- }
1420
- move--;
1421
- }
1422
- }
1423
- else {
1424
- cRoot = root;
1425
- currentPathArr = [];
1426
- currentPath = '';
1427
- currentPathLen = 0;
1428
- }
1429
- repeat = true;
1430
- }
1431
- } while (repeat);
1432
- }
1433
- info.roomEnums.sort((a, b) => {
1434
- const aName = getName(objects[a]?.common?.name, options.lang) || a.split('.').pop();
1435
- const bName = getName(objects[b]?.common?.name, options.lang) || b.split('.').pop();
1436
- if (aName > bName) {
1437
- return 1;
1438
- }
1439
- if (aName < bName) {
1440
- return -1;
1441
- }
1442
- return 0;
1443
- });
1444
- info.funcEnums.sort((a, b) => {
1445
- const aName = getName(objects[a]?.common?.name, options.lang) || a.split('.').pop();
1446
- const bName = getName(objects[b]?.common?.name, options.lang) || b.split('.').pop();
1447
- if (aName > bName) {
1448
- return 1;
1449
- }
1450
- if (aName < bName) {
1451
- return -1;
1452
- }
1453
- return 0;
1454
- });
1455
- info.roles.sort((a, b) => a.role.localeCompare(b.role));
1456
- info.types.sort();
1457
- return { info, root };
1458
- }
1459
- function findNode(root, id, _parts, _path, _level) {
1460
- if (root.data.id === id) {
1461
- return root;
1462
- }
1463
- if (!_parts) {
1464
- _parts = id.split('.');
1465
- _level = 0;
1466
- _path = _parts[_level];
1467
- }
1468
- if (!root.children && root.data.id !== id) {
1469
- return null;
1470
- }
1471
- let found;
1472
- if (root.children) {
1473
- for (let i = 0; i < root.children.length; i++) {
1474
- const _id = root.children[i].data.id;
1475
- if (_id === _path) {
1476
- found = root.children[i];
1477
- break;
1478
- }
1479
- else if (_id > _path) {
1480
- break;
1481
- }
1482
- }
1483
- }
1484
- if (found) {
1485
- _level ||= 0;
1486
- return findNode(found, id, _parts, `${_path}.${_parts[_level + 1]}`, _level + 1);
1487
- }
1488
- return null;
1489
- }
1490
- function findRoomsForObject(info, id, lang, rooms) {
1491
- if (!id) {
1492
- return { rooms: [], per: false };
1493
- }
1494
- rooms ||= [];
1495
- for (const room of info.roomEnums) {
1496
- const common = info.objects[room]?.common;
1497
- if (!common) {
1498
- continue;
1499
- }
1500
- const name = getName(common.name, lang);
1501
- if (common.members?.includes(id) && !rooms.includes(name)) {
1502
- rooms.push(name);
1503
- }
1504
- }
1505
- let ownEnums;
1506
- // Check parent
1507
- const parts = id.split('.');
1508
- parts.pop();
1509
- id = parts.join('.');
1510
- if (info.objects[id]) {
1511
- ownEnums = rooms.length;
1512
- findRoomsForObject(info, id, lang, rooms);
1513
- }
1514
- return { rooms, per: !ownEnums }; // per is if the enums are from parent
1515
- }
1516
- function findEnumsForObjectAsIds(info, id, enumName, funcs) {
1517
- if (!id) {
1518
- return [];
1519
- }
1520
- funcs ||= [];
1521
- for (let i = 0; i < info[enumName].length; i++) {
1522
- const common = info.objects[info[enumName][i]]?.common;
1523
- if (common?.members?.includes(id) && !funcs.includes(info[enumName][i])) {
1524
- funcs.push(info[enumName][i]);
1525
- }
1526
- }
1527
- funcs.sort();
1528
- return funcs;
1529
- }
1530
- function findFunctionsForObject(info, id, lang, funcs) {
1531
- if (!id) {
1532
- return { funcs: [], pef: false };
1533
- }
1534
- funcs ||= [];
1535
- for (let i = 0; i < info.funcEnums.length; i++) {
1536
- const common = info.objects[info.funcEnums[i]]?.common;
1537
- if (!common) {
1538
- continue;
1539
- }
1540
- const name = getName(common.name, lang);
1541
- if (common.members?.includes(id) && !funcs.includes(name)) {
1542
- funcs.push(name);
1543
- }
1544
- }
1545
- let ownEnums;
1546
- // Check parent
1547
- const parts = id.split('.');
1548
- parts.pop();
1549
- id = parts.join('.');
1550
- if (info.objects[id]) {
1551
- ownEnums = funcs.length;
1552
- findFunctionsForObject(info, id, lang, funcs);
1553
- }
1554
- return { funcs, pef: !ownEnums };
1555
- }
1556
- /*
1557
- function quality2text(q) {
1558
- if (!q) {
1559
- return 'ok';
1560
- }
1561
- const custom = q & 0xFFFF0000;
1562
- let text = '';
1563
- if (q & 0x40) text += 'device';
1564
- if (q & 0x80) text += 'sensor';
1565
- if (q & 0x01) text += ' bad';
1566
- if (q & 0x02) text += ' not connected';
1567
- if (q & 0x04) text += ' error';
1568
-
1569
- return text + (custom ? '|0x' + (custom >> 16).toString(16).toUpperCase() : '') + ' [0x' + q.toString(16).toUpperCase() + ']';
1570
- }
1571
- */
1572
- /**
1573
- * Format a state value for visualization
1574
- */
1575
- function formatValue(options) {
1576
- const { dateFormat, state, isFloatComma, texts, obj } = options;
1577
- const states = Utils.getStates(obj);
1578
- const isCommon = obj.common;
1579
- let fileViewer;
1580
- let v =
1581
- // @ts-expect-error deprecated from js-controller 6
1582
- isCommon?.type === 'file'
1583
- ? '[file]'
1584
- : !state || state.val === null
1585
- ? '(null)'
1586
- : state.val === undefined
1587
- ? '[undef]'
1588
- : state.val;
1589
- const type = typeof v;
1590
- if (isCommon?.role && typeof isCommon.role === 'string' && isCommon.role.match(/^value\.time|^date/)) {
1591
- if (v && typeof v === 'string') {
1592
- if (Utils.isStringInteger(v)) {
1593
- // we assume a unix ts
1594
- v = new Date(parseInt(v, 10)).toString();
1595
- }
1596
- else {
1597
- // check if parsable by new date
1598
- try {
1599
- const parsedDate = new Date(v);
1600
- if (Utils.isValidDate(parsedDate)) {
1601
- v = parsedDate.toString();
1602
- }
1603
- }
1604
- catch {
1605
- // ignore
1606
- }
1607
- }
1608
- }
1609
- else {
1610
- if (v > 946681200 && v < 946681200000) {
1611
- // '2000-01-01T00:00:00' => 946681200000
1612
- v *= 1_000; // maybe the time is in seconds (UNIX time)
1613
- }
1614
- // "null" and undefined could not be here. See `let v = (isCommon && isCommon.type === 'file') ....` above
1615
- v = v ? new Date(v).toString() : v;
1616
- }
1617
- }
1618
- else if (isCommon?.role && typeof isCommon.role === 'string' && isCommon.role.match(/^value\.duration/)) {
1619
- // Format duration values in HH:mm:ss format
1620
- if (typeof v === 'number' && v >= 0) {
1621
- const hours = Math.floor(v / 3600);
1622
- const minutes = Math.floor((v % 3600) / 60);
1623
- const seconds = Math.floor(v % 60);
1624
- v = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
1625
- }
1626
- else if (typeof v === 'string' && Utils.isStringInteger(v)) {
1627
- const numValue = parseInt(v, 10);
1628
- if (numValue >= 0) {
1629
- const hours = Math.floor(numValue / 3600);
1630
- const minutes = Math.floor((numValue % 3600) / 60);
1631
- const seconds = Math.floor(numValue % 60);
1632
- v = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
1633
- }
1634
- }
1635
- }
1636
- else {
1637
- if (type === 'number') {
1638
- if (!Number.isInteger(v)) {
1639
- v = Math.round(v * 100_000_000) / 100_000_000; // remove 4.00000000000000001
1640
- if (isFloatComma) {
1641
- v = v.toString().replace('.', ',');
1642
- }
1643
- }
1644
- }
1645
- else if (type === 'object') {
1646
- v = JSON.stringify(v);
1647
- }
1648
- else if (type !== 'string') {
1649
- v = v.toString();
1650
- }
1651
- else if (v.startsWith('data:image/')) {
1652
- fileViewer = 'image';
1653
- }
1654
- if (typeof v !== 'string') {
1655
- v = v.toString();
1656
- }
1657
- }
1658
- const valText = { v: v };
1659
- // try to replace number with "common.states"
1660
- if (states && states[v] !== undefined) {
1661
- if (v !== states[v]) {
1662
- valText.s = v;
1663
- v = states[v];
1664
- valText.v = v;
1665
- }
1666
- }
1667
- if (valText.v?.length > 40) {
1668
- valText.c = valText.v;
1669
- valText.v = `${valText.v.substring(0, 40)}...`;
1670
- }
1671
- if (isCommon?.unit) {
1672
- valText.u = isCommon.unit;
1673
- }
1674
- let valFull;
1675
- if (options.full) {
1676
- if (typeof v === 'string' && v.length > 100) {
1677
- valFull = [{ t: texts.value, v: `${v.substring(0, 100)}...` }];
1678
- }
1679
- else {
1680
- valFull = [{ t: texts.value, v }];
1681
- }
1682
- if (state) {
1683
- if (state.ack !== undefined && state.ack !== null) {
1684
- valFull.push({ t: texts.ack, v: state.ack.toString() });
1685
- }
1686
- if (state.ts) {
1687
- valFull.push({ t: texts.ts, v: state.ts ? Utils.formatDate(new Date(state.ts), dateFormat) : '' });
1688
- }
1689
- if (state.lc) {
1690
- valFull.push({ t: texts.lc, v: state.lc ? Utils.formatDate(new Date(state.lc), dateFormat) : '' });
1691
- }
1692
- if (state.from) {
1693
- let from = state.from.toString();
1694
- if (from.startsWith('system.adapter.')) {
1695
- from = from.substring(15);
1696
- }
1697
- valFull.push({ t: texts.from, v: from });
1698
- }
1699
- if (state.user) {
1700
- let user = state.user.toString();
1701
- if (user.startsWith('system.user.')) {
1702
- user = user.substring(12);
1703
- }
1704
- valFull.push({ t: texts.user, v: user });
1705
- }
1706
- if (state.c) {
1707
- valFull.push({ t: texts.c, v: state.c });
1708
- }
1709
- valFull.push({ t: texts.quality, v: Utils.quality2text(state.q || 0).join(', '), nbr: true });
1710
- }
1711
- }
1712
- return {
1713
- valText,
1714
- valFull,
1715
- fileViewer,
1716
- };
1717
- }
1718
- /**
1719
- * Get CSS style for given state value
1720
- */
1721
- function getValueStyle(options) {
1722
- const { state /* , isExpertMode, isButton */ } = options;
1723
- const color = state?.ack ? (state.q ? '#ffa500' : '') : '#ff2222c9';
1724
- // do not show the color of the button in non-expert mode
1725
- // if (!isExpertMode && isButton) {
1726
- // color = '';
1727
- // }
1728
- return { color };
1729
- }
1730
- function prepareSparkData(values, from) {
1731
- // set one point every hour
1732
- let time = from;
1733
- let i = 1;
1734
- const v = [];
1735
- while (i < values.length && time < from + 25 * 3600000) {
1736
- // find the interval
1737
- while (values[i - 1].ts < time && time <= values[i].ts && i < values.length) {
1738
- i++;
1739
- }
1740
- if (i === 1 && values[i - 1].ts >= time) {
1741
- // assume the value was always null
1742
- v.push(0);
1743
- }
1744
- else if (i < values.length) {
1745
- if (typeof values[i].val === 'boolean' || typeof values[i - 1].val === 'boolean') {
1746
- v.push(values[i].val ? 1 : 0);
1747
- }
1748
- else {
1749
- // remove nulls
1750
- values[i - 1].val ||= 0;
1751
- values[i].val ||= 0;
1752
- // interpolate
1753
- const nm1 = values[i - 1].val;
1754
- const n = values[i].val;
1755
- const val = nm1 + ((n - nm1) * (time - values[i - 1].ts)) / (values[i].ts - values[i - 1].ts);
1756
- v.push(val);
1757
- }
1758
- }
1759
- time += 3600000;
1760
- }
1761
- return v;
1762
- }
646
+ color: theme.palette.mode === 'dark' ? '#e079ff' : '#500070',
647
+ }),
648
+ contextMenuRoom: (theme) => ({
649
+ color: theme.palette.mode === 'dark' ? '#ff9a33' : '#642a00',
650
+ }),
651
+ contextMenuRole: (theme) => ({
652
+ color: theme.palette.mode === 'dark' ? '#ffdb43' : '#562d00',
653
+ }),
654
+ contextMenuDelete: (theme) => ({
655
+ color: theme.palette.mode === 'dark' ? '#ff4f4f' : '#cf0000',
656
+ }),
657
+ contextMenuKeys: {
658
+ marginLeft: 8,
659
+ opacity: 0.7,
660
+ fontSize: 'smaller',
661
+ },
662
+ contextMenuWithSubMenu: {
663
+ display: 'flex',
664
+ },
665
+ ...utilStyles,
666
+ };
1763
667
  export const ITEM_IMAGES = {
1764
668
  state: (React.createElement(IconState, { className: "itemIcon", style: { verticalAlign: 'middle' } })),
1765
669
  channel: (React.createElement(IconChannel, { className: "itemIcon", style: { verticalAlign: 'middle' } })),
@@ -1882,18 +786,19 @@ export class ObjectBrowserClass extends Component {
1882
786
  aliasesMap: {},
1883
787
  };
1884
788
  localStorage = window._localStorage || window.localStorage;
1885
- lastAppliedFilter = null;
1886
789
  tableRef;
1887
- filterRefs;
1888
790
  pausedSubscribes = false;
1889
791
  selectFirst;
792
+ /** Last navigation that was applied from `navigateTo` or reported via `onNavigateTo` (loop guard). */
793
+ lastNav = null;
794
+ /** True while applying `navigateTo`, so the derived-state watcher does not echo it back. */
795
+ applyingNav = false;
1890
796
  root = null;
1891
797
  states = {};
1892
798
  subscribes = [];
1893
799
  unsubscribeTimer = null;
1894
800
  statesUpdateTimer = null;
1895
801
  objectsUpdateTimer = null;
1896
- filterTimer = null;
1897
802
  visibleCols;
1898
803
  texts;
1899
804
  possibleCols;
@@ -1921,22 +826,22 @@ export class ObjectBrowserClass extends Component {
1921
826
  resizerActiveDiv = null;
1922
827
  resizerNextDiv = null;
1923
828
  storedWidths = null;
1924
- systemConfig;
829
+ systemConfig = null;
1925
830
  objects;
1926
831
  defaultHistory = '';
1927
- cltrPressed = false;
832
+ ctrlPressed = false;
1928
833
  columnsVisibility = {};
1929
834
  changedIds = null;
1930
835
  contextMenu = null;
1931
836
  recordStates = [];
1932
837
  styles = {};
838
+ expertMode = false;
1933
839
  customColumnDialog = null;
1934
- /** Namespaces which are allowed to be edited by non-expert users */
1935
- static NON_EXPERT_NAMESPACES = ['0_userdata.0.', 'alias.0.'];
1936
840
  constructor(props) {
1937
841
  super(props);
1938
842
  const lastSelectedItemStr = this.localStorage.getItem(`${props.dialogName || 'App'}.objectSelected`) || '';
1939
843
  this.selectFirst = '';
844
+ this.expertMode = !!this.props.expertMode;
1940
845
  if (lastSelectedItemStr.startsWith('[')) {
1941
846
  try {
1942
847
  const lastSelectedItems = JSON.parse(lastSelectedItemStr);
@@ -1976,20 +881,35 @@ export class ObjectBrowserClass extends Component {
1976
881
  filter = { ...DEFAULT_FILTER };
1977
882
  }
1978
883
  // Migrate old filters to new one
1979
- if (typeof filter.room === 'string') {
1980
- filter.room = [filter.room];
884
+ if (typeof filter.room === 'string' && filter.room) {
885
+ filter.room = [filter.room].filter(s => s);
886
+ if (!filter.room.length) {
887
+ delete filter.room;
888
+ }
1981
889
  }
1982
- if (typeof filter.func === 'string') {
1983
- filter.func = [filter.func];
890
+ if (typeof filter.func === 'string' && filter.func) {
891
+ filter.func = [filter.func].filter(s => s);
892
+ if (!filter.func.length) {
893
+ delete filter.func;
894
+ }
1984
895
  }
1985
- if (typeof filter.role === 'string') {
1986
- filter.role = [filter.role];
896
+ if (typeof filter.role === 'string' && filter.role) {
897
+ filter.role = [filter.role].filter(s => s);
898
+ if (!filter.role.length) {
899
+ delete filter.role;
900
+ }
1987
901
  }
1988
902
  if (typeof filter.type === 'string') {
1989
- filter.type = [filter.type];
903
+ filter.type = [filter.type].filter(s => s);
904
+ if (!filter.type.length) {
905
+ delete filter.type;
906
+ }
1990
907
  }
1991
908
  if (typeof filter.custom === 'string') {
1992
- filter.custom = [filter.custom];
909
+ filter.custom = [filter.custom].filter(s => s);
910
+ if (!filter.custom.length) {
911
+ delete filter.custom;
912
+ }
1993
913
  }
1994
914
  filter.expertMode =
1995
915
  props.expertMode !== undefined
@@ -1997,8 +917,6 @@ export class ObjectBrowserClass extends Component {
1997
917
  : (window._sessionStorage || window.sessionStorage).getItem('App.expertMode') ===
1998
918
  'true';
1999
919
  this.tableRef = createRef();
2000
- this.filterRefs = {};
2001
- Object.keys(DEFAULT_FILTER).forEach(name => (this.filterRefs[name] = createRef()));
2002
920
  this.visibleCols = props.columns || SCREEN_WIDTHS[props.width || 'lg'].fields;
2003
921
  // remove type column if only one type must be selected
2004
922
  if (props.types && props.types.length === 1) {
@@ -2062,46 +980,48 @@ export class ObjectBrowserClass extends Component {
2062
980
  // ignore
2063
981
  }
2064
982
  this.state = {
2065
- loaded: false,
2066
- foldersFirst,
2067
- selected,
2068
- selectedNonObject: this.localStorage.getItem(`${props.dialogName || 'App'}.selectedNonObject`) || '',
2069
- filter,
2070
- filterKey: 0,
2071
- focused: this.localStorage.getItem(`${props.dialogName || 'App'}.focused`) || '',
2072
- depth: 0,
2073
- expandAllVisible: false,
2074
- expanded,
2075
- toast: '',
2076
- scrollBarWidth: 16,
2077
- customDialog,
2078
- editObjectDialog: '',
2079
- editObjectAlias: false, // open the edit object dialog on alias tab
2080
- viewFileDialog: '',
2081
- showAliasEditor: '',
2082
- enumDialog: null,
2083
- roleDialog: null,
2084
- statesView,
983
+ aliasMenu: '',
984
+ beautifyJsonExport: true,
2085
985
  columns,
2086
- columnsForAdmin: null,
2087
- columnsSelectorShow: false,
2088
986
  columnsAuto: this.localStorage.getItem(`${props.dialogName || 'App'}.columnsAuto`) !== 'false',
2089
- columnsWidths,
2090
987
  columnsDialogTransparent: 100,
2091
988
  columnsEditCustomDialog: null,
989
+ columnsForAdmin: null,
990
+ columnsSelectorShow: false,
991
+ columnsWidths,
2092
992
  customColumnDialogValueChanged: false,
2093
- showExportDialog: false,
2094
- showAllExportOptions: false,
2095
- linesEnabled: this.localStorage.getItem(`${props.dialogName || 'App'}.lines`) === 'true',
2096
- showDescription: this.localStorage.getItem(`${props.dialogName || 'App'}.desc`) !== 'false',
2097
- showContextMenu: null,
2098
- noStatesByExportImport: false,
2099
- beautifyJsonExport: true,
993
+ customDialog,
994
+ depth: 0,
995
+ editObjectAlias: false, // open the edit object dialog on alias tab
996
+ editObjectDialog: '',
997
+ enumDialog: null,
2100
998
  excludeSystemRepositoriesFromExport: true,
2101
999
  excludeTranslations: false,
2102
- tooltipInfo: null,
2103
- aliasMenu: '',
1000
+ expandAllVisible: false,
1001
+ expanded,
1002
+ filter,
1003
+ filterKey: 0,
1004
+ focused: this.localStorage.getItem(`${props.dialogName || 'App'}.focused`) || '',
1005
+ foldersFirst,
1006
+ linesEnabled: this.localStorage.getItem(`${props.dialogName || 'App'}.lines`) === 'true',
1007
+ loaded: false,
1008
+ noStatesByExportImport: false,
1009
+ roleDialog: null,
1010
+ scrollBarWidth: 16,
1011
+ selected,
1012
+ selectedNonObject: this.localStorage.getItem(`${props.dialogName || 'App'}.selectedNonObject`) || '',
1013
+ showAliasEditor: '',
1014
+ showAllExportOptions: false,
1015
+ showContextMenu: null,
1016
+ showDescription: this.localStorage.getItem(`${props.dialogName || 'App'}.desc`) !== 'false',
1017
+ showExportDialog: false,
1018
+ showImportDialog: false,
1019
+ showImportMenu: null,
2104
1020
  showRenameDialog: null,
1021
+ statesView,
1022
+ toast: '',
1023
+ tooltipInfo: null,
1024
+ viewFileDialog: '',
2105
1025
  };
2106
1026
  this.texts = {
2107
1027
  name: props.t('ra_Name'),
@@ -2210,22 +1130,19 @@ export class ObjectBrowserClass extends Component {
2210
1130
  Object.assign(objects || {}, moreObjects);
2211
1131
  }
2212
1132
  }
2213
- this.systemConfig =
2214
- this.systemConfig ||
2215
- objects?.['system.config'] ||
1133
+ this.systemConfig ||=
1134
+ objects?.['system.config'] ||
2216
1135
  (await props.socket.getObject('system.config'));
2217
- this.systemConfig.common = this.systemConfig.common || {};
2218
- this.systemConfig.common.defaultNewAcl = this.systemConfig.common.defaultNewAcl || {
1136
+ this.systemConfig.common ||= {};
1137
+ this.systemConfig.common.defaultNewAcl ||= {
2219
1138
  object: 0,
2220
1139
  state: 0,
2221
1140
  file: 0,
2222
1141
  owner: 'system.user.admin',
2223
1142
  ownerGroup: 'system.group.administrator',
2224
1143
  };
2225
- this.systemConfig.common.defaultNewAcl.owner =
2226
- this.systemConfig.common.defaultNewAcl.owner || 'system.user.admin';
2227
- this.systemConfig.common.defaultNewAcl.ownerGroup =
2228
- this.systemConfig.common.defaultNewAcl.ownerGroup || 'system.group.administrator';
1144
+ this.systemConfig.common.defaultNewAcl.owner ||= 'system.user.admin';
1145
+ this.systemConfig.common.defaultNewAcl.ownerGroup ||= 'system.group.administrator';
2229
1146
  if (typeof this.systemConfig.common.defaultNewAcl.state !== 'number') {
2230
1147
  // TODO: may be convert here from string
2231
1148
  this.systemConfig.common.defaultNewAcl.state = 0x664;
@@ -2308,31 +1225,30 @@ export class ObjectBrowserClass extends Component {
2308
1225
  this.info = info;
2309
1226
  // Show first selected item
2310
1227
  const node = this.state.selected?.length && findNode(this.root, this.state.selected[0]);
2311
- this.lastAppliedFilter = null;
2312
1228
  // If the selected ID is not visible, reset filter
2313
1229
  if (node &&
2314
1230
  !applyFilter(node, this.state.filter, props.lang, this.objects, undefined, undefined, props.customFilter, props.types)) {
2315
1231
  // reset filter
2316
1232
  this.setState({ filter: { ...DEFAULT_FILTER }, columnsForAdmin }, () => {
2317
- this.setState({ loaded: true, updating: false }, () => this.expandAllSelected(() => this.onAfterSelect()));
1233
+ this.doFilter();
1234
+ this.setState({ loaded: true, updating: false }, () => this.expandAllSelected(() => {
1235
+ this.onAfterSelect();
1236
+ this.applyInitialNavigateTo();
1237
+ }));
2318
1238
  });
2319
1239
  }
2320
1240
  else {
2321
- this.setState({ loaded: true, updating: false, columnsForAdmin }, () => this.expandAllSelected(() => this.onAfterSelect()));
1241
+ this.doFilter();
1242
+ this.setState({ loaded: true, updating: false, columnsForAdmin }, () => this.expandAllSelected(() => {
1243
+ this.onAfterSelect();
1244
+ this.applyInitialNavigateTo();
1245
+ }));
2322
1246
  }
2323
1247
  }
2324
1248
  catch (error) {
2325
1249
  this.showError(error);
2326
1250
  }
2327
1251
  }
2328
- /**
2329
- * Check if it is a non-expert id
2330
- */
2331
- static isNonExpertId(
2332
- /** id to test */
2333
- id) {
2334
- return !!ObjectBrowserClass.NON_EXPERT_NAMESPACES.find(saveNamespace => id.startsWith(saveNamespace));
2335
- }
2336
1252
  expandAllSelected(cb) {
2337
1253
  const expanded = [...this.state.expanded];
2338
1254
  let changed = false;
@@ -2421,6 +1337,7 @@ export class ObjectBrowserClass extends Component {
2421
1337
  }
2422
1338
  }
2423
1339
  }
1340
+ // This function is used
2424
1341
  static getDerivedStateFromProps(props, state) {
2425
1342
  const newState = {};
2426
1343
  let changed = false;
@@ -2454,14 +1371,14 @@ export class ObjectBrowserClass extends Component {
2454
1371
  }
2455
1372
  }
2456
1373
  onKeyPress = (event) => {
2457
- if (event.type === 'keydown' && event.ctrlKey && !this.cltrPressed) {
2458
- this.cltrPressed = true;
1374
+ if (event.type === 'keydown' && event.ctrlKey && !this.ctrlPressed) {
1375
+ this.ctrlPressed = true;
2459
1376
  if (this.tableRef.current) {
2460
1377
  this.tableRef.current.className = 'highlight-link';
2461
1378
  }
2462
1379
  }
2463
- else if (event.type === 'keyup' && !event.ctrlKey && this.cltrPressed) {
2464
- this.cltrPressed = false;
1380
+ else if (event.type === 'keyup' && !event.ctrlKey && this.ctrlPressed) {
1381
+ this.ctrlPressed = false;
2465
1382
  if (this.tableRef.current) {
2466
1383
  this.tableRef.current.className = '';
2467
1384
  }
@@ -2471,10 +1388,6 @@ export class ObjectBrowserClass extends Component {
2471
1388
  * Called when component is unmounted.
2472
1389
  */
2473
1390
  componentWillUnmount() {
2474
- if (this.filterTimer) {
2475
- clearTimeout(this.filterTimer);
2476
- this.filterTimer = null;
2477
- }
2478
1391
  window.removeEventListener('contextmenu', this.onContextMenu, true);
2479
1392
  window.removeEventListener('keydown', this.onKeyPress, true);
2480
1393
  window.removeEventListener('keyup', this.onKeyPress, true);
@@ -2813,7 +1726,7 @@ export class ObjectBrowserClass extends Component {
2813
1726
  let cColumns;
2814
1727
  if (columns) {
2815
1728
  cColumns = aColumns
2816
- .map((_item) => {
1729
+ ?.map((_item) => {
2817
1730
  if (typeof _item !== 'object') {
2818
1731
  return { path: _item, name: _item.split('.').pop() };
2819
1732
  }
@@ -2853,7 +1766,7 @@ export class ObjectBrowserClass extends Component {
2853
1766
  else {
2854
1767
  cColumns = null;
2855
1768
  }
2856
- if (cColumns?.length) {
1769
+ if (cColumns && cColumns.length) {
2857
1770
  columnsForAdmin ||= {};
2858
1771
  columnsForAdmin[obj.common.name] = cColumns.sort((a, b) => a.path > b.path ? -1 : a.path < b.path ? 1 : 0);
2859
1772
  }
@@ -2906,9 +1819,8 @@ export class ObjectBrowserClass extends Component {
2906
1819
  });
2907
1820
  this.root = root;
2908
1821
  this.info = info;
2909
- this.lastAppliedFilter = null; // apply filter anew
2910
1822
  if (!this.pausedSubscribes) {
2911
- this.forceUpdate();
1823
+ this.doFilter();
2912
1824
  }
2913
1825
  // else it will be re-rendered when the dialog will be closed
2914
1826
  }, 500);
@@ -3032,129 +1944,52 @@ export class ObjectBrowserClass extends Component {
3032
1944
  this.subscribes.forEach(id => this.props.socket.subscribeState(id, this.onStateChange));
3033
1945
  }
3034
1946
  }
3035
- onFilter(name, value) {
3036
- this.filterTimer = null;
3037
- const filter = { ...this.state.filter };
3038
- Object.keys(this.filterRefs).forEach((_name) => {
3039
- if (this.filterRefs[_name]?.current) {
3040
- const filterRef = this.filterRefs[_name].current;
3041
- for (let i = 0; i < filterRef.children.length; i++) {
3042
- if (filterRef.children[i].tagName === 'INPUT') {
3043
- if (_name === 'role' ||
3044
- _name === 'type' ||
3045
- _name === 'func' ||
3046
- _name === 'custom' ||
3047
- _name === 'room') {
3048
- const value = filterRef.children[i].value;
3049
- if (value) {
3050
- filter[_name] = value.split(',');
3051
- }
3052
- else {
3053
- filter[_name] = undefined;
3054
- }
3055
- }
3056
- else {
3057
- filter[_name] = filterRef.children[i].value;
3058
- }
3059
- break;
3060
- }
3061
- }
3062
- }
3063
- });
3064
- if (name) {
3065
- filter[name] = value;
3066
- if (name === 'expertMode') {
3067
- (window._sessionStorage || window.sessionStorage).setItem('App.expertMode', value ? 'true' : 'false');
3068
- }
3069
- }
3070
- if (JSON.stringify(this.state.filter) !== JSON.stringify(filter)) {
3071
- this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(filter));
3072
- this.setState({ filter }, () => this.props.onFilterChanged && this.props.onFilterChanged(filter));
3073
- }
3074
- }
3075
1947
  clearFilter() {
3076
- const filter = { ...this.state.filter };
3077
- Object.keys(this.filterRefs).forEach(name => {
3078
- if (this.filterRefs[name]?.current) {
3079
- const filterRef = this.filterRefs[name].current;
3080
- for (let i = 0; i < filterRef.childNodes.length; i++) {
3081
- const item = filterRef.childNodes[i];
3082
- if (item.tagName === 'INPUT') {
3083
- filter[name] = '';
3084
- item.value = '';
3085
- break;
3086
- }
3087
- }
3088
- }
3089
- });
3090
- if (JSON.stringify(this.state.filter) !== JSON.stringify(filter)) {
3091
- this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(filter));
3092
- this.setState({ filter, filterKey: this.state.filterKey + 1 }, () => this.props.onFilterChanged?.(filter));
1948
+ if (JSON.stringify(this.state.filter) !== JSON.stringify(DEFAULT_FILTER)) {
1949
+ this.setState({ filter: { ...DEFAULT_FILTER }, filterKey: this.state.filterKey + 1 }, () => {
1950
+ this.doFilter();
1951
+ this.props.onFilterChanged?.({ ...DEFAULT_FILTER });
1952
+ });
3093
1953
  }
3094
1954
  }
3095
1955
  isFilterEmpty() {
3096
- const someNotEmpty = Object.keys(this.state.filter).find(attr => attr !== 'expertMode' && this.state.filter[attr]);
3097
- return !someNotEmpty;
3098
- }
3099
- getFilterInput(filterName) {
3100
- return (React.createElement(FormControl, { sx: this.styles.filterInput, key: `${filterName}_${this.state.filterKey}`, title: filterName === 'name' || filterName === 'id'
3101
- ? this.props.t('ra_You can use * as wildcard')
3102
- : undefined,
3103
- // style={{ marginTop: 0, marginBottom: 0 }}
3104
- margin: "dense" },
3105
- React.createElement(Input, { ref: this.filterRefs[filterName], classes: { underline: 'no-underline' }, id: filterName, placeholder: this.texts[`filter_${filterName}`], defaultValue: this.state.filter[filterName] || '', onChange: () => {
3106
- if (this.filterTimer) {
3107
- clearTimeout(this.filterTimer);
3108
- }
3109
- this.filterTimer = setTimeout(() => this.onFilter(), 400);
3110
- }, autoComplete: "off" }),
3111
- this.filterRefs[filterName]?.current?.firstChild?.value ? (React.createElement("div", { style: {
3112
- position: 'absolute',
3113
- right: 0,
3114
- } },
3115
- React.createElement(IconButton, { size: "small", onClick: () => {
3116
- (this.filterRefs[filterName].current?.firstChild).value = '';
3117
- this.onFilter(filterName, '');
3118
- } },
3119
- React.createElement(IconClose, null)))) : null));
1956
+ return (!!this.state.filter.id ||
1957
+ !!this.state.filter.name ||
1958
+ !!this.state.filter.room?.length ||
1959
+ !!this.state.filter.func?.length ||
1960
+ !!this.state.filter.role?.length ||
1961
+ !!this.state.filter.type?.length ||
1962
+ !!this.state.filter.custom?.length);
1963
+ }
1964
+ getFilterInput(name) {
1965
+ return (React.createElement(CustomFilterInput, { key: `${name}_${this.state.filterKey}`, styles: this.styles.filterInput, name: name, texts: this.texts, t: this.props.t, initialValue: this.state.filter[name], onChange: (name, value) => {
1966
+ const filter = { ...this.state.filter };
1967
+ if (value === undefined) {
1968
+ delete filter[name];
1969
+ }
1970
+ else {
1971
+ filter[name] = value;
1972
+ }
1973
+ this.setState({ filter }, () => {
1974
+ this.doFilter();
1975
+ this.props.onFilterChanged?.(filter);
1976
+ });
1977
+ } }));
3120
1978
  }
3121
1979
  getFilterSelect(name, values) {
3122
- const hasIcons = !!values?.find(item => item.icon);
3123
- return (React.createElement("div", { style: { position: 'relative' } },
3124
- React.createElement(Select, { variant: "standard", key: `${name}_${this.state.filterKey}`, ref: this.filterRefs[name], sx: styles.headerCellInput, className: "no-underline", multiple: true, onChange: () => {
3125
- if (this.filterTimer) {
3126
- clearTimeout(this.filterTimer);
3127
- }
3128
- this.filterTimer = setTimeout(() => this.onFilter(), 400);
3129
- }, defaultValue: this.state.filter[name] || [], inputProps: { name, id: name }, displayEmpty: true },
3130
- React.createElement(MenuItem, { key: "empty", value: "" },
3131
- React.createElement("span", { style: styles.selectNone }, name === 'custom' ? this.texts.showAll : this.texts[`filter_${name}`])),
3132
- values?.map(item => {
3133
- let id;
3134
- let _name;
3135
- let icon;
3136
- if (typeof item === 'object') {
3137
- id = item.value;
3138
- _name = item.name;
3139
- icon = item.icon;
3140
- }
3141
- else {
3142
- id = item;
3143
- _name = item;
3144
- }
3145
- return (React.createElement(MenuItem, { sx: styles.headerCellSelectItem, key: id, value: id },
3146
- icon || (hasIcons ? React.createElement("div", { className: "itemIcon" }) : null),
3147
- _name));
3148
- })),
3149
- this.filterRefs[name]?.current?.childNodes[1]?.value ? (React.createElement(Box, { component: "div", sx: styles.selectClearButton },
3150
- React.createElement(IconButton, { size: "small", onClick: () => {
3151
- const newFilter = { ...this.state.filter };
3152
- delete newFilter[name];
3153
- (this.filterRefs[name].current?.childNodes[1]).value = '';
3154
- this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(newFilter));
3155
- this.setState({ filter: newFilter, filterKey: this.state.filterKey + 1 }, () => this.props.onFilterChanged && this.props.onFilterChanged(newFilter));
3156
- } },
3157
- React.createElement(IconClose, null)))) : null));
1980
+ return (React.createElement(CustomFilterSelect, { key: `${name}_${this.state.filterKey}`, name: name, texts: this.texts, initialValue: this.state.filter[name] || [], values: values || [], onChange: (name, value) => {
1981
+ const filter = { ...this.state.filter };
1982
+ if (value === undefined) {
1983
+ delete filter[name];
1984
+ }
1985
+ else {
1986
+ filter[name] = value;
1987
+ }
1988
+ this.setState({ filter }, () => {
1989
+ this.doFilter();
1990
+ this.props.onFilterChanged?.(filter);
1991
+ });
1992
+ } }));
3158
1993
  }
3159
1994
  getFilterSelectRole() {
3160
1995
  return this.getFilterSelect('role', this.info.roles.map(it => it.role));
@@ -3215,6 +2050,9 @@ export class ObjectBrowserClass extends Component {
3215
2050
  this.setState({ expanded: [], depth: 0, selected: [] }, () => this.onAfterSelect());
3216
2051
  }
3217
2052
  expandDepth(root, depth, expanded) {
2053
+ if (!this.root) {
2054
+ throw new Error('No root');
2055
+ }
3218
2056
  root ||= this.root;
3219
2057
  if (depth > 0) {
3220
2058
  root.children?.forEach(item => {
@@ -3492,79 +2330,80 @@ export class ObjectBrowserClass extends Component {
3492
2330
  return null;
3493
2331
  }
3494
2332
  const ObjectMoveRenameDialog = this.props.objectMoveRenameDialog;
3495
- return (React.createElement(ObjectMoveRenameDialog, { expertMode: this.props.expertMode, onClose: () => this.setState({ showRenameDialog: null }), id: this.state.showRenameDialog.id, childrenIds: this.state.showRenameDialog.childrenIds, theme: this.props.theme, socket: this.props.socket, t: this.props.t, objectType: this.objects[this.state.showRenameDialog.id]?.type }));
2333
+ return (React.createElement(ObjectMoveRenameDialog, { expertMode: !!this.props.expertMode, onClose: () => this.setState({ showRenameDialog: null }), id: this.state.showRenameDialog.id, childrenIds: this.state.showRenameDialog.childrenIds, theme: this.props.theme, socket: this.props.socket, t: this.props.t, objectType: this.objects[this.state.showRenameDialog.id]?.type }));
3496
2334
  }
3497
- handleJsonUpload(evt) {
3498
- const target = evt.target;
3499
- const f = target.files?.length && target.files[0];
3500
- if (f) {
3501
- const r = new FileReader();
3502
- r.onload = async (e) => {
3503
- const contents = e.target?.result;
2335
+ async parseJsonFile(contents) {
2336
+ try {
2337
+ const json = JSON.parse(contents);
2338
+ const len = Object.keys(json).length;
2339
+ const id = json._id;
2340
+ // it could be a single object or many objects
2341
+ if (id === undefined && len) {
2342
+ // many objects
2343
+ await this.loadObjects(json);
2344
+ window.alert(this.props.t('ra_%s object(s) processed', len));
2345
+ }
2346
+ else {
2347
+ // it is only one object in form
2348
+ // {
2349
+ // "_id": "xxx",
2350
+ // "common": "yyy",
2351
+ // "native": "zzz"
2352
+ // "val": JSON.stringify(value)
2353
+ // "ack": true
2354
+ // }
2355
+ if (!id) {
2356
+ return window.alert(this.props.t('ra_Invalid structure'));
2357
+ }
3504
2358
  try {
3505
- const json = JSON.parse(contents);
3506
- const len = Object.keys(json).length;
3507
- const id = json._id;
3508
- // it could be a single object or many objects
3509
- if (id === undefined && len) {
3510
- // many objects
3511
- await this.loadObjects(json);
3512
- window.alert(this.props.t('ra_%s object(s) processed', len));
2359
+ let enums;
2360
+ let val;
2361
+ let ack;
2362
+ if (json.common.enums) {
2363
+ enums = json.common.enums;
2364
+ delete json.common.enums;
3513
2365
  }
3514
- else {
3515
- // it is only one object in form
3516
- // {
3517
- // "_id": "xxx",
3518
- // "common": "yyy",
3519
- // "native": "zzz"
3520
- // "val": JSON.stringify(value)
3521
- // "ack": true
3522
- // }
3523
- if (!id) {
3524
- return window.alert(this.props.t('ra_Invalid structure'));
2366
+ if (json.val) {
2367
+ val = json.val;
2368
+ delete json.val;
2369
+ }
2370
+ if (json.ack !== undefined) {
2371
+ ack = json.ack;
2372
+ delete json.ack;
2373
+ }
2374
+ await this.props.socket.setObject(json._id, json);
2375
+ if (json.type === 'state') {
2376
+ if (val !== undefined && val !== null) {
2377
+ await this.props.socket.setState(json._id, val, ack === undefined ? true : ack);
3525
2378
  }
3526
- try {
3527
- let enums;
3528
- let val;
3529
- let ack;
3530
- if (json.common.enums) {
3531
- enums = json.common.enums;
3532
- delete json.common.enums;
3533
- }
3534
- if (json.val) {
3535
- val = json.val;
3536
- delete json.val;
3537
- }
3538
- if (json.ack !== undefined) {
3539
- ack = json.ack;
3540
- delete json.ack;
3541
- }
3542
- await this.props.socket.setObject(json._id, json);
3543
- if (json.type === 'state') {
3544
- if (val !== undefined && val !== null) {
3545
- await this.props.socket.setState(json._id, val, ack === undefined ? true : ack);
3546
- }
3547
- else {
3548
- const state = await this.props.socket.getState(json._id);
3549
- if (!state || state.val === null || state.val === undefined) {
3550
- await this.props.socket.setState(json._id, json.common.def === undefined ? null : json.common.def, true);
3551
- }
3552
- }
3553
- }
3554
- if (enums) {
3555
- await this._createAllEnums(enums, json._id);
2379
+ else {
2380
+ const state = await this.props.socket.getState(json._id);
2381
+ if (!state || state.val === null || state.val === undefined) {
2382
+ await this.props.socket.setState(json._id, json.common.def === undefined ? null : json.common.def, true);
3556
2383
  }
3557
- window.alert(this.props.t('ra_%s was imported', json._id));
3558
- }
3559
- catch (err) {
3560
- window.alert(err);
3561
2384
  }
3562
2385
  }
2386
+ if (enums) {
2387
+ await this._createAllEnums(enums, json._id);
2388
+ }
2389
+ window.alert(this.props.t('ra_%s was imported', json._id));
3563
2390
  }
3564
2391
  catch (err) {
3565
2392
  window.alert(err);
3566
2393
  }
3567
- return null;
2394
+ }
2395
+ }
2396
+ catch (err) {
2397
+ window.alert(err);
2398
+ }
2399
+ }
2400
+ handleJsonUpload(evt) {
2401
+ const target = evt.target;
2402
+ const f = target.files?.length && target.files[0];
2403
+ if (f) {
2404
+ const r = new FileReader();
2405
+ r.onload = (e) => {
2406
+ this.parseJsonFile(e.target?.result).catch(e => console.log(`Cannot parse file: ${e}`));
3568
2407
  };
3569
2408
  r.readAsText(f);
3570
2409
  }
@@ -3641,6 +2480,26 @@ export class ObjectBrowserClass extends Component {
3641
2480
  }
3642
2481
  return value.length ? value : t('ra_Add new child object to selected parent');
3643
2482
  };
2483
+ onOpenFile() {
2484
+ const input = document.createElement('input');
2485
+ input.setAttribute('type', 'file');
2486
+ input.setAttribute('id', 'files');
2487
+ input.setAttribute('opacity', '0');
2488
+ input.addEventListener('change', (e) => this.handleJsonUpload(e), false);
2489
+ input.click();
2490
+ }
2491
+ renderInputJsonDialog() {
2492
+ if (!this.state.showImportDialog) {
2493
+ return null;
2494
+ }
2495
+ const ObjectBrowserInsertJsonObjects = this.props.objectBrowserInsertJsonObjects;
2496
+ return (React.createElement(ObjectBrowserInsertJsonObjects, { onClose: (text) => {
2497
+ this.setState({ showImportDialog: false });
2498
+ if (text) {
2499
+ void this.parseJsonFile(text);
2500
+ }
2501
+ }, themeName: this.props.themeName, themeType: this.props.themeType, t: this.props.t }));
2502
+ }
3644
2503
  /**
3645
2504
  * Renders the toolbar.
3646
2505
  */
@@ -3674,7 +2533,14 @@ export class ObjectBrowserClass extends Component {
3674
2533
  React.createElement(IconButton, { onClick: () => this.refreshComponent(), disabled: this.state.updating, size: "large" },
3675
2534
  React.createElement(RefreshIcon, null)))),
3676
2535
  this.props.showExpertButton && !this.props.expertMode && (React.createElement(Tooltip, { title: this.props.t('ra_expertMode'), slotProps: { popper: { sx: styles.tooltip } } },
3677
- React.createElement(IconButton, { key: "expertMode", color: this.state.filter.expertMode ? 'secondary' : 'default', onClick: () => this.onFilter('expertMode', !this.state.filter.expertMode), size: "large" },
2536
+ React.createElement(IconButton, { key: "expertMode", color: this.state.filter.expertMode ? 'secondary' : 'default', onClick: () => {
2537
+ const filter = { ...this.state.filter };
2538
+ filter.expertMode = !filter.expertMode;
2539
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(filter));
2540
+ this.setState({
2541
+ filter,
2542
+ });
2543
+ }, size: "large" },
3678
2544
  React.createElement(IconExpert, null)))),
3679
2545
  !this.props.disableColumnSelector && this.props.width !== 'xs' && (React.createElement(Tooltip, { title: this.props.t('ra_Configure'), slotProps: { popper: { sx: styles.tooltip } } },
3680
2546
  React.createElement(IconButton, { key: "columnSelector", color: this.state.columnsAuto ? 'primary' : 'default', onClick: () => this.setState({ columnsSelectorShow: true }), size: "large" },
@@ -3725,15 +2591,24 @@ export class ObjectBrowserClass extends Component {
3725
2591
  }), size: "large" },
3726
2592
  React.createElement(AddIcon, null))))) : null,
3727
2593
  this.props.objectImportExport && (React.createElement(Tooltip, { title: this.props.t('ra_Add objects tree from JSON file'), slotProps: { popper: { sx: styles.tooltip } } },
3728
- React.createElement(IconButton, { onClick: () => {
3729
- const input = document.createElement('input');
3730
- input.setAttribute('type', 'file');
3731
- input.setAttribute('id', 'files');
3732
- input.setAttribute('opacity', '0');
3733
- input.addEventListener('change', (e) => this.handleJsonUpload(e), false);
3734
- input.click();
2594
+ React.createElement(IconButton, { onClick: e => {
2595
+ if (this.props.objectBrowserInsertJsonObjects) {
2596
+ this.setState({ showImportMenu: e.currentTarget });
2597
+ }
2598
+ else {
2599
+ this.onOpenFile();
2600
+ }
3735
2601
  }, size: "large" },
3736
2602
  React.createElement(PublishIcon, null)))),
2603
+ this.props.objectBrowserInsertJsonObjects ? (React.createElement(Menu, { anchorEl: this.state.showImportMenu, open: !!this.state.showImportMenu, onClose: () => this.setState({ showImportMenu: null }) },
2604
+ React.createElement(MenuItem, { onClick: () => this.setState({ showImportMenu: null }, () => this.onOpenFile()) },
2605
+ React.createElement(ListItemIcon, null,
2606
+ React.createElement(UploadFile, null)),
2607
+ React.createElement(ListItemText, null, this.props.t('ra_From file'))),
2608
+ React.createElement(MenuItem, { onClick: () => this.setState({ showImportMenu: null, showImportDialog: true }) },
2609
+ React.createElement(ListItemIcon, null,
2610
+ React.createElement(ContentPaste, null)),
2611
+ React.createElement(ListItemText, null, this.props.t('ra_From text'))))) : null,
3737
2612
  this.props.objectImportExport &&
3738
2613
  (!!this.state.selected.length || this.state.selectedNonObject) && (React.createElement(Tooltip, { title: this.props.t('ra_Save objects tree as JSON file'), slotProps: { popper: { sx: styles.tooltip } } },
3739
2614
  React.createElement(IconButton, { onClick: () => this.setState({ showExportDialog: this._getSelectedIdsForExport().length }), size: "large" },
@@ -3887,9 +2762,9 @@ export class ObjectBrowserClass extends Component {
3887
2762
  : 0;
3888
2763
  const aclSystemConfig = item.data.obj.acl &&
3889
2764
  (item.data.obj.type === 'state'
3890
- ? this.systemConfig.common.defaultNewAcl.state
3891
- : this.systemConfig.common.defaultNewAcl.object);
3892
- const showEdit = this.state.filter.expertMode || ObjectBrowserClass.isNonExpertId(item.data.id);
2765
+ ? this.systemConfig?.common.defaultNewAcl.state
2766
+ : this.systemConfig?.common.defaultNewAcl.object);
2767
+ const showEdit = !!item.data.obj && (this.state.filter.expertMode || isNonExpertId(item.data.id));
3893
2768
  return [
3894
2769
  this.state.filter.expertMode && this.props.objectEditOfAccessControl ? (React.createElement(Tooltip, { key: "acl", title: item.data.aclTooltip, slotProps: { popper: { sx: styles.tooltip } } },
3895
2770
  React.createElement(IconButton, { sx: {
@@ -4002,8 +2877,10 @@ export class ObjectBrowserClass extends Component {
4002
2877
  state,
4003
2878
  obj: obj,
4004
2879
  texts: this.texts,
4005
- dateFormat: this.props.dateFormat || this.systemConfig.common.dateFormat,
4006
- isFloatComma: this.props.isFloatComma === undefined ? this.systemConfig.common.isFloatComma : this.props.isFloatComma,
2880
+ dateFormat: this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT,
2881
+ isFloatComma: this.props.isFloatComma === undefined
2882
+ ? (this.systemConfig?.common.isFloatComma ?? true)
2883
+ : this.props.isFloatComma,
4007
2884
  full: true,
4008
2885
  });
4009
2886
  const valFullRx = [];
@@ -4032,7 +2909,7 @@ export class ObjectBrowserClass extends Component {
4032
2909
  if (fileViewer === 'image') {
4033
2910
  valFullRx.push(React.createElement("img", { style: styles.cellValueTooltipImage, src: state.val, alt: id }));
4034
2911
  }
4035
- else if (role === 'url' || obj.common.role === 'url.self' || obj.common.role === 'url.blank') {
2912
+ else if (role === 'url' || role === 'url.self' || role === 'url.blank') {
4036
2913
  // Show comment about "Hold Ctrl/⌘ key to open the link"
4037
2914
  valFullRx.unshift(React.createElement("div", { key: "ctrl", style: { textDecoration: 'underline', fontWeight: 'bold' } }, this.texts.ctrlForLink));
4038
2915
  }
@@ -4077,9 +2954,9 @@ export class ObjectBrowserClass extends Component {
4077
2954
  state,
4078
2955
  obj: obj,
4079
2956
  texts: this.texts,
4080
- dateFormat: this.props.dateFormat || this.systemConfig.common.dateFormat,
2957
+ dateFormat: this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT,
4081
2958
  isFloatComma: this.props.isFloatComma === undefined
4082
- ? this.systemConfig.common.isFloatComma
2959
+ ? (this.systemConfig?.common.isFloatComma ?? true)
4083
2960
  : this.props.isFloatComma,
4084
2961
  });
4085
2962
  const valTextRx = [];
@@ -4106,7 +2983,12 @@ export class ObjectBrowserClass extends Component {
4106
2983
  // <IconEdit className="copyButton" style={{{ ...styles.cellButtonsValueButton, styles.cellButtonsValueButtonEdit)} key="ce" />
4107
2984
  info = item.data.state;
4108
2985
  }
4109
- info.style = getValueStyle({ state, isExpertMode: this.state.filter.expertMode, isButton: item.data.button });
2986
+ info.style = getValueStyle({
2987
+ state,
2988
+ isExpertMode: this.state.filter.expertMode,
2989
+ isButton: item.data.button,
2990
+ nonAckColor: this.props.theme.palette.nonAck,
2991
+ });
4110
2992
  let val = info.valTextRx;
4111
2993
  if (!this.state.filter.expertMode) {
4112
2994
  if (item.data.button) {
@@ -4250,7 +3132,9 @@ export class ObjectBrowserClass extends Component {
4250
3132
  this.setState({ enumDialogEnums });
4251
3133
  }, secondaryAction: icon },
4252
3134
  React.createElement(ListItemIcon, { sx: { '&.MuiListItemIcon-root': styles.enumCheckbox } },
4253
- React.createElement(Checkbox, { edge: "start", checked: itemEnums.includes(id), tabIndex: -1, disableRipple: true, inputProps: { 'aria-labelledby': labelId } })),
3135
+ React.createElement(Checkbox, { edge: "start", checked: itemEnums.includes(id), tabIndex: -1, disableRipple: true, slotProps: {
3136
+ input: { 'aria-labelledby': labelId },
3137
+ } })),
4254
3138
  React.createElement(ListItemText, { id: labelId }, name)));
4255
3139
  }))));
4256
3140
  }
@@ -4261,7 +3145,7 @@ export class ObjectBrowserClass extends Component {
4261
3145
  if (this.state.roleDialog && this.props.objectBrowserEditRole) {
4262
3146
  const ObjectBrowserEditRole = this.props.objectBrowserEditRole;
4263
3147
  return (React.createElement(ObjectBrowserEditRole, { key: "objectBrowserEditRole", id: this.state.roleDialog, socket: this.props.socket, t: this.props.t, roleArray: this.info.roles, commonType: this.info.objects[this.state.roleDialog]?.common?.type, onClose: (obj) => {
4264
- if (obj) {
3148
+ if (obj && this.state.roleDialog) {
4265
3149
  this.info.objects[this.state.roleDialog] = obj;
4266
3150
  }
4267
3151
  this.setState({ roleDialog: null });
@@ -4284,7 +3168,7 @@ export class ObjectBrowserClass extends Component {
4284
3168
  this.props.socket
4285
3169
  .getObject(this.state.columnsEditCustomDialog?.obj?._id || '')
4286
3170
  .then(obj => {
4287
- if (obj && ObjectBrowserClass.setCustomValue(obj, this.state.columnsEditCustomDialog?.it, value)) {
3171
+ if (obj && setCustomValue(obj, this.state.columnsEditCustomDialog?.it, value)) {
4288
3172
  return this.props.socket.setObject(obj._id, obj);
4289
3173
  }
4290
3174
  throw new Error(this.props.t('ra_Cannot update attribute, because not found in the object'));
@@ -4302,7 +3186,7 @@ export class ObjectBrowserClass extends Component {
4302
3186
  return null;
4303
3187
  }
4304
3188
  if (!this.customColumnDialog) {
4305
- const value = ObjectBrowserClass.getCustomValue(this.state.columnsEditCustomDialog.obj, this.state.columnsEditCustomDialog.it);
3189
+ const value = getCustomValue(this.state.columnsEditCustomDialog.obj, this.state.columnsEditCustomDialog.it);
4306
3190
  this.customColumnDialog = {
4307
3191
  type: (this.state.columnsEditCustomDialog.it.type || typeof value),
4308
3192
  initValue: (value === null || value === undefined ? '' : value).toString(),
@@ -4331,111 +3215,11 @@ export class ObjectBrowserClass extends Component {
4331
3215
  React.createElement(Button, { variant: "contained", onClick: () => this.onColumnsEditCustomDialogClose(true), disabled: !this.state.customColumnDialogValueChanged, color: "primary", startIcon: React.createElement(IconCheck, null) }, this.props.t('ra_Update')),
4332
3216
  React.createElement(Button, { color: "grey", variant: "contained", onClick: () => this.onColumnsEditCustomDialogClose(), startIcon: React.createElement(IconClose, null) }, this.props.t('ra_Cancel')))));
4333
3217
  }
4334
- static getCustomValue(obj, it) {
4335
- if (obj?._id?.startsWith(`${it.adapter}.`) && it.path.length > 1) {
4336
- const p = it.path;
4337
- let value;
4338
- const anyObj = obj;
4339
- if (anyObj[p[0]] && typeof anyObj[p[0]] === 'object') {
4340
- if (p.length === 2) {
4341
- // most common case
4342
- value = anyObj[p[0]][p[1]];
4343
- }
4344
- else if (p.length === 3) {
4345
- value =
4346
- anyObj[p[0]][p[1]] && typeof anyObj[p[0]][p[1]] === 'object' ? anyObj[p[0]][p[1]][p[2]] : null;
4347
- }
4348
- else if (p.length === 4) {
4349
- value =
4350
- anyObj[p[0]][p[1]] && typeof anyObj[p[0]][p[1]] === 'object' && anyObj[p[0]][p[1]][p[2]]
4351
- ? anyObj[p[0]][p[1]][p[2]][p[3]]
4352
- : null;
4353
- }
4354
- else if (p.length === 5) {
4355
- value =
4356
- anyObj[p[0]][p[1]] &&
4357
- typeof anyObj[p[0]][p[1]] === 'object' &&
4358
- anyObj[p[0]][p[1]][p[2]] &&
4359
- anyObj[p[0]][p[1]][p[2]][p[3]]
4360
- ? anyObj[p[0]][p[1]][p[2]][p[3]][p[4]]
4361
- : null;
4362
- }
4363
- else if (p.length === 6) {
4364
- value =
4365
- anyObj[p[0]][p[1]] &&
4366
- typeof anyObj[p[0]][p[1]] === 'object' &&
4367
- anyObj[p[0]][p[1]][p[2]] &&
4368
- anyObj[p[0]][p[1]][p[2]][p[3]] &&
4369
- anyObj[p[0]][p[1]][p[2]][p[3]][p[4]]
4370
- ? anyObj[p[0]][p[1]][p[2]][p[3]][p[4]][p[5]]
4371
- : null;
4372
- }
4373
- if (value === undefined || value === null) {
4374
- return null;
4375
- }
4376
- return value;
4377
- }
4378
- }
4379
- return null;
4380
- }
4381
- static setCustomValue(obj, it, value) {
4382
- if (obj?._id?.startsWith(`${it.adapter}.`) && it.path.length > 1) {
4383
- const p = it.path;
4384
- const anyObj = obj;
4385
- if (anyObj[p[0]] && typeof anyObj[p[0]] === 'object') {
4386
- if (p.length === 2) {
4387
- // most common case
4388
- anyObj[p[0]][p[1]] = value;
4389
- return true;
4390
- }
4391
- if (p.length === 3) {
4392
- if (anyObj[p[0]][p[1]] && typeof anyObj[p[0]][p[1]] === 'object') {
4393
- anyObj[p[0]][p[1]][p[2]] = value;
4394
- return true;
4395
- }
4396
- }
4397
- else if (p.length === 4) {
4398
- if (anyObj[p[0]][p[1]] &&
4399
- typeof anyObj[p[0]][p[1]] === 'object' &&
4400
- anyObj[p[0]][p[1]][p[2]] &&
4401
- typeof anyObj[p[0]][p[1]][p[2]] === 'object') {
4402
- anyObj[p[0]][p[1]][p[2]][p[3]] = value;
4403
- return true;
4404
- }
4405
- }
4406
- else if (p.length === 5) {
4407
- if (anyObj[p[0]][p[1]] &&
4408
- typeof anyObj[p[0]][p[1]] === 'object' &&
4409
- anyObj[p[0]][p[1]][p[2]] &&
4410
- typeof anyObj[p[0]][p[1]][p[2]] === 'object' &&
4411
- anyObj[p[0]][p[1]][p[2]][p[3]] &&
4412
- typeof anyObj[p[0]][p[1]][p[2]][p[3]] === 'object') {
4413
- anyObj[p[0]][p[1]][p[2]][p[3]][p[4]] = value;
4414
- return true;
4415
- }
4416
- }
4417
- else if (p.length === 6) {
4418
- if (anyObj[p[0]][p[1]] &&
4419
- typeof anyObj[p[0]][p[1]] === 'object' &&
4420
- anyObj[p[0]][p[1]][p[2]] &&
4421
- typeof anyObj[p[0]][p[1]][p[2]] === 'object' &&
4422
- anyObj[p[0]][p[1]][p[2]][p[3]] &&
4423
- typeof anyObj[p[0]][p[1]][p[2]][p[3]] === 'object' &&
4424
- anyObj[p[0]][p[1]][p[2]][p[3]][p[4]] &&
4425
- typeof anyObj[p[0]][p[1]][p[2]][p[3]][p[4]] === 'object') {
4426
- anyObj[p[0]][p[1]][p[2]][p[3]][p[4]][p[5]] = value;
4427
- return true;
4428
- }
4429
- }
4430
- }
4431
- }
4432
- return false;
4433
- }
4434
3218
  /**
4435
3219
  * Renders a custom value.
4436
3220
  */
4437
3221
  renderCustomValue(obj, it, item) {
4438
- const text = ObjectBrowserClass.getCustomValue(obj, it);
3222
+ const text = getCustomValue(obj, it);
4439
3223
  if (text !== null && text !== undefined) {
4440
3224
  if (it.edit && !this.props.notEditable && (!it.objTypes || it.objTypes.includes(obj.type))) {
4441
3225
  return (React.createElement(Box, { component: "div", style: {
@@ -4568,7 +3352,7 @@ export class ObjectBrowserClass extends Component {
4568
3352
  newValueTitle.push(`${this.texts.objectChangedBy} ${obj.user.replace(/^system\.user\./, '')}`);
4569
3353
  }
4570
3354
  if (obj.ts) {
4571
- newValueTitle.push(`${this.texts.objectChangedByUser} ${Utils.formatDate(new Date(obj.ts), this.props.dateFormat || this.systemConfig.common.dateFormat)}`);
3355
+ newValueTitle.push(`${this.texts.objectChangedByUser} ${Utils.formatDate(new Date(obj.ts), this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT)}`);
4572
3356
  }
4573
3357
  }
4574
3358
  let readWriteAlias = false;
@@ -4634,24 +3418,30 @@ export class ObjectBrowserClass extends Component {
4634
3418
  if (id === '0_userdata') {
4635
3419
  checkColor = COLOR_NAME_USERDATA(this.props.themeType);
4636
3420
  bold = true;
3421
+ invertBackground = false;
4637
3422
  }
4638
3423
  else if (id === 'alias') {
4639
3424
  checkColor = COLOR_NAME_ALIAS(this.props.themeType);
4640
3425
  bold = true;
3426
+ invertBackground = false;
4641
3427
  }
4642
3428
  else if (id === 'javascript') {
4643
3429
  checkColor = COLOR_NAME_JAVASCRIPT(this.props.themeType);
4644
3430
  bold = true;
3431
+ invertBackground = false;
4645
3432
  }
4646
3433
  else if (id === 'system') {
4647
3434
  checkColor = COLOR_NAME_SYSTEM(this.props.themeType);
4648
3435
  bold = true;
3436
+ invertBackground = false;
4649
3437
  }
4650
3438
  else if (id === 'system.adapter') {
4651
3439
  checkColor = COLOR_NAME_SYSTEM_ADAPTER(this.props.themeType);
3440
+ invertBackground = false;
4652
3441
  }
4653
3442
  else if (!checkColor || this.state.selected.includes(id)) {
4654
3443
  checkColor = 'inherit';
3444
+ invertBackground = false;
4655
3445
  }
4656
3446
  const icons = [];
4657
3447
  if (common?.statusStates) {
@@ -4680,6 +3470,7 @@ export class ObjectBrowserClass extends Component {
4680
3470
  let colorSet = false;
4681
3471
  if (common.statusStates.errorId && this.states[ids.errorId] && this.states[ids.errorId].val) {
4682
3472
  checkColor = this.props.themeType === 'dark' ? COLOR_NAME_ERROR_DARK : COLOR_NAME_ERROR_LIGHT;
3473
+ invertBackground = false;
4683
3474
  colorSet = true;
4684
3475
  icons.push(React.createElement(IconError, { key: "error",
4685
3476
  // title={this.texts.deviceError}
@@ -4690,6 +3481,7 @@ export class ObjectBrowserClass extends Component {
4690
3481
  if (this.states[ids.onlineId].val) {
4691
3482
  checkColor =
4692
3483
  this.props.themeType === 'dark' ? COLOR_NAME_CONNECTED_DARK : COLOR_NAME_CONNECTED_LIGHT;
3484
+ invertBackground = false;
4693
3485
  icons.push(React.createElement(IconConnection, { key: "conn",
4694
3486
  // title={this.texts.deviceError}
4695
3487
  style: this.styles.iconDeviceConnected }));
@@ -4699,6 +3491,7 @@ export class ObjectBrowserClass extends Component {
4699
3491
  this.props.themeType === 'dark'
4700
3492
  ? COLOR_NAME_DISCONNECTED_DARK
4701
3493
  : COLOR_NAME_DISCONNECTED_LIGHT;
3494
+ invertBackground = false;
4702
3495
  icons.push(React.createElement(IconDisconnected, { key: "disc",
4703
3496
  // title={this.texts.deviceError}
4704
3497
  style: this.styles.iconDeviceDisconnected }));
@@ -4722,6 +3515,7 @@ export class ObjectBrowserClass extends Component {
4722
3515
  this.props.themeType === 'dark'
4723
3516
  ? COLOR_NAME_DISCONNECTED_DARK
4724
3517
  : COLOR_NAME_DISCONNECTED_LIGHT;
3518
+ invertBackground = false;
4725
3519
  icons.push(React.createElement(IconDisconnected, { key: "disc",
4726
3520
  // title={this.texts.deviceError}
4727
3521
  style: this.styles.iconDeviceDisconnected }));
@@ -4729,6 +3523,7 @@ export class ObjectBrowserClass extends Component {
4729
3523
  else {
4730
3524
  checkColor =
4731
3525
  this.props.themeType === 'dark' ? COLOR_NAME_CONNECTED_DARK : COLOR_NAME_CONNECTED_LIGHT;
3526
+ invertBackground = false;
4732
3527
  icons.push(React.createElement(IconConnection, { key: "conn",
4733
3528
  // title={this.texts.deviceError}
4734
3529
  style: this.styles.iconDeviceConnected }));
@@ -4760,11 +3555,11 @@ export class ObjectBrowserClass extends Component {
4760
3555
  }
4761
3556
  }
4762
3557
  const narrowStyleWithDetails = this.props.width === 'xs' && this.state.focused === id;
4763
- const colID = (React.createElement(Grid2, { container: true, wrap: "nowrap", direction: "row", sx: styles.cellId, style: { width: this.columnsVisibility.id, paddingLeft } },
4764
- React.createElement(Grid2, { container: true, alignItems: "center" },
3558
+ const colID = (React.createElement(Grid, { container: true, wrap: "nowrap", direction: "row", sx: styles.cellId, style: { width: this.columnsVisibility.id, paddingLeft } },
3559
+ React.createElement(Grid, { container: true, sx: { alignItems: 'center' } },
4765
3560
  checkbox,
4766
3561
  iconFolder),
4767
- React.createElement(Grid2, { style: {
3562
+ React.createElement(Grid, { style: {
4768
3563
  ...styles.cellIdSpan,
4769
3564
  ...(invertBackground ? this.styles.invertedBackground : undefined),
4770
3565
  color: checkColor,
@@ -4775,7 +3570,7 @@ export class ObjectBrowserClass extends Component {
4775
3570
  alias,
4776
3571
  icons),
4777
3572
  React.createElement("div", { style: { ...styles.grow, ...(invertBackground ? this.styles.invertedBackgroundFlex : {}) } }),
4778
- React.createElement(Grid2, { container: true, alignItems: "center" }, iconItem),
3573
+ React.createElement(Grid, { container: true, sx: { alignItems: 'center' } }, iconItem),
4779
3574
  this.props.width !== 'xs' ? (React.createElement("div", null,
4780
3575
  React.createElement(IconCopy, { className: narrowStyleWithDetails ? '' : 'copyButton', style: styles.cellCopyButton, onClick: e => this.onCopy(e, id) }))) : null));
4781
3576
  let colName = (narrowStyleWithDetails && name) || this.columnsVisibility.name ? (React.createElement(Box, { component: "div", sx: {
@@ -4929,7 +3724,9 @@ export class ObjectBrowserClass extends Component {
4929
3724
  ...styles.cellRole,
4930
3725
  width: this.props.width !== 'xs' ? this.columnsVisibility.timestamp : undefined,
4931
3726
  } }, checkVisibleObjectType && this.states[id]?.ts
4932
- ? Utils.formatDate(new Date(this.states[id].ts), this.props.dateFormat || this.systemConfig.common.dateFormat)
3727
+ ? Utils.formatDate(new Date(this.states[id].ts), this.props.dateFormat ||
3728
+ this.systemConfig?.common.dateFormat ||
3729
+ DEFAULT_DATE_FORMAT)
4933
3730
  : null)),
4934
3731
  type: 'ts',
4935
3732
  }
@@ -4941,7 +3738,9 @@ export class ObjectBrowserClass extends Component {
4941
3738
  ...styles.cellRole,
4942
3739
  width: this.props.width !== 'xs' ? this.columnsVisibility.lastChange : undefined,
4943
3740
  } }, checkVisibleObjectType && this.states[id]?.lc
4944
- ? Utils.formatDate(new Date(this.states[id].lc), this.props.dateFormat || this.systemConfig.common.dateFormat)
3741
+ ? Utils.formatDate(new Date(this.states[id].lc), this.props.dateFormat ||
3742
+ this.systemConfig?.common.dateFormat ||
3743
+ DEFAULT_DATE_FORMAT)
4945
3744
  : null)),
4946
3745
  type: 'lc',
4947
3746
  }
@@ -5081,9 +3880,11 @@ export class ObjectBrowserClass extends Component {
5081
3880
  state: this.states[id],
5082
3881
  obj: this.objects[id],
5083
3882
  texts: this.texts,
5084
- dateFormat: this.props.dateFormat || this.systemConfig.common.dateFormat,
3883
+ dateFormat: this.props.dateFormat ||
3884
+ this.systemConfig?.common.dateFormat ||
3885
+ DEFAULT_DATE_FORMAT,
5085
3886
  isFloatComma: this.props.isFloatComma === undefined
5086
- ? this.systemConfig.common.isFloatComma
3887
+ ? (this.systemConfig?.common.isFloatComma ?? true)
5087
3888
  : this.props.isFloatComma,
5088
3889
  });
5089
3890
  this.onCopy(e, valText.c !== undefined ? valText.c : valText.v.toString());
@@ -5095,7 +3896,7 @@ export class ObjectBrowserClass extends Component {
5095
3896
  colValue = null;
5096
3897
  colButtons = null;
5097
3898
  }
5098
- const row = (React.createElement(Grid2, { container: true, direction: "row", wrap: "nowrap", sx: Utils.getStyle(this.props.theme, styles.tableRow, this.state.linesEnabled && styles.tableRowLines, !this.props.dragEnabled && styles.tableRowNoDragging, alias && styles.tableRowAlias, readWriteAlias && styles.tableRowAliasReadWrite, this.state.focused === id && this.props.multiSelect && styles.tableRowFocused, !item.data.visible && styles.filteredOut, item.data.hasVisibleParent &&
3899
+ const row = (React.createElement(Grid, { container: true, direction: "row", wrap: "nowrap", sx: Utils.getStyle(this.props.theme, styles.tableRow, this.state.linesEnabled && styles.tableRowLines, !this.props.dragEnabled && styles.tableRowNoDragging, alias && styles.tableRowAlias, readWriteAlias && styles.tableRowAliasReadWrite, this.state.focused === id && this.props.multiSelect && styles.tableRowFocused, !item.data.visible && styles.filteredOut, item.data.hasVisibleParent &&
5099
3900
  !item.data.visible &&
5100
3901
  !item.data.hasVisibleChildren &&
5101
3902
  styles.filteredParentOut, this.state.selected.includes(id) && styles.itemSelected, this.state.selectedNonObject === id && styles.itemSelected), key: id, id: id, onMouseDown: e => {
@@ -5635,10 +4436,109 @@ export class ObjectBrowserClass extends Component {
5635
4436
  return (React.createElement(Snackbar, { open: !!this.state.toast, autoHideDuration: 3000, onClick: () => this.setState({ toast: '' }), onClose: () => this.setState({ toast: '' }), message: this.state.toast, action: React.createElement(IconButton, { size: "small", "aria-label": "close", color: "inherit", onClick: () => this.setState({ toast: '' }) },
5636
4437
  React.createElement(IconClose, { fontSize: "small" })) }));
5637
4438
  }
4439
+ // --- Routing (navigateTo / onNavigateTo) ---
4440
+ // The browser never reads the URL itself; the parent drives it via `navigateTo` and is informed
4441
+ // of user navigation via `onNavigateTo`. All URL parsing/writing lives in the parent component.
4442
+ /** Derive the current navigation target from the dialog/selection state. */
4443
+ getStateNav() {
4444
+ if (this.state.editObjectDialog) {
4445
+ return { mode: 'edit', id: this.state.editObjectDialog };
4446
+ }
4447
+ if (this.state.customDialog && this.state.customDialog.length === 1 && !this.state.customDialogAll) {
4448
+ return { mode: 'settings', id: this.state.customDialog[0] };
4449
+ }
4450
+ if (this.state.viewFileDialog) {
4451
+ return { mode: 'viewFile', id: this.state.viewFileDialog };
4452
+ }
4453
+ if (this.state.selected.length === 1 && this.state.selected[0]) {
4454
+ return { mode: 'select', id: this.state.selected[0] };
4455
+ }
4456
+ return null;
4457
+ }
4458
+ static navEqual(a, b) {
4459
+ if (!a || !b) {
4460
+ return !a && !b;
4461
+ }
4462
+ return a.mode === b.mode && a.id === b.id;
4463
+ }
4464
+ /** Apply a navigation target coming from the parent (`navigateTo`): select + open the dialog. */
4465
+ applyNavigateTo(nav) {
4466
+ this.applyingNav = true;
4467
+ const done = () => {
4468
+ this.applyingNav = false;
4469
+ };
4470
+ if (!nav?.id) {
4471
+ // No target: just close any open dialog (keep the current selection).
4472
+ this.setState({ editObjectDialog: '', customDialog: null, viewFileDialog: '' }, done);
4473
+ return;
4474
+ }
4475
+ const { id } = nav;
4476
+ const open = () => {
4477
+ if (nav.mode === 'edit') {
4478
+ this.setState({ editObjectDialog: id, editObjectAlias: false, customDialog: null, viewFileDialog: '' }, done);
4479
+ }
4480
+ else if (nav.mode === 'settings') {
4481
+ this.setState({ customDialog: [id], customDialogAll: false, editObjectDialog: '', viewFileDialog: '' }, done);
4482
+ }
4483
+ else if (nav.mode === 'viewFile') {
4484
+ this.setState({ viewFileDialog: id, editObjectDialog: '', customDialog: null }, done);
4485
+ }
4486
+ else {
4487
+ this.setState({ editObjectDialog: '', customDialog: null, viewFileDialog: '' }, done);
4488
+ }
4489
+ };
4490
+ // Select the target (if needed), expand to it and scroll into view, then open the dialog.
4491
+ if (this.state.selected.length === 1 && this.state.selected[0] === id) {
4492
+ open();
4493
+ }
4494
+ else {
4495
+ this.onSelect(id, false, () => this.expandAllSelected(() => {
4496
+ this.scrollToItem(id);
4497
+ open();
4498
+ }));
4499
+ }
4500
+ }
4501
+ /** Apply the initial `navigateTo` after the tree has loaded (called from componentDidMount). */
4502
+ applyInitialNavigateTo() {
4503
+ const nav = this.props.navigateTo ?? null;
4504
+ if (nav?.id) {
4505
+ this.lastNav = nav;
4506
+ this.applyNavigateTo(nav);
4507
+ }
4508
+ else {
4509
+ // Don't push the restored-from-localStorage selection into the URL on load.
4510
+ this.lastNav = this.getStateNav();
4511
+ }
4512
+ }
4513
+ /** Reconcile `navigateTo` (parent/URL) with the browser's selection/dialog state. */
4514
+ reconcileNavigation(prevProps) {
4515
+ if (this.props.navigateTo === undefined && !this.props.onNavigateTo) {
4516
+ return; // routing not used by this consumer
4517
+ }
4518
+ if (this.applyingNav) {
4519
+ return; // currently applying a target; ignore the intermediate state
4520
+ }
4521
+ const propNav = this.props.navigateTo ?? null;
4522
+ const stateNav = this.getStateNav();
4523
+ if (ObjectBrowserClass.navEqual(propNav, stateNav)) {
4524
+ this.lastNav = stateNav;
4525
+ return;
4526
+ }
4527
+ if (!ObjectBrowserClass.navEqual(propNav, prevProps.navigateTo ?? null)) {
4528
+ // The parent (URL) drove a change → apply it to the browser.
4529
+ this.lastNav = propNav;
4530
+ this.applyNavigateTo(propNav);
4531
+ }
4532
+ else if (!ObjectBrowserClass.navEqual(stateNav, this.lastNav)) {
4533
+ // The user changed selection/dialog → report it so the parent can update the URL.
4534
+ this.lastNav = stateNav;
4535
+ this.props.onNavigateTo?.(stateNav);
4536
+ }
4537
+ }
5638
4538
  /**
5639
4539
  * Called when component is updated.
5640
4540
  */
5641
- componentDidUpdate() {
4541
+ componentDidUpdate(prevProps) {
5642
4542
  if (this.tableRef.current) {
5643
4543
  const scrollBarWidth = this.tableRef.current.offsetWidth - this.tableRef.current.clientWidth;
5644
4544
  if (this.state.scrollBarWidth !== scrollBarWidth) {
@@ -5648,6 +4548,7 @@ export class ObjectBrowserClass extends Component {
5648
4548
  this.scrollToItem(this.selectFirst);
5649
4549
  }
5650
4550
  }
4551
+ this.reconcileNavigation(prevProps);
5651
4552
  }
5652
4553
  scrollToItem(id) {
5653
4554
  this.selectFirst = '';
@@ -5659,10 +4560,10 @@ export class ObjectBrowserClass extends Component {
5659
4560
  });
5660
4561
  }
5661
4562
  renderCustomDialog() {
5662
- if (this.state.customDialog && this.props.objectCustomDialog) {
4563
+ if (this.state.customDialog && this.props.objectCustomDialog && this.systemConfig) {
5663
4564
  const ObjectCustomDialog = this.props.objectCustomDialog;
5664
4565
  return (React.createElement(ObjectCustomDialog, { reportChangedIds: (changedIds) => (this.changedIds = [...changedIds]), objectIDs: this.state.customDialog, allVisibleObjects: !!this.state.customDialogAll, expertMode: this.state.filter.expertMode, isFloatComma: this.props.isFloatComma === undefined
5665
- ? this.systemConfig.common.isFloatComma
4566
+ ? (this.systemConfig?.common.isFloatComma ?? true)
5666
4567
  : this.props.isFloatComma, t: this.props.t, lang: this.props.lang, socket: this.props.socket, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, objects: this.objects, customsInstances: this.info.customs, onClose: () => {
5667
4568
  this.pauseSubscribe(false);
5668
4569
  this.setState({ customDialog: null });
@@ -5690,9 +4591,14 @@ export class ObjectBrowserClass extends Component {
5690
4591
  if (!this.state.editObjectDialog || !this.props.objectBrowserEditObject) {
5691
4592
  return null;
5692
4593
  }
4594
+ // Guard against opening the editor for a node without a real object (e.g. a virtual folder
4595
+ // that only exists because child states share an ID prefix). Its constructor reads obj._id.
4596
+ if (!this.objects[this.state.editObjectDialog]) {
4597
+ return null;
4598
+ }
5693
4599
  const ObjectBrowserEditObject = this.props.objectBrowserEditObject;
5694
- return (React.createElement(ObjectBrowserEditObject, { key: this.state.editObjectDialog, obj: this.objects[this.state.editObjectDialog], roleArray: this.info.roles, objects: this.objects, dateFormat: this.props.dateFormat || this.systemConfig.common.dateFormat, isFloatComma: this.props.isFloatComma === undefined
5695
- ? this.systemConfig.common.isFloatComma
4600
+ return (React.createElement(ObjectBrowserEditObject, { key: this.state.editObjectDialog, obj: this.objects[this.state.editObjectDialog], roleArray: this.info.roles, objects: this.objects, dateFormat: this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT, isFloatComma: this.props.isFloatComma === undefined
4601
+ ? (this.systemConfig?.common.isFloatComma ?? true)
5696
4602
  : this.props.isFloatComma, themeType: this.props.themeType, theme: this.props.theme, socket: this.props.socket, dialogName: this.props.dialogName, aliasTab: this.state.editObjectAlias, t: this.props.t, expertMode: !!this.state.filter.expertMode, onNewObject: (obj) => this.props.socket
5697
4603
  .setObject(obj._id, obj)
5698
4604
  .then(() => this.setState({ editObjectDialog: obj._id, editObjectAlias: false }, () => this.onSelect(obj._id)))
@@ -5782,8 +4688,8 @@ export class ObjectBrowserClass extends Component {
5782
4688
  const acl = obj.acl ? (obj.type === 'state' ? obj.acl.state : obj.acl.object) : 0;
5783
4689
  const aclSystemConfig = obj.acl &&
5784
4690
  (obj.type === 'state'
5785
- ? this.systemConfig.common.defaultNewAcl.state
5786
- : this.systemConfig.common.defaultNewAcl.object);
4691
+ ? this.systemConfig?.common.defaultNewAcl.state
4692
+ : this.systemConfig?.common.defaultNewAcl.object);
5787
4693
  showACL = Number.isNaN(Number(acl)) ? Number(aclSystemConfig).toString(16) : Number(acl).toString(16);
5788
4694
  }
5789
4695
  }
@@ -5814,7 +4720,7 @@ export class ObjectBrowserClass extends Component {
5814
4720
  key: '0',
5815
4721
  visibility: !!(this.props.objectBrowserEditObject &&
5816
4722
  obj &&
5817
- (this.state.filter.expertMode || ObjectBrowserClass.isNonExpertId(id))),
4723
+ (this.state.filter.expertMode || isNonExpertId(id))),
5818
4724
  icon: (React.createElement(IconEdit, { fontSize: "small", style: this.styles.contextMenuEdit })),
5819
4725
  label: this.texts.editObject,
5820
4726
  onClick: () => this.setState({ editObjectDialog: item.data.id, showContextMenu: null, editObjectAlias: false }),
@@ -5825,7 +4731,7 @@ export class ObjectBrowserClass extends Component {
5825
4731
  !this.props.notEditable &&
5826
4732
  obj &&
5827
4733
  obj.type === 'state' &&
5828
- // @ts-expect-error deprecated from js-controller 6
4734
+ // deprecated from js-controller 6
5829
4735
  obj.common?.type !== 'file' &&
5830
4736
  (this.state.filter.expertMode || obj.common.write !== false)),
5831
4737
  icon: (React.createElement(IconValueEdit, { fontSize: "small", style: this.styles.contextMenuEditValue })),
@@ -5843,7 +4749,7 @@ export class ObjectBrowserClass extends Component {
5843
4749
  VIEW: {
5844
4750
  visibility: !!this.props.objectBrowserViewFile &&
5845
4751
  obj?.type === 'state' &&
5846
- // @ts-expect-error deprecated from js-controller 6
4752
+ // deprecated from js-controller 6
5847
4753
  obj.common?.type === 'file',
5848
4754
  icon: (React.createElement(FindInPage, { fontSize: "small", style: this.styles.contextMenuView })),
5849
4755
  label: this.props.t('ra_View file'),
@@ -5855,7 +4761,7 @@ export class ObjectBrowserClass extends Component {
5855
4761
  this.info.hasSomeCustoms &&
5856
4762
  obj &&
5857
4763
  obj.type === 'state' &&
5858
- // @ts-expect-error deprecated from js-controller 6
4764
+ // deprecated from js-controller 6
5859
4765
  obj.common?.type !== 'file'),
5860
4766
  icon: (React.createElement(IconConfig, { fontSize: "small", style: item.data.hasCustoms
5861
4767
  ? this.styles.cellButtonsButtonWithCustoms
@@ -5931,7 +4837,7 @@ export class ObjectBrowserClass extends Component {
5931
4837
  this.props.objectBrowserAliasEditor &&
5932
4838
  this.props.objectBrowserEditObject &&
5933
4839
  obj?.type === 'state' &&
5934
- // @ts-expect-error deprecated from js-controller 6
4840
+ // deprecated from js-controller 6
5935
4841
  obj.common?.type !== 'file'),
5936
4842
  icon: (React.createElement(IconLink, { style: obj?.common?.alias
5937
4843
  ? this.styles.cellButtonsButtonWithCustoms
@@ -6117,8 +5023,8 @@ export class ObjectBrowserClass extends Component {
6117
5023
  : typeof this.edit.val;
6118
5024
  const role = this.objects[this.edit.id].common.role;
6119
5025
  const ObjectBrowserValue = this.props.objectBrowserValue;
6120
- return (React.createElement(ObjectBrowserValue, { t: this.props.t, lang: this.props.lang, type: type, role: role || '', states: Utils.getStates(this.objects[this.edit.id]), themeType: this.props.themeType, theme: this.props.theme, expertMode: !!this.state.filter.expertMode, value: this.edit.val, socket: this.props.socket, object: this.objects[this.edit.id], defaultHistory: this.defaultHistory, dateFormat: this.props.dateFormat || this.systemConfig.common.dateFormat, isFloatComma: this.props.isFloatComma === undefined
6121
- ? this.systemConfig.common.isFloatComma
5026
+ return (React.createElement(ObjectBrowserValue, { t: this.props.t, lang: this.props.lang, type: type, role: role || '', states: Utils.getStates(this.objects[this.edit.id]), themeType: this.props.themeType, theme: this.props.theme, expertMode: !!this.state.filter.expertMode, value: this.edit.val, socket: this.props.socket, object: this.objects[this.edit.id], defaultHistory: this.defaultHistory, dateFormat: this.props.dateFormat || this.systemConfig?.common.dateFormat || DEFAULT_DATE_FORMAT, isFloatComma: this.props.isFloatComma === undefined
5027
+ ? (this.systemConfig?.common.isFloatComma ?? true)
6122
5028
  : this.props.isFloatComma, onClose: (res) => {
6123
5029
  this.setState({ updateOpened: false });
6124
5030
  if (res) {
@@ -6126,6 +5032,25 @@ export class ObjectBrowserClass extends Component {
6126
5032
  }
6127
5033
  }, width: this.props.width }));
6128
5034
  }
5035
+ doFilter(doNotStore) {
5036
+ if (!this.objects || !this.root) {
5037
+ return;
5038
+ }
5039
+ if (!doNotStore) {
5040
+ this.localStorage.setItem(`${this.props.dialogName || 'App'}.objectFilter`, JSON.stringify(this.state.filter));
5041
+ }
5042
+ const counter = { count: 0 };
5043
+ applyFilter(this.root, this.state.filter, this.props.lang, this.objects, undefined, counter, this.props.customFilter, this.props.types);
5044
+ if (counter.count < 500 && !this.state.expandAllVisible) {
5045
+ setTimeout(() => this.setState({ expandAllVisible: true }));
5046
+ }
5047
+ else if (counter.count >= 500 && this.state.expandAllVisible) {
5048
+ setTimeout(() => this.setState({ expandAllVisible: false }));
5049
+ }
5050
+ else {
5051
+ this.forceUpdate();
5052
+ }
5053
+ }
6129
5054
  /**
6130
5055
  * The rendering method of this component.
6131
5056
  */
@@ -6159,23 +5084,14 @@ export class ObjectBrowserClass extends Component {
6159
5084
  };
6160
5085
  this.styleTheme = this.props.themeType;
6161
5086
  }
6162
- // apply filter if changed
6163
- const jsonFilter = JSON.stringify(this.state.filter);
6164
- if (this.lastAppliedFilter !== jsonFilter && this.objects && this.root) {
6165
- const counter = { count: 0 };
6166
- applyFilter(this.root, this.state.filter, this.props.lang, this.objects, undefined, counter, this.props.customFilter, this.props.types);
6167
- if (counter.count < 500 && !this.state.expandAllVisible) {
6168
- setTimeout(() => this.setState({ expandAllVisible: true }));
6169
- }
6170
- else if (counter.count >= 500 && this.state.expandAllVisible) {
6171
- setTimeout(() => this.setState({ expandAllVisible: false }));
6172
- }
6173
- this.lastAppliedFilter = jsonFilter;
6174
- }
6175
5087
  this.unsubscribeTimer = setTimeout(() => {
6176
5088
  this.unsubscribeTimer = null;
6177
5089
  this.checkUnsubscribes();
6178
5090
  }, 200);
5091
+ if (this.expertMode !== !!this.state.filter.expertMode) {
5092
+ this.expertMode = !!this.state.filter.expertMode;
5093
+ this.doFilter(true);
5094
+ }
6179
5095
  if (!this.state.loaded) {
6180
5096
  return React.createElement(CircularProgress, { key: `${this.props.dialogName}_c` });
6181
5097
  }
@@ -6234,6 +5150,7 @@ export class ObjectBrowserClass extends Component {
6234
5150
  this.renderErrorDialog(),
6235
5151
  this.renderExportDialog(),
6236
5152
  this.renderRenameDialog(),
5153
+ this.renderInputJsonDialog(),
6237
5154
  this.state.modalNewObj && this.props.modalNewObject && this.props.modalNewObject(this),
6238
5155
  this.state.modalEditOfAccess &&
6239
5156
  this.state.modalEditOfAccessObjData &&