From 24e76de3d0315f0ca8ce85d6097057168725d501 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Fri, 29 Nov 2024 00:08:24 +0000 Subject: [PATCH] New eslint config --- packages/iobroker.vis-2/admin/jsonConfig.json | 184 +- packages/iobroker.vis-2/lib/convert.js | 29 +- packages/iobroker.vis-2/lib/install.js | 67 +- packages/iobroker.vis-2/lib/states.js | 48 +- packages/iobroker.vis-2/lib/updating.html | 197 +- packages/iobroker.vis-2/src/.env | 1 - packages/iobroker.vis-2/src/.eslintignore | 2 - packages/iobroker.vis-2/src/.eslintrc.js | 102 - packages/iobroker.vis-2/src/craco.config.js | 2 +- packages/iobroker.vis-2/src/package.json | 44 +- .../iobroker.vis-2/src/src/Attributes/CSS.tsx | 139 +- .../src/src/Attributes/Scripts.tsx | 24 +- .../src/src/Attributes/View.tsx | 460 ++-- .../src/Attributes/View/AllViewsDialog.tsx | 303 +-- .../src/Attributes/View/ApplyProperties.tsx | 71 +- .../src/src/Attributes/View/EditField.tsx | 436 ++-- .../src/Attributes/View/EditFieldIcon64.tsx | 101 +- .../src/Attributes/View/EditFieldImage.tsx | 214 +- .../src/src/Attributes/View/Items.tsx | 1253 +++++----- .../src/src/Attributes/Widget/TextDialog.tsx | 42 +- .../Attributes/Widget/WidgetBindingField.tsx | 1691 ++++++++------ .../src/src/Attributes/Widget/WidgetCSS.tsx | 76 +- .../src/src/Attributes/Widget/WidgetField.tsx | 2022 +++++++++-------- .../src/src/Attributes/Widget/WidgetJS.tsx | 76 +- .../src/src/Attributes/Widget/index.tsx | 1571 ++++++++----- .../src/src/Attributes/index.tsx | 206 +- .../src/src/Components/CodeDialog.tsx | 87 +- .../Components/CreateFirstProjectDialog.tsx | 20 +- .../src/src/Components/CustomAceEditor.tsx | 58 +- .../src/src/Components/IOContextMenu.tsx | 130 +- .../src/src/Components/IODialog.tsx | 104 +- .../src/Components/MaterialIconSelector.tsx | 545 +++-- .../src/src/Components/UploadFile.tsx | 181 +- .../src/src/Components/WizardHelpers.tsx | 78 +- packages/iobroker.vis-2/src/src/Editor.tsx | 1745 ++++++++------ .../src/src/Marketplace/MarketplaceDialog.tsx | 76 +- .../src/Marketplace/MarketplacePalette.tsx | 20 +- .../iobroker.vis-2/src/src/Palette/Widget.tsx | 213 +- .../iobroker.vis-2/src/src/Palette/index.tsx | 593 +++-- packages/iobroker.vis-2/src/src/Runtime.tsx | 732 +++--- packages/iobroker.vis-2/src/src/Store.tsx | 132 +- .../src/src/Toolbar/MultiSelect.tsx | 256 ++- .../src/src/Toolbar/Projects.tsx | 217 +- .../ProjectsManager/ImportProjectDialog.tsx | 182 +- .../ProjectsManager/PermissionsDialog.tsx | 505 ++-- .../Toolbar/ProjectsManager/ProjectDialog.tsx | 67 +- .../src/src/Toolbar/ProjectsManager/index.tsx | 427 ++-- .../src/src/Toolbar/Settings.tsx | 381 ++-- .../src/src/Toolbar/ToolbarItems.tsx | 271 ++- .../iobroker.vis-2/src/src/Toolbar/Views.tsx | 147 +- .../src/Toolbar/ViewsManager/ExportDialog.tsx | 36 +- .../src/src/Toolbar/ViewsManager/Folder.tsx | 359 +-- .../src/Toolbar/ViewsManager/FolderDialog.tsx | 82 +- .../src/Toolbar/ViewsManager/ImportDialog.tsx | 56 +- .../src/src/Toolbar/ViewsManager/Root.tsx | 57 +- .../src/src/Toolbar/ViewsManager/View.tsx | 245 +- .../src/Toolbar/ViewsManager/ViewDialog.tsx | 87 +- .../src/src/Toolbar/ViewsManager/index.tsx | 394 ++-- .../src/src/Toolbar/WidgetExportDialog.tsx | 47 +- .../src/src/Toolbar/WidgetFilterDialog.tsx | 279 +-- .../src/src/Toolbar/WidgetImportDialog.tsx | 129 +- .../src/src/Toolbar/Widgets.tsx | 518 +++-- .../iobroker.vis-2/src/src/Toolbar/index.tsx | 453 ++-- packages/iobroker.vis-2/src/src/Utils.tsx | 24 +- .../iobroker.vis-2/src/src/Utils/styles.tsx | 8 +- .../src/src/Utils/usePrevious.tsx | 5 +- .../iobroker.vis-2/src/src/Utils/utils.tsx | 43 +- .../src/src/Vis/Widgets/Basic/BasicBar.tsx | 133 +- .../src/src/Vis/Widgets/Basic/BasicBulb.tsx | 207 +- .../Vis/Widgets/Basic/BasicFilterDropdown.tsx | 613 ++--- .../src/src/Vis/Widgets/Basic/BasicGroup.tsx | 239 +- .../src/src/Vis/Widgets/Basic/BasicHtml.tsx | 76 +- .../src/Vis/Widgets/Basic/BasicHtmlNav.tsx | 73 +- .../src/src/Vis/Widgets/Basic/BasicIFrame.tsx | 174 +- .../src/Vis/Widgets/Basic/BasicIFrame8.tsx | 212 +- .../src/src/Vis/Widgets/Basic/BasicImage.tsx | 83 +- .../src/src/Vis/Widgets/Basic/BasicImage8.tsx | 145 +- .../Vis/Widgets/Basic/BasicImageGeneric.tsx | 80 +- .../src/src/Vis/Widgets/Basic/BasicLink.tsx | 84 +- .../src/Vis/Widgets/Basic/BasicRedNumber.tsx | 251 +- .../Widgets/Basic/BasicScreenResolution.tsx | 125 +- .../Vis/Widgets/Basic/BasicSpeechToText.tsx | 359 +-- .../src/Vis/Widgets/Basic/BasicSvgBool.tsx | 131 +- .../src/Vis/Widgets/Basic/BasicSvgShape.tsx | 325 +-- .../src/Vis/Widgets/Basic/BasicValueInput.tsx | 287 +-- .../Vis/Widgets/Basic/BasicValueString.tsx | 113 +- .../Vis/Widgets/Basic/BasicViewInWidget.tsx | 230 +- .../Vis/Widgets/Basic/BasicViewInWidget8.tsx | 38 +- .../Vis/Widgets/Basic/FiltersEditorDialog.tsx | 758 +++--- .../src/src/Vis/Widgets/JQui/BulkEditor.tsx | 1247 +++++----- .../src/Vis/Widgets/JQui/JQuiBinaryState.tsx | 495 ++-- .../src/src/Vis/Widgets/JQui/JQuiButton.tsx | 570 +++-- .../src/Vis/Widgets/JQui/JQuiButtonBlank.tsx | 7 +- .../Widgets/JQui/JQuiButtonDialogClose.tsx | 257 ++- .../Vis/Widgets/JQui/JQuiButtonNavigation.tsx | 8 +- .../JQui/JQuiButtonPasswordNavigation.tsx | 8 +- .../JQui/JQuiContainerButtonDialog.tsx | 7 +- .../Vis/Widgets/JQui/JQuiContainerDialog.tsx | 2 +- .../Widgets/JQui/JQuiContainerIconDialog.jsx | 5 +- .../src/src/Vis/Widgets/JQui/JQuiDialog.jsx | 2 +- .../Vis/Widgets/JQui/JQuiDialogExternal.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiIconDialog.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiIconHttpGet.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiIconInc.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiIconLink.jsx | 2 +- .../Vis/Widgets/JQui/JQuiIconNavigation.jsx | 2 +- .../Vis/Widgets/JQui/JQuiIconStateBool.jsx | 2 +- .../Widgets/JQui/JQuiIconStatePushButton.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiInput.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiInputDate.tsx | 78 +- .../Vis/Widgets/JQui/JQuiInputDateTime.tsx | 74 +- .../src/src/Vis/Widgets/JQui/JQuiInputSet.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiRadio.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiRadioList.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiRadioSteps.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiSelectList.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiSlider.jsx | 2 +- .../Vis/Widgets/JQui/JQuiSliderVertical.jsx | 2 +- .../src/src/Vis/Widgets/JQui/JQuiState.jsx | 485 ++-- .../src/src/Vis/Widgets/JQui/JQuiToggle.jsx | 2 +- .../src/Vis/Widgets/JQui/JQuiWriteState.tsx | 2 +- .../src/Vis/Widgets/Swipe/InstallSwipe.tsx | 42 +- .../src/src/Vis/Widgets/Swipe/Swipe.tsx | 88 +- .../src/Vis/Widgets/Tabs/TabsSliderTabs.tsx | 134 +- .../Widgets/Utils/DangerousHtmlWithScript.tsx | 14 +- .../src/src/Vis/Widgets/index.tsx | 1 - .../src/src/Vis/css/backgrounds.css | 1213 ++++++++-- .../iobroker.vis-2/src/src/Vis/css/vis.css | 31 +- .../src/src/Vis/visBaseWidget.tsx | 907 +++++--- .../src/src/Vis/visCanWidget.tsx | 369 +-- .../src/src/Vis/visContextMenu.tsx | 585 ++--- .../iobroker.vis-2/src/src/Vis/visEngine.tsx | 1057 +++++---- .../src/src/Vis/visFormatUtils.tsx | 93 +- .../src/src/Vis/visLoadWidgets.tsx | 317 +-- .../src/src/Vis/visNavigation.tsx | 538 +++-- .../src/src/Vis/visOrderMenu.tsx | 169 +- .../src/src/Vis/visRxWidget.tsx | 483 ++-- .../iobroker.vis-2/src/src/Vis/visUtils.tsx | 99 +- .../iobroker.vis-2/src/src/Vis/visView.tsx | 1442 +++++++----- .../src/src/Vis/visWidgetsCatalog.tsx | 454 ++-- .../iobroker.vis-2/src/src/Vis/visWords.tsx | 46 +- packages/iobroker.vis-2/src/src/bootstrap.tsx | 39 +- packages/iobroker.vis-2/src/src/index.css | 84 +- packages/iobroker.vis-2/src/src/index.tsx | 2 +- .../iobroker.vis-2/src/src/serviceWorker.jsx | 41 +- packages/iobroker.vis-2/src/src/theme.tsx | 5 +- packages/iobroker.vis-2/src/src/version.json | 2 +- packages/iobroker.vis-2/src/tsconfig.json | 51 +- packages/types-vis-2/index.d.ts | 1 + 149 files changed, 20537 insertions(+), 15137 deletions(-) delete mode 100644 packages/iobroker.vis-2/src/.env delete mode 100644 packages/iobroker.vis-2/src/.eslintignore delete mode 100644 packages/iobroker.vis-2/src/.eslintrc.js diff --git a/packages/iobroker.vis-2/admin/jsonConfig.json b/packages/iobroker.vis-2/admin/jsonConfig.json index bdab4ed83..ab175f612 100644 --- a/packages/iobroker.vis-2/admin/jsonConfig.json +++ b/packages/iobroker.vis-2/admin/jsonConfig.json @@ -1,98 +1,98 @@ { - "type": "tabs", - "i18n": true, - "items": { - "license": { - "label": "License", - "type": "panel", - "items": { - "useLicenseManager": { - "type": "checkbox", - "label": "Use license manager", - "sm": 12 - }, + "type": "tabs", + "i18n": true, + "items": { "license": { - "newLine": true, - "type": "text", - "minRows": 4, - "sm": 12, - "label": "License", - "hidden": "!!data.useLicenseManager" - }, - "_uuid": { - "newLine": true, - "type": "uuid", - "sm": 12, - "md": 6, - "lg": 4 - }, - "_checkLicense": { - "newLine": true, - "type": "checkLicense", - "uuid": true, - "version": true - }, - "_help": { - "newLine": true, - "type": "staticText", - "sm": 12, - "md": 6, - "lg": 4, - "text": "instruction" - }, - "doNotShowProjectDialog": { - "newLine": true, - "type": "checkbox", - "label": "Do not show project dialog if URL is wrong", - "sm": 12 - }, - "forceBuild": { - "newLine": true, - "type": "checkbox", - "label": "Force pages rebuild", - "sm": 12 - } - } - }, - "loading": { - "label": "Loading background", - "type": "panel", - "items": { - "loadingBackgroundColor": { - "type": "color", - "sm": 12, - "md": 8, - "lg": 4, - "label": "Background color of the loading screen" - }, - "loadingHideLogo": { - "newLine": true, - "sm": 12, - "md": 8, - "lg": 4, - "type": "checkbox", - "label": "Hide logo" - }, - "loadingBackgroundImage": { - "newLine": true, - "sm": 12, - "md": 8, - "lg": 4, - "type": "checkbox", - "label": "Use background image" + "label": "License", + "type": "panel", + "items": { + "useLicenseManager": { + "type": "checkbox", + "label": "Use license manager", + "sm": 12 + }, + "license": { + "newLine": true, + "type": "text", + "minRows": 4, + "sm": 12, + "label": "License", + "hidden": "!!data.useLicenseManager" + }, + "_uuid": { + "newLine": true, + "type": "uuid", + "sm": 12, + "md": 6, + "lg": 4 + }, + "_checkLicense": { + "newLine": true, + "type": "checkLicense", + "uuid": true, + "version": true + }, + "_help": { + "newLine": true, + "type": "staticText", + "sm": 12, + "md": 6, + "lg": 4, + "text": "instruction" + }, + "doNotShowProjectDialog": { + "newLine": true, + "type": "checkbox", + "label": "Do not show project dialog if URL is wrong", + "sm": 12 + }, + "forceBuild": { + "newLine": true, + "type": "checkbox", + "label": "Force pages rebuild", + "sm": 12 + } + } }, - "loading-bg.png": { - "newLine": true, - "type": "image", - "hidden": "!data.loadingBackgroundImage", - "sm": 12, - "md": 8, - "lg": 4, - "accept": "image/png", - "label": "Upload image", - "crop": true + "loading": { + "label": "Loading background", + "type": "panel", + "items": { + "loadingBackgroundColor": { + "type": "color", + "sm": 12, + "md": 8, + "lg": 4, + "label": "Background color of the loading screen" + }, + "loadingHideLogo": { + "newLine": true, + "sm": 12, + "md": 8, + "lg": 4, + "type": "checkbox", + "label": "Hide logo" + }, + "loadingBackgroundImage": { + "newLine": true, + "sm": 12, + "md": 8, + "lg": 4, + "type": "checkbox", + "label": "Use background image" + }, + "loading-bg.png": { + "newLine": true, + "type": "image", + "hidden": "!data.loadingBackgroundImage", + "sm": 12, + "md": 8, + "lg": 4, + "accept": "image/png", + "label": "Upload image", + "crop": true + } + } } - } } - } } diff --git a/packages/iobroker.vis-2/lib/convert.js b/packages/iobroker.vis-2/lib/convert.js index a757358cd..ff4d9f667 100644 --- a/packages/iobroker.vis-2/lib/convert.js +++ b/packages/iobroker.vis-2/lib/convert.js @@ -10,15 +10,16 @@ function stringify(name, data, isConvert, files) { for (let mm = 0; mm < m.length; mm++) { let fn = m[mm].substring(5); // remove ": "/ const originalFileName = fn.replace(/"/g, ''); // remove last " - const p = fn.split('/'); + const p = fn.split('/'); const adapter = p.shift(); // remove vis.0 or whatever const _project = p.length > 1 ? p.shift() : ''; - fn = p.length ? p.shift() : ''; // keep only one subdirectory - fn += p.length ? `/${p.join('/')}` : '';// all other subdirectories combine again + fn = p.length ? p.shift() : ''; // keep only one subdirectory + fn += p.length ? `/${p.join('/')}` : ''; // all other subdirectories combine again if (adapter !== 'vis-2.0' || _project !== project) { // add to files - if (files.indexOf(originalFileName) === -1) { // if "vis.0/dir/otherProject.png" + if (files.indexOf(originalFileName) === -1) { + // if "vis.0/dir/otherProject.png" files.push(originalFileName); } data = data.replace(m[mm], `": "/vis-2.0/${project}/${fn}`); @@ -31,15 +32,16 @@ function stringify(name, data, isConvert, files) { for (let mm = 0; mm < m.length; mm++) { let fn = m[mm].substring(7); // remove src=\"/ const originalFileName = fn.replace(/\\"/g, ''); // remove last " - const p = fn.split('/'); + const p = fn.split('/'); const adapter = p.shift(); // remove vis-2.0 or whatever const _project = p.length > 1 ? p.shift() : ''; - fn = p.length ? p.shift() : ''; // keep only one subdirectory - fn += p.length ? `/${p.join('/')}` : '';// all other subdirectories combine again + fn = p.length ? p.shift() : ''; // keep only one subdirectory + fn += p.length ? `/${p.join('/')}` : ''; // all other subdirectories combine again if (adapter !== 'vis-2.0' || _project !== project) { // add to files - if (files.indexOf(originalFileName) === -1) { // if "vis-2.0/dir/otherProject.png" + if (files.indexOf(originalFileName) === -1) { + // if "vis-2.0/dir/otherProject.png" files.push(originalFileName); } data = data.replace(m[mm], `src=\\"/vis-2.0/${project}/${fn}`); @@ -52,15 +54,16 @@ function stringify(name, data, isConvert, files) { for (let mm = 0; mm < m.length; mm++) { let fn = m[mm].substring(6); // remove src="/ const originalFileName = fn.replace(/'/g, ''); // remove last " - const p = fn.split('/'); + const p = fn.split('/'); const adapter = p.shift(); // remove vis.0 or whatever const _project = p.length > 1 ? p.shift() : ''; - fn = p.length ? p.shift() : ''; // keep only one subdirectory - fn += p.length ? `/${p.join('/')}` : '';// all other subdirectories combine again + fn = p.length ? p.shift() : ''; // keep only one subdirectory + fn += p.length ? `/${p.join('/')}` : ''; // all other subdirectories combine again if (adapter !== 'vis-2.0' || _project !== project) { // add to files - if (files.indexOf(originalFileName) === -1) { // if "vis-2.0/dir/otherProject.png" + if (files.indexOf(originalFileName) === -1) { + // if "vis-2.0/dir/otherProject.png" files.push(originalFileName); } data = data.replace(m[mm], `src='/vis-2.0/${project}/${fn}`); @@ -99,4 +102,4 @@ function parse(projectName, fileName, data, settings) { return data; } module.exports.stringify = stringify; -module.exports.parse = parse; +module.exports.parse = parse; diff --git a/packages/iobroker.vis-2/lib/install.js b/packages/iobroker.vis-2/lib/install.js index 887edfae3..63ac3303a 100644 --- a/packages/iobroker.vis-2/lib/install.js +++ b/packages/iobroker.vis-2/lib/install.js @@ -1,14 +1,10 @@ -const fs = require('node:fs'); +const fs = require('node:fs'); const path = require('node:path'); let mime; -import('mime').then(m => mime = m.default); +import('mime').then(m => (mime = m.default)); const crypto = require('node:crypto'); -const TEXT_TYPES = [ - 'application/json', - 'application/javascript', - 'image/svg+xml', -]; +const TEXT_TYPES = ['application/json', 'application/javascript', 'image/svg+xml']; function copyFileSync(source, target, forceBuild) { let targetFile = target; @@ -84,14 +80,13 @@ function copyFolderRecursiveSync(source, target, forceBuild) { // Delete all old files if (target !== path.normalize(`${__dirname}/../www/`)) { - oldFiles - .forEach(file => { - const pathName = path.join(targetFolder, file); - if (!files.includes(file) && !fs.lstatSync(pathName).isDirectory()) { - fs.unlinkSync(pathName); - changed = true; - } - }); + oldFiles.forEach(file => { + const pathName = path.join(targetFolder, file); + if (!files.includes(file) && !fs.lstatSync(pathName).isDirectory()) { + fs.unlinkSync(pathName); + changed = true; + } + }); } } @@ -104,9 +99,11 @@ function deleteFolderRecursive(dirPath) { files = fs.readdirSync(dirPath); files.forEach((file, index) => { const curPath = `${dirPath}/${file}`; - if (fs.lstatSync(curPath).isDirectory()) { // recurse + if (fs.lstatSync(curPath).isDirectory()) { + // recurse deleteFolderRecursive(curPath); - } else { // delete file + } else { + // delete file fs.unlinkSync(curPath); } }); @@ -114,16 +111,10 @@ function deleteFolderRecursive(dirPath) { } } -const generic = [ - 'basic', - 'jqplot', - 'jqui', - 'swipe', - 'tabs', -]; +const generic = ['basic', 'jqplot', 'jqui', 'swipe', 'tabs']; const widgetSetsDependencies = { - jqui: ['basic'] + jqui: ['basic'], }; function syncWidgetSets(enabledList, forceBuild) { @@ -133,11 +124,16 @@ function syncWidgetSets(enabledList, forceBuild) { // Now we have the list of widgets => copy them all to widgets directory for (let d = 0; d < enabledList.length; d++) { - let _changed = copyFolderRecursiveSync(`${enabledList[d].path}/widgets/`, path.normalize(`${__dirname}/../www/`, forceBuild)); + let _changed = copyFolderRecursiveSync( + `${enabledList[d].path}/widgets/`, + path.normalize(`${__dirname}/../www/`, forceBuild), + ); if (_changed) { filesChanged = true; } - console.log(`Check ${enabledList[d].path.replace(/\\/g, '/').split('/').pop()}... ${_changed ? 'COPIED.' : 'no changes.'}`); + console.log( + `Check ${enabledList[d].path.replace(/\\/g, '/').split('/').pop()}... ${_changed ? 'COPIED.' : 'no changes.'}`, + ); } const widgetSets = []; @@ -151,7 +147,9 @@ function syncWidgetSets(enabledList, forceBuild) { found = isGeneric; if (!found) { - found = enabledList.find(w => w.name.toLowerCase() === `iobroker.vis-${name}` || w.name.toLowerCase() === `iobroker.${name}`); + found = enabledList.find( + w => w.name.toLowerCase() === `iobroker.vis-${name}` || w.name.toLowerCase() === `iobroker.${name}`, + ); } if (!found) { @@ -164,15 +162,20 @@ function syncWidgetSets(enabledList, forceBuild) { } else { if (isGeneric) { if (widgetSetsDependencies[name] && widgetSetsDependencies[name].length) { - widgetSets.push({name, depends: widgetSetsDependencies[name]}); + widgetSets.push({ name, depends: widgetSetsDependencies[name] }); } else { widgetSets.push(name); } } else { if (found.pack && found.pack.native && found.pack.native.always) { - widgetSets.push({name, always: true}); - } else if (found.pack && found.pack.native && found.pack.native.dependencies && found.pack.native.dependencies.length) { - widgetSets.push({name, depends: found.pack.native.dependencies}); + widgetSets.push({ name, always: true }); + } else if ( + found.pack && + found.pack.native && + found.pack.native.dependencies && + found.pack.native.dependencies.length + ) { + widgetSets.push({ name, depends: found.pack.native.dependencies }); } else { widgetSets.push(name); } diff --git a/packages/iobroker.vis-2/lib/states.js b/packages/iobroker.vis-2/lib/states.js index b355387e4..398e226ab 100644 --- a/packages/iobroker.vis-2/lib/states.js +++ b/packages/iobroker.vis-2/lib/states.js @@ -94,7 +94,9 @@ function extractBinding(format) { systemOid = systemOid.substring(0, systemOid.length - 3); } let operations = null; - const isEval = visOid.match(/^[\d\w_]+:\s?[-._/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+$/u) || (!visOid.length && parts.length > 0); // (visOid.indexOf(':') !== -1) && (visOid.indexOf('::') === -1); + const isEval = + visOid.match(/^[\d\w_]+:\s?[-._/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+$/u) || + (!visOid.length && parts.length > 0); // (visOid.indexOf(':') !== -1) && (visOid.indexOf('::') === -1); if (isEval) { const xx = visOid.split(':', 2); @@ -104,11 +106,13 @@ function extractBinding(format) { operations = []; operations.push({ op: 'eval', - arg: [{ - name: xx[0], - visOid, - systemOid, - }], + arg: [ + { + name: xx[0], + visOid, + systemOid, + }, + ], }); } @@ -116,7 +120,8 @@ function extractBinding(format) { // eval construction if (isEval) { const trimmed = parts[u].trim(); - if (isIdBinding(trimmed)) { // parts[u].indexOf(':') !== -1 && parts[u].indexOf('::') === -1) { + if (isIdBinding(trimmed)) { + // parts[u].indexOf(':') !== -1 && parts[u].indexOf('::') === -1) { const argParts = trimmed.split(':', 2); let _visOid = argParts[1].trim(); let _systemOid = _visOid; @@ -159,7 +164,8 @@ function extractBinding(format) { if (parse && parse[1]) { parse[1] = parse[1].trim(); // operators requires parameter - if (parse[1] === '*' || + if ( + parse[1] === '*' || parse[1] === '+' || parse[1] === '-' || parse[1] === '/' || @@ -201,7 +207,7 @@ function extractBinding(format) { } else if (parse[1] === 'value') { // value formatting operations = operations || []; - let param = (parse[2] === undefined) ? '(2)' : (parse[2] || ''); + let param = parse[2] === undefined ? '(2)' : parse[2] || ''; param = param.trim(); param = param.substring(1, param.length - 1); operations.push({ op: parse[1], arg: param }); @@ -353,7 +359,6 @@ function getUsedObjectIDsInWidget(views, view, wid, linkContext) { // if widget is in the group => replace groupAttrX values if (widget.grouped) { - // widget.groupid = widget.groupid || getWidgetGroup(views, view, wid); if (!views[view].widgets[widget.groupid]) { @@ -452,7 +457,12 @@ function getUsedObjectIDsInWidget(views, view, wid, linkContext) { } } }); - } else if (attr !== 'oidTrueValue' && attr !== 'oidFalseValue' && data[attr] && data[attr] !== 'nothing_selected') { + } else if ( + attr !== 'oidTrueValue' && + attr !== 'oidFalseValue' && + data[attr] && + data[attr] !== 'nothing_selected' + ) { let isID = attr.match(/oid\d{0,2}$/); if (attr.startsWith('oid')) { isID = true; @@ -460,7 +470,10 @@ function getUsedObjectIDsInWidget(views, view, wid, linkContext) { isID = true; } else if (linkContext.widgetAttrInfo) { const _attr = attr.replace(/\d{0,2}$/, ''); - if (linkContext.widgetAttrInfo[_attr]?.type === 'id' && linkContext.widgetAttrInfo[_attr].noSubscribe !== true) { + if ( + linkContext.widgetAttrInfo[_attr]?.type === 'id' && + linkContext.widgetAttrInfo[_attr].noSubscribe !== true + ) { isID = true; } } @@ -686,7 +699,10 @@ function calcProject(objects, projects, instance, result, callback) { } const dps = getUsedObjectIDs(json, false); if (dps && dps.IDs) { - result.push({id: `vis-2.${instance}.datapoints.${project.file.replace(/[.\\s]/g, '_')}`, val: dps.IDs.length}); + result.push({ + id: `vis-2.${instance}.datapoints.${project.file.replace(/[.\\s]/g, '_')}`, + val: dps.IDs.length, + }); } setImmediate(calcProject, objects, projects, instance, result, callback); }); @@ -695,7 +711,7 @@ function calcProject(objects, projects, instance, result, callback) { function calcProjects(objects, states, instance, config, callback) { objects.readDir(`vis-2.${instance}`, '/', (err, projects) => { if (err || !projects || !projects.length) { - callback && callback(err || null, [{id: `vis-2.${instance}.datapoints.total`, val: 0}]); + callback && callback(err || null, [{ id: `vis-2.${instance}.datapoints.total`, val: 0 }]); } else { calcProject(objects, projects, instance, [], (err, result) => { if (result && result.length) { @@ -703,7 +719,7 @@ function calcProjects(objects, states, instance, config, callback) { for (let r = 0; r < result.length; r++) { total += result[r].val; } - result.push({id: `vis-2.${instance}.datapoints.total`, val: total}); + result.push({ id: `vis-2.${instance}.datapoints.total`, val: total }); } callback && callback(err, result); @@ -712,4 +728,4 @@ function calcProjects(objects, states, instance, config, callback) { }); } -module.exports = calcProjects; \ No newline at end of file +module.exports = calcProjects; diff --git a/packages/iobroker.vis-2/lib/updating.html b/packages/iobroker.vis-2/lib/updating.html index 3888258e7..5e9989cfd 100644 --- a/packages/iobroker.vis-2/lib/updating.html +++ b/packages/iobroker.vis-2/lib/updating.html @@ -1,100 +1,121 @@ - - - - Updating - - - - -
-

The vis is updating just now...

-
- - - - - - - - -
-
- + + + +
+

The vis is updating just now...

+
+ + + + + + + + +
+
+ diff --git a/packages/iobroker.vis-2/src/.env b/packages/iobroker.vis-2/src/.env deleted file mode 100644 index 3ccb8fff3..000000000 --- a/packages/iobroker.vis-2/src/.env +++ /dev/null @@ -1 +0,0 @@ -EXTEND_ESLINT=true \ No newline at end of file diff --git a/packages/iobroker.vis-2/src/.eslintignore b/packages/iobroker.vis-2/src/.eslintignore deleted file mode 100644 index 6f5c632b4..000000000 --- a/packages/iobroker.vis-2/src/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -src/Vis/lib/* -src/Vis/oldVis.js \ No newline at end of file diff --git a/packages/iobroker.vis-2/src/.eslintrc.js b/packages/iobroker.vis-2/src/.eslintrc.js deleted file mode 100644 index e18dcae39..000000000 --- a/packages/iobroker.vis-2/src/.eslintrc.js +++ /dev/null @@ -1,102 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - globals: { - ioBroker: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'airbnb', - // 'react-app', - 'plugin:eqeqeq-fix/recommended', - 'plugin:@typescript-eslint/recommended', - ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 'latest', - sourceType: 'module', - }, - plugins: [ - 'react', - 'only-warn', - 'react-hooks', - ], - settings: { - 'import/resolver': { - typescript: {}, // this loads /tsconfig.json to eslint - }, - }, - rules: { - 'arrow-parens': [1, 'as-needed'], - 'react/jsx-indent': 'off', - 'react/jsx-indent-props': 'off', - 'react/no-access-state-in-setstate': 'off', - 'jsx-a11y/click-events-have-key-events': 'off', - 'jsx-a11y/no-static-element-interactions': 'off', - 'no-plusplus': 'off', - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', - 'react/no-render-return-value': 'off', - 'max-len': 'off', - 'react/destructuring-assignment': 'off', - 'react/prefer-stateless-function': 'off', - 'react/self-closing-comp': 'off', - 'react/jsx-filename-extension': 'off', - 'no-nested-ternary': 'off', - 'react/no-array-index-key': 'off', - 'react/jsx-props-no-spreading': 'off', - 'react/sort-comp': 'off', - 'react/no-did-update-set-state': 'off', - 'global-require': 'off', - 'import/extensions': 'off', - 'operator-linebreak': 'off', - 'no-unused-expressions': 'off', - 'prefer-destructuring': 'off', - 'no-return-assign': 'off', - 'no-multi-spaces': 'off', - 'key-spacing': 'off', - 'no-undef': 2, - 'react/forbid-prop-types': 'off', - 'react/require-default-props': 'off', - 'import/no-extraneous-dependencies': 'off', - 'react/jsx-wrap-multilines': 'off', - 'react/jsx-closing-tag-location': 'off', - 'no-restricted-syntax': 'off', - 'guard-for-in': 'off', - // 'linebreak-style': ["error", "windows"], - 'linebreak-style': ['off'], - 'no-param-reassign': 'off', - 'no-await-in-loop': 'off', - 'no-console': ['error', { allow: ['warn', 'error', 'log'] }], - 'no-underscore-dangle': 'off', - 'no-constant-condition': 'off', - 'no-loop-func': 'off', - 'no-continue': 'off', - 'implicit-arrow-linebreak': 'off', - 'react/function-component-definition': 'off', - radix: 'off', - indent: ['error', 4, { SwitchCase: 1 }], - 'no-alert': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'semi', - requireLast: true, - }, - singleline: { - delimiter: 'semi', - requireLast: false, - }, - }, - ], - '@typescript-eslint/type-annotation-spacing': 'error', - '@typescript-eslint/consistent-type-imports': 'error', - }, -}; diff --git a/packages/iobroker.vis-2/src/craco.config.js b/packages/iobroker.vis-2/src/craco.config.js index 7613bef79..3556eb175 100644 --- a/packages/iobroker.vis-2/src/craco.config.js +++ b/packages/iobroker.vis-2/src/craco.config.js @@ -1,6 +1,6 @@ // const CracoEsbuildPlugin = require('craco-esbuild'); const { ProvidePlugin } = require('webpack'); -const path = require('path'); +const path = require('node:path'); const cracoModuleFederation = require('@iobroker/adapter-react-v5/craco-module-federation'); module.exports = { diff --git a/packages/iobroker.vis-2/src/package.json b/packages/iobroker.vis-2/src/package.json index b3bd6e337..1fb3553a2 100644 --- a/packages/iobroker.vis-2/src/package.json +++ b/packages/iobroker.vis-2/src/package.json @@ -1,24 +1,24 @@ { - "name": "vis-2-gui", - "private": true, - "dependencies": { - "@iobroker/types-vis-2": "file:../../types-vis-2" - }, - "scripts": { - "start": "craco start", - "types": "tsc ./src/Vis/visRxWidget.tsx --declaration --emitDeclarationOnly --outDir ../../types-vis-2", - "lint": "eslint --fix --ext .js,.jsx src", - "build": "craco build", - "check-ts": "tsc --noEmit --checkJS false" - }, - "eslintConfig": { - "extends": "react-app" - }, - "homepage": ".", - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ] + "name": "vis-2-gui", + "private": true, + "dependencies": { + "@iobroker/types-vis-2": "file:../../types-vis-2" + }, + "scripts": { + "start": "craco start", + "types": "tsc ./src/Vis/visRxWidget.tsx --declaration --emitDeclarationOnly --outDir ../../types-vis-2", + "lint": "eslint --fix --ext .js,.jsx src", + "build": "craco build", + "check-ts": "tsc --noEmit --checkJS false" + }, + "eslintConfig": { + "extends": "react-app" + }, + "homepage": ".", + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] } diff --git a/packages/iobroker.vis-2/src/src/Attributes/CSS.tsx b/packages/iobroker.vis-2/src/src/Attributes/CSS.tsx index 0452918a9..fb0330001 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/CSS.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/CSS.tsx @@ -1,8 +1,15 @@ import React, { useEffect, useState } from 'react'; import { - MenuItem, Select, Dialog, DialogTitle, Button, - DialogContent, DialogActions, IconButton, CircularProgress, + MenuItem, + Select, + Dialog, + DialogTitle, + Button, + DialogContent, + DialogActions, + IconButton, + CircularProgress, } from '@mui/material'; import { HelpOutline, Check as CheckIcon } from '@mui/icons-material'; @@ -22,7 +29,7 @@ interface CSSProps { editMode: boolean; } -const CSS = (props: CSSProps) => { +const CSS = (props: CSSProps): React.JSX.Element => { const [type, setType] = useState<'global' | 'local'>('global'); const [localCss, setLocalCss] = useState(''); @@ -52,9 +59,9 @@ const CSS = (props: CSSProps) => { }; useEffect(() => { - const load = async () => { + const load = async (): Promise => { try { - const commonCss = await readFile(props.socket, props.adapterId, 'vis-common-user.css') as string; + const commonCss = (await readFile(props.socket, props.adapterId, 'vis-common-user.css')) as string; setGlobalCss(commonCss); } catch (e) { if (e !== 'Not exists') { @@ -62,7 +69,11 @@ const CSS = (props: CSSProps) => { } } try { - const userCss = await readFile(props.socket, props.adapterId, `${props.projectName}/vis-user.css`) as string; + const userCss = (await readFile( + props.socket, + props.adapterId, + `${props.projectName}/vis-user.css`, + )) as string; setLocalCss(userCss); } catch (e) { if (e !== 'Not exists') { @@ -74,68 +85,78 @@ const CSS = (props: CSSProps) => { } }; - load() - .catch(e => console.error('Error loading CSS: ', e)); + load().catch(e => console.error('Error loading CSS: ', e)); }, []); - const save = (value: string, saveType: 'global' | 'local') => { + const save = (value: string, saveType: 'global' | 'local'): void => { timers[saveType].setValue(value); clearTimeout(timers[saveType].timer); - timers[saveType].setTimer(setTimeout(() => { - timers[saveType].setTimer(null); - // inform views about changed CSS - props.saveCssFile(timers[saveType].directory, timers[saveType].file, value); - }, 1000)); + timers[saveType].setTimer( + setTimeout(() => { + timers[saveType].setTimer(null); + // inform views about changed CSS + props.saveCssFile(timers[saveType].directory, timers[saveType].file, value); + }, 1000), + ); }; const value = type === 'global' ? globalCss : localCss; - return <> -
- {showHelp ? - {I18n.t('Explanation')} - - {type === 'global' ? I18n.t('help_css_global') : I18n.t('help_css_project')} - - - - - : null} - - setShowHelp(true)} size="small"> - {globalCssTimer || localCssTimer ? : null} -
- save(newValue, type)} - width="100%" - focus - height="calc(100% - 34px)" - /> - ; + {I18n.t('Explanation')} + + {type === 'global' ? I18n.t('help_css_global') : I18n.t('help_css_project')} + + + + + + ) : null} + + setShowHelp(true)} + size="small" + > + + + {globalCssTimer || localCssTimer ? : null} + + save(newValue, type)} + width="100%" + focus + height="calc(100% - 34px)" + /> + + ); }; export default CSS; diff --git a/packages/iobroker.vis-2/src/src/Attributes/Scripts.tsx b/packages/iobroker.vis-2/src/src/Attributes/Scripts.tsx index 2ff8a7f08..25436253f 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Scripts.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Scripts.tsx @@ -11,16 +11,18 @@ interface ScriptsProps { editMode: boolean; } -const Scripts = (props: ScriptsProps) => { - const project = JSON.parse(JSON.stringify(store.getState().visProject)); - project.___settings.scripts = newValue; - props.changeProject(project); - }} -/>; +const Scripts = (props: ScriptsProps): React.JSX.Element => ( + { + const project = JSON.parse(JSON.stringify(store.getState().visProject)); + project.___settings.scripts = newValue; + props.changeProject(project); + }} + /> +); export default Scripts; diff --git a/packages/iobroker.vis-2/src/src/Attributes/View.tsx b/packages/iobroker.vis-2/src/src/Attributes/View.tsx index 815c0c204..a29e0e755 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View.tsx @@ -1,28 +1,10 @@ -import React, { - useEffect, - useState, - useMemo, -} from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; -import { - Accordion, - AccordionDetails, - AccordionSummary, - Tooltip, - IconButton, Box, -} from '@mui/material'; +import { Accordion, AccordionDetails, AccordionSummary, Tooltip, IconButton, Box } from '@mui/material'; -import { - ExpandMore as ExpandMoreIcon, FormatPaint, - Info as InfoIcon, Visibility, -} from '@mui/icons-material'; +import { ExpandMore as ExpandMoreIcon, FormatPaint, Info as InfoIcon, Visibility } from '@mui/icons-material'; -import { - Utils, - I18n, - type LegacyConnection, - type ThemeType, -} from '@iobroker/adapter-react-v5'; +import { Utils, I18n, type LegacyConnection, type ThemeType } from '@iobroker/adapter-react-v5'; import { store } from '@/Store'; import type { Project, View, VisTheme } from '@iobroker/types-vis-2'; @@ -91,7 +73,7 @@ const checkFunction = ( _func = funcText; } else { // eslint-disable-next-line no-new-func - _func = (new Function('data', `return ${funcText}`)) as (dataSettings: Record) => boolean; + _func = new Function('data', `return ${funcText}`) as (dataSettings: Record) => boolean; } return _func(settings); } catch (e) { @@ -105,49 +87,57 @@ function addButton( disabled: boolean, onShowViews: () => void, onClick?: () => void, -) { - return
-
- {content} -
- {onClick ? - - onClick()} +): React.JSX.Element { + return ( +
+
+ {content} +
+ {onClick ? ( + - - - - : null} - {onShowViews ? - - onShowViews()} + + onClick()} + > + + + + + ) : null} + {onShowViews ? ( + - - - - :
} -
; + + onShowViews()} + > + + + + + ) : ( +
+ )} +
+ ); } interface ViewProps { @@ -169,29 +159,20 @@ interface ViewProps { socket: LegacyConnection; } -const ViewAttributes = (props: ViewProps) => { - const project: Project = store.getState().visProject; - if (!project?.[props.selectedView]) { - return null; - } - +const ViewAttributes = (props: ViewProps): React.JSX.Element | null => { const [triggerAllOpened, setTriggerAllOpened] = useState(0); const [triggerAllClosed, setTriggerAllClosed] = useState(0); const [showAllViewDialog, setShowAllViewDialog] = useState(null); const [showViewsDialog, setShowViewsDialog] = useState(null); - const view: View = project[props.selectedView]; + let resolutionSelect = 'none'; + const project: Project = store.getState().visProject; - let resolutionSelect = `${view.settings.sizex}x${view.settings.sizey}`; - if (!view.settings || (view.settings.sizex === undefined && view.settings.sizey === undefined)) { - resolutionSelect = 'none'; - } else if (!resolution.find(item => item.value === resolutionSelect)) { - resolutionSelect = 'user'; - } + const view: View | null = project[props.selectedView]; const fields = useMemo( - () => getFields(resolutionSelect, view, props.selectedView, props.editMode, props.changeProject), - [resolutionSelect, view.settings?.sizex, view.settings?.sizey, props.selectedView, props.editMode], + () => (view ? getFields(resolutionSelect, view, props.selectedView, props.editMode, props.changeProject) : []), + [resolutionSelect, view?.settings?.sizex, view?.settings?.sizey, props.selectedView, props.editMode], ); const [accordionOpen, setAccordionOpen] = useState>({}); @@ -202,14 +183,14 @@ const ViewAttributes = (props: ViewProps) => { if (_accordionOpen) { try { _accordionOpen = JSON.parse(accordionOpenStr || ''); - } catch (e) { + } catch { // ignore } } if (_accordionOpen) { // convert from old format Object.keys(_accordionOpen).forEach(key => { - if (_accordionOpen[key] as any === true || _accordionOpen[key] === 1) { + if ((_accordionOpen[key] as any) === true || _accordionOpen[key] === 1) { _accordionOpen[key] = 1; } else { _accordionOpen[key] = 0; @@ -220,21 +201,32 @@ const ViewAttributes = (props: ViewProps) => { }, []); useEffect(() => { - const newAccordionOpen: Record = {}; + const newAccordionOpen: Record = {}; if (props.triggerAllOpened !== triggerAllOpened) { - fields.forEach((_group, key) => newAccordionOpen[key] = 1); + fields.forEach((_group, key) => (newAccordionOpen[key] = 1)); setTriggerAllOpened(props.triggerAllOpened || 0); window.localStorage.setItem('attributesView', JSON.stringify(newAccordionOpen)); setAccordionOpen(newAccordionOpen); } if (props.triggerAllClosed !== triggerAllClosed) { - fields.forEach((_group, key) => newAccordionOpen[key] = 0); + fields.forEach((_group, key) => (newAccordionOpen[key] = 0)); setTriggerAllClosed(props.triggerAllClosed || 0); window.localStorage.setItem('attributesView', JSON.stringify(newAccordionOpen)); setAccordionOpen(newAccordionOpen); } }, [props.triggerAllOpened, props.triggerAllClosed]); + if (!project?.[props.selectedView]) { + return null; + } + + resolutionSelect = `${view.settings.sizex}x${view.settings.sizey}`; + if (!view.settings || (view.settings.sizex === undefined && view.settings.sizey === undefined)) { + resolutionSelect = 'none'; + } else if (!resolution.find(item => item.value === resolutionSelect)) { + resolutionSelect = 'user'; + } + const allOpened = !fields.find((_group, key) => accordionOpen[key] === 0 || accordionOpen[key] === 2); const allClosed = !fields.find((_group, key) => accordionOpen[key] === 1); @@ -247,7 +239,7 @@ const ViewAttributes = (props: ViewProps) => { const viewList = Object.keys(project).filter(v => v !== '___settings' && v !== props.selectedView); - const allViewDialog = renderApplyDialog({ + const allViewDialog = renderApplyDialog({ field: showAllViewDialog, viewList, project, @@ -272,149 +264,175 @@ const ViewAttributes = (props: ViewProps) => { checkFunction, }); - return
- {fields.map((group, key) => { - if (checkFunction(group.hidden, project[props.selectedView]?.settings || {})) { - return null; - } - return { - const newAccordionOpen: Record = { ...accordionOpen }; - newAccordionOpen[key] = expanded ? 1 : 2; - expanded && window.localStorage.setItem('attributesView', JSON.stringify(newAccordionOpen)); - setAccordionOpen(newAccordionOpen); + return ( +
+ {fields.map((group, key) => { + if (checkFunction(group.hidden, project[props.selectedView]?.settings || {})) { + return null; + } + return ( + { + const newAccordionOpen: Record = { ...accordionOpen }; + newAccordionOpen[key] = expanded ? 1 : 2; + expanded && window.localStorage.setItem('attributesView', JSON.stringify(newAccordionOpen)); + setAccordionOpen(newAccordionOpen); - if (!expanded) { - props.setIsAllClosed(false); - setTimeout(() => { - const _newAccordionOpen: Record = { ...accordionOpen }; - _newAccordionOpen[key] = 0; - window.localStorage.setItem('attributesView', JSON.stringify(_newAccordionOpen)); - setAccordionOpen(_newAccordionOpen); - }, 200); - } - }} - > - } - > - {I18n.t(group.label)} - - {accordionOpen[key] !== 0 ? - - - {group.fields.map((field, key2) => { - let disabled = false; - if (field.disabled !== undefined) { - if (field.disabled === true) { - disabled = true; - } else if (field.disabled === false) { - disabled = false; - } else { - disabled = !!checkFunction(field.disabled, project[props.selectedView].settings || {}); - } - } + if (!expanded) { + props.setIsAllClosed(false); + setTimeout(() => { + const _newAccordionOpen: Record = { ...accordionOpen }; + _newAccordionOpen[key] = 0; + window.localStorage.setItem('attributesView', JSON.stringify(_newAccordionOpen)); + setAccordionOpen(_newAccordionOpen); + }, 200); + } + }} + > + } + > + {I18n.t(group.label)} + + {accordionOpen[key] !== 0 ? ( + +
+ + {group.fields.map((field, key2) => { + let disabled = false; + if (field.disabled !== undefined) { + if (field.disabled === true) { + disabled = true; + } else if (field.disabled === false) { + disabled = false; + } else { + disabled = !!checkFunction( + field.disabled, + project[props.selectedView].settings || {}, + ); + } + } - let result = getEditField({ - field, - disabled, - view: props.selectedView, - editMode: props.editMode, - changeProject: props.changeProject, - userGroups: props.userGroups, - adapterName: props.adapterName, - themeType: props.themeType, - instance: props.instance, - projectName: props.projectName, - socket: props.socket, - checkFunction, - project, - theme: props.theme, - }); + let result = getEditField({ + field, + disabled, + view: props.selectedView, + editMode: props.editMode, + changeProject: props.changeProject, + userGroups: props.userGroups, + adapterName: props.adapterName, + themeType: props.themeType, + instance: props.instance, + projectName: props.projectName, + socket: props.socket, + checkFunction, + project, + theme: props.theme, + }); - if (!result) { - return null; - } + if (!result) { + return null; + } - let helpText = null; - if (field.title) { - helpText = - - ; - } + let helpText = null; + if (field.title) { + helpText = ( + + + + ); + } - // if all attributes of navigation could be applied to all views with enabled navigation - if (field.groupApply) { - // find all fields with applyToAll flag, and if any is not equal show button - const isShow = group.fields.find(_field => - _field.applyToAll && - getViewsWithDifferentValues( - project, - _field, - props.selectedView, - viewList, - checkFunction, - )); + // if all attributes of navigation could be applied to all views with enabled navigation + if (field.groupApply) { + // find all fields with applyToAll flag, and if any is not equal show button + const isShow = group.fields.find( + _field => + _field.applyToAll && + getViewsWithDifferentValues( + project, + _field, + props.selectedView, + viewList, + checkFunction, + ), + ); - result = addButton( - result, - !props.editMode || disabled, - () => setShowViewsDialog(field), - isShow && (project[props.selectedView].settings as Record)?.[field.attr] ? () => setShowAllViewDialog({ ...field, group }) : null, - ); - } else if (field.attr?.startsWith('navigation')) { - result = addButton( - result, - !props.editMode || disabled, - () => setShowViewsDialog(field), - ); - } + result = addButton( + result, + !props.editMode || disabled, + () => setShowViewsDialog(field), + isShow && + (project[props.selectedView].settings as Record)?.[ + field.attr + ] + ? () => setShowAllViewDialog({ ...field, group }) + : null, + ); + } else if (field.attr?.startsWith('navigation')) { + result = addButton(result, !props.editMode || disabled, () => + setShowViewsDialog(field), + ); + } - return - - {result} - ; - })} - -
- {I18n.t(field.label)} - {helpText} -
-
: null} -
; - })} - {allViewDialog} - {showViewsDialogElement} -
; + return ( + + + {I18n.t(field.label)} + {helpText} + + + {result} + + + ); + })} + + + + ) : null} +
+ ); + })} + {allViewDialog} + {showViewsDialogElement} +
+ ); }; export default ViewAttributes; diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/AllViewsDialog.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/AllViewsDialog.tsx index 93f1eb657..ee61c0c4b 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/AllViewsDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/AllViewsDialog.tsx @@ -1,24 +1,17 @@ import React from 'react'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; -import { - Button, - Dialog, DialogActions, - DialogContent, DialogTitle, IconButton, - Tooltip, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tooltip } from '@mui/material'; import { Close, DragHandle, FormatPaint } from '@mui/icons-material'; -import { I18n } from '@iobroker/adapter-react-v5'; -import type { LegacyConnection, ThemeType } from '@iobroker/adapter-react-v5'; +import { I18n, type LegacyConnection, type ThemeType } from '@iobroker/adapter-react-v5'; import type { Project, VisTheme } from '@iobroker/types-vis-2'; import { getViewsWithDifferentValues } from '@/Attributes/View/ApplyProperties'; import getEditField from '@/Attributes/View/EditField'; import type { Field } from '@/Attributes/View/Items'; import { deepClone } from '@/Utils/utils'; -import commonStyles from '@/Utils/styles'; const styles: { draggableItem: React.CSSProperties } = { draggableItem: { @@ -36,7 +29,10 @@ interface ShowAllViewsDialogProps { onClose: () => void; themeType: ThemeType; theme: VisTheme; - checkFunction: (funcText: boolean | string | ((settings: Record) => boolean), settings: Record) => boolean; + checkFunction: ( + funcText: boolean | string | ((settings: Record) => boolean), + settings: Record, + ) => boolean; userGroups: Record; adapterName: string; instance: number; @@ -44,145 +40,180 @@ interface ShowAllViewsDialogProps { socket: LegacyConnection; } -export default function showAllViewsDialog(props: ShowAllViewsDialogProps) { +export default function showAllViewsDialog(props: ShowAllViewsDialogProps): React.JSX.Element | null { if (!props.field) { return null; } const viewList = Object.keys(props.project).filter(v => v !== '___settings'); - const items = viewList.map(view => { - let disabled = false; - if (props.field.disabled !== undefined) { - if (props.field.disabled === true) { - disabled = true; - } else if (props.field.disabled === false) { - disabled = false; - } else { - disabled = !!props.checkFunction(props.field.disabled, props.project[view].settings || {}); + const items = viewList + .map(view => { + let disabled = false; + if (props.field.disabled !== undefined) { + if (props.field.disabled === true) { + disabled = true; + } else if (props.field.disabled === false) { + disabled = false; + } else { + disabled = !!props.checkFunction(props.field.disabled, props.project[view].settings || {}); + } } - } - const control = getEditField({ - field: props.field, - disabled, - view, - editMode: true, - changeProject: props.changeProject, - userGroups: props.userGroups, - adapterName: props.adapterName, - themeType: props.themeType, - instance: props.instance, - projectName: props.projectName, - socket: props.socket, - checkFunction: props.checkFunction, - project: props.project, - theme: props.theme, - }); - if (!control) { - return null; - } - return { control, view, order: parseInt((props.project[view].settings.navigationOrder as any as string) ?? '0') }; - }) + const control = getEditField({ + field: props.field, + disabled, + view, + editMode: true, + changeProject: props.changeProject, + userGroups: props.userGroups, + adapterName: props.adapterName, + themeType: props.themeType, + instance: props.instance, + projectName: props.projectName, + socket: props.socket, + checkFunction: props.checkFunction, + project: props.project, + theme: props.theme, + }); + if (!control) { + return null; + } + return { + control, + view, + order: parseInt((props.project[view].settings.navigationOrder as any as string) ?? '0'), + }; + }) .filter(it => it); - items.sort((prevItem, nextItem) => (prevItem.order === nextItem.order ? 0 : prevItem.order < nextItem.order ? -1 : 1)); + items.sort((prevItem, nextItem) => + prevItem.order === nextItem.order ? 0 : prevItem.order < nextItem.order ? -1 : 1, + ); const viewOrderList = items.map(item => item.view); - const applyToAllButtonVisible = props.field.applyToAll && getViewsWithDifferentValues( - props.project, - props.field, - items[0].view, - null, - props.checkFunction, - ); + const applyToAllButtonVisible = + props.field.applyToAll && + getViewsWithDifferentValues(props.project, props.field, items[0].view, null, props.checkFunction); - return - - {I18n.t(props.field.label)} - - - { - const newProject = deepClone(props.project); - // first, fill all views with navigationOrder - viewOrderList.forEach((view, index) => { - newProject[view].settings.navigationOrder = index; - }); - const index = newProject[viewOrderList[data.destination.index]].settings.navigationOrder; - newProject[viewOrderList[data.destination.index]].settings.navigationOrder = - newProject[viewOrderList[data.source.index]].settings.navigationOrder; - newProject[viewOrderList[data.source.index]].settings.navigationOrder = index; - props.changeProject(newProject); - }} - > - - {(dropProvided /* dropSnapshot */) =>
+ {I18n.t(props.field.label)} + + { + const newProject = deepClone(props.project); + // first, fill all views with navigationOrder + viewOrderList.forEach((view, index) => { + newProject[view].settings.navigationOrder = index; + }); + const index = newProject[viewOrderList[data.destination.index]].settings.navigationOrder; + newProject[viewOrderList[data.destination.index]].settings.navigationOrder = + newProject[viewOrderList[data.source.index]].settings.navigationOrder; + newProject[viewOrderList[data.source.index]].settings.navigationOrder = index; + props.changeProject(newProject); + }} + > + - {items.map((item, index) => - {(dragProvided /* dragSnapshot */) =>
( +
-
- -
-
-
- {props.project[item.view].settings.navigationTitle || item.view} -
- {props.project[item.view].settings.navigationTitle ? -
{item.view}
: null} -
-
- {item.control} -
- {applyToAllButtonVisible ? - { - const newProject: Project = deepClone(props.project) as Project; - const _viewsToChange = getViewsWithDifferentValues(props.project, props.field, item.view, null, props.checkFunction) || []; - _viewsToChange?.forEach(_view => { - (newProject[_view].settings as Record)[props.field.attr] = (newProject[item.view].settings as Record)[props.field.attr]; - }); - props.changeProject(newProject); - }} + {items.map((item, index) => ( + - - - : null} -
} - )} - {dropProvided.placeholder} -
} -
-
-
- - - -
; + {(dragProvided /* dragSnapshot */) => ( +
+
+ +
+
+
+ {props.project[item.view].settings.navigationTitle || item.view} +
+ {props.project[item.view].settings.navigationTitle ? ( +
+ {item.view} +
+ ) : null} +
+
+ {item.control} +
+ {applyToAllButtonVisible ? ( + + { + const newProject: Project = deepClone(props.project); + + const _viewsToChange = + getViewsWithDifferentValues( + props.project, + props.field, + item.view, + null, + props.checkFunction, + ) || []; + _viewsToChange?.forEach(_view => { + (newProject[_view].settings as Record)[ + props.field.attr + ] = ( + newProject[item.view].settings as Record< + string, + any + > + )[props.field.attr]; + }); + props.changeProject(newProject); + }} + > + + + + ) : null} +
+ )} + + ))} + {dropProvided.placeholder} +
+ )} + + + + + + + + ); } diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/ApplyProperties.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/ApplyProperties.tsx index 7ba0824da..fb53e6b6b 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/ApplyProperties.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/ApplyProperties.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Confirm as ConfirmDialog, I18n } from '@iobroker/adapter-react-v5'; + import type { Field, FieldGroup } from '@/Attributes/View/Items'; import type { Project } from '@iobroker/types-vis-2'; @@ -8,7 +9,10 @@ export function getViewsWithDifferentValues( field: Field, selectedView: string, views: string[], - checkFunction: (funcText: boolean | string | ((settings: Record) => boolean), settings: Record) => boolean, + checkFunction: ( + funcText: boolean | string | ((settings: Record) => boolean), + settings: Record, + ) => boolean, ): string[] | null { views = views || Object.keys(project).filter(v => v !== '___settings' && v !== selectedView); if (!views.length) { @@ -65,22 +69,17 @@ interface RenderApplyDialogProps { selectedView: string; field: ApplyField | null; changeProject: (newProject: Project) => void; - checkFunction: (funcText: boolean | string | ((settings: Record) => boolean), settings: Record) => boolean; + checkFunction: ( + funcText: boolean | string | ((settings: Record) => boolean), + settings: Record, + ) => boolean; } -export function renderApplyDialog(props: RenderApplyDialogProps) { +export function renderApplyDialog(props: RenderApplyDialogProps): React.JSX.Element | null { if (!props.field) { return null; } - const { - project, - viewList, - onClose, - selectedView, - field, - changeProject, - checkFunction, - } = props; + const { project, viewList, onClose, selectedView, field, changeProject, checkFunction } = props; const viewsToChange: string[] = []; // find all fields with applyToAll flag, and if any is not equal show button @@ -88,7 +87,8 @@ export function renderApplyDialog(props: RenderApplyDialogProps) { const cField = field.group.fields[f]; if (cField.applyToAll) { - const viewsToChangeForOne: string[] | null = getViewsWithDifferentValues(project, cField, selectedView, viewList, checkFunction) || []; + const viewsToChangeForOne: string[] | null = + getViewsWithDifferentValues(project, cField, selectedView, viewList, checkFunction) || []; viewsToChangeForOne?.forEach(_view => { if (!viewsToChange.includes(_view)) { viewsToChange.push(_view); @@ -97,26 +97,31 @@ export function renderApplyDialog(props: RenderApplyDialogProps) { } } - return { - if (result) { - const newProject = JSON.parse(JSON.stringify(project)); - for (let f = 0; f < field.group.fields.length; f++) { - const cField = field.group.fields[f]; - - if (cField.applyToAll) { - const _viewsToChange: string[] | null = getViewsWithDifferentValues(project, field, selectedView, viewList, checkFunction) || []; - _viewsToChange?.forEach(_view => { - newProject[_view].settings[cField.attr] = newProject[selectedView].settings[cField.attr]; - }); + return ( + { + if (result) { + const newProject = JSON.parse(JSON.stringify(project)); + for (let f = 0; f < field.group.fields.length; f++) { + const cField = field.group.fields[f]; + + if (cField.applyToAll) { + const _viewsToChange: string[] | null = + getViewsWithDifferentValues(project, field, selectedView, viewList, checkFunction) || + []; + _viewsToChange?.forEach(_view => { + newProject[_view].settings[cField.attr] = + newProject[selectedView].settings[cField.attr]; + }); + } } - } - changeProject(newProject); - } - onClose(); - }} - />; + changeProject(newProject); + } + onClose(); + }} + /> + ); } diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/EditField.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/EditField.tsx index 8388b7bf1..591136927 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/EditField.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/EditField.tsx @@ -2,9 +2,13 @@ import React from 'react'; import { Autocomplete, Checkbox, - IconButton, Input, ListItemText, MenuItem, + IconButton, + Input, + ListItemText, + MenuItem, Select, - type SelectChangeEvent, Slider, + type SelectChangeEvent, + Slider, TextField, } from '@mui/material'; @@ -13,10 +17,8 @@ import { I18n, IconPicker, TextWithIcon, -} from '@iobroker/adapter-react-v5'; -import type { - LegacyConnection, - ThemeType, + type LegacyConnection, + type ThemeType, } from '@iobroker/adapter-react-v5'; import { Clear as ClearIcon } from '@mui/icons-material'; @@ -35,7 +37,10 @@ interface EditFieldProps { editMode: boolean; themeType: ThemeType; theme: VisTheme; - checkFunction: (funcText: boolean | string | ((settings: Record) => boolean), settings: Record) => boolean; + checkFunction: ( + funcText: boolean | string | ((settings: Record) => boolean), + settings: Record, + ) => boolean; changeProject: (project: Project) => void; userGroups: Record; adapterName: string; @@ -71,13 +76,15 @@ export default function getEditField(gProps: EditFieldProps): React.JSX.Element } const error = checkFunction(field.error, viewSettings || {}); - const rawValue: any = field.notStyle ? (viewSettings as Record)?.[field.attr] : (viewSettings as Record)?.style[field.attr]; + const rawValue: any = field.notStyle + ? (viewSettings as Record)?.[field.attr] + : (viewSettings as Record)?.style[field.attr]; let value: any = rawValue; if (value === null || value === undefined) { value = ''; } - const change = (changeValue: boolean | number | string | null) => { + const change = (changeValue: boolean | number | string | null): void => { const newProject = deepClone(project); if (newProject[view].settings) { if (field.notStyle) { @@ -98,210 +105,261 @@ export default function getEditField(gProps: EditFieldProps): React.JSX.Element options = field.options || []; } - return change(inputValue)} - onChange={(_e, inputValue) => change(inputValue)} - sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} - renderInput={params => ( - - )} - />; + return ( + change(inputValue)} + onChange={(_e, inputValue) => change(inputValue)} + sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} + renderInput={params => ( + + )} + /> + ); } if (field.type === 'checkbox') { - return change(e.target.checked)} - />; + return ( + change(e.target.checked)} + /> + ); } if (field.type === 'select') { - return ; + {field.options?.map(selectItem => ( + + {field.itemModify + ? field.itemModify(selectItem) + : field.noTranslation + ? selectItem.label + : I18n.t(selectItem.label)} + + ))} + + ); } if (field.type === 'multi-select') { - return ; + {field.options?.map(selectItem => ( + + + + + ))} + + ); } if (field.type === 'groups') { - return ; + {Object.values(userGroups).map((_group, i) => ( + + + + + ))} + + ); } if (field.type === 'raw') { return field.Component; } if (field.type === 'color') { - return change(color)} - />; + return ( + change(color)} + /> + ); } if (field.type === 'icon') { - return change(fileBlob)} - disabled={!editMode || disabled} - // icon={ImageIcon} - // classes={classes} - />; - } - if (field.type === 'image') { - return ; - } - if (field.type === 'slider') { - return
- change(fileBlob)} disabled={!editMode || disabled} - style={commonStyles.fieldContentSlider} - size="small" - onChange={(_e, newValue) => change(Array.isArray(newValue) ? newValue[0] : newValue)} - value={typeof value === 'number' ? value : 0} - min={field.min} - max={field.max} - step={field.step} - marks={field.marks} - valueLabelDisplay={field.valueLabelDisplay} + // icon={ImageIcon} + // classes={classes} /> - change(parseFloat(e.target.value))} - sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} - inputProps={{ - step: field.step, - min: field.min, - max: field.max, - type: 'number', - }} + error={error} + editMode={editMode} + disabled={disabled} + themeType={themeType} + change={change} + adapterName={adapterName} + instance={instance} + projectName={projectName} + socket={socket} /> - change(null)}> -
; + ); + } + if (field.type === 'slider') { + return ( +
+ change(Array.isArray(newValue) ? newValue[0] : newValue)} + value={typeof value === 'number' ? value : 0} + min={field.min} + max={field.max} + step={field.step} + marks={field.marks} + valueLabelDisplay={field.valueLabelDisplay} + /> + change(parseFloat(e.target.value))} + sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} + inputProps={{ + step: field.step, + min: field.min, + max: field.max, + type: 'number', + }} + /> + change(null)} + > + + +
+ ); } if (field.type === 'icon64') { - return ; + return ( + + ); } - return change(null)}> - -
: null, - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - value={value} - onChange={e => change(e.target.value)} - type={field.type} - error={!!error} - helperText={typeof error === 'string' ? I18n.t(error) : null} - />; + return ( + change(null)} + > + + + ) : null, + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + value={value} + onChange={e => change(e.target.value)} + type={field.type} + error={!!error} + helperText={typeof error === 'string' ? I18n.t(error) : null} + /> + ); } diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldIcon64.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldIcon64.tsx index 3c8536298..f84f93d61 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldIcon64.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldIcon64.tsx @@ -1,14 +1,9 @@ import React, { useState } from 'react'; -import { - Button, IconButton, TextField, -} from '@mui/material'; +import { Button, IconButton, TextField } from '@mui/material'; import { Clear as ClearIcon } from '@mui/icons-material'; -import { - Icon, - type ThemeType, -} from '@iobroker/adapter-react-v5'; +import { Icon, type ThemeType } from '@iobroker/adapter-react-v5'; import MaterialIconSelector from '@/Components/MaterialIconSelector'; import type { VisTheme } from '@iobroker/types-vis-2'; @@ -24,48 +19,60 @@ interface EditFieldIcon64Props { theme: VisTheme; } -export default function EditFieldIcon64(props: EditFieldIcon64Props) { +export default function EditFieldIcon64(props: EditFieldIcon64Props): React.JSX.Element { const [showDialog, setShowDialog] = useState(false); - return
- props.change(e.target.value)} - InputProps={{ - endAdornment: props.value ? props.change('')} - > - - : null, - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - /> - - {showDialog && - + { - setShowDialog(false); - if (icon !== null) { - props.change(icon); - } + error={!!props.error} + disabled={!props.editMode || props.disabled} + onChange={e => props.change(e.target.value)} + InputProps={{ + endAdornment: props.value ? ( + props.change('')} + > + + + ) : null, + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, }} - />} -
; + /> + + {showDialog && ( + { + setShowDialog(false); + if (icon !== null) { + props.change(icon); + } + }} + /> + )} +
+ ); } diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldImage.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldImage.tsx index 4c217d5d8..aaa57ffe8 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldImage.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/EditFieldImage.tsx @@ -1,20 +1,14 @@ -import type { RefObject } from 'react'; -import React, { useRef, useState } from 'react'; -import { - Button, Fade, IconButton, - Paper, Popper, TextField, -} from '@mui/material'; +import React, { useRef, useState, type RefObject } from 'react'; +import { Button, Fade, IconButton, Paper, Popper, TextField } from '@mui/material'; import { Clear as ClearIcon } from '@mui/icons-material'; import { I18n, SelectFile as SelectFileDialog, -} from '@iobroker/adapter-react-v5'; -import type { - LegacyConnection, - Connection, - ThemeType, + type LegacyConnection, + type Connection, + type ThemeType, } from '@iobroker/adapter-react-v5'; import type { Field } from '@/Attributes/View/Items'; @@ -36,33 +30,50 @@ interface EditFieldImageProps { theme: VisTheme; } -export default function EditFieldImage(props: EditFieldImageProps) { +export default function EditFieldImage(props: EditFieldImageProps): React.JSX.Element { const [textDialogFocused, setTextDialogFocused] = useState(false); const [textDialogEnabled, setTextDialogEnabled] = useState(false); const [showDialog, setShowDialog] = useState(false); const textRef: RefObject = useRef(); - const urlPopper = !props.disabled ? - {({ TransitionProps }) => - - - setTextDialogEnabled(false)}> - - - - } - : null; + + + setTextDialogEnabled(false)} + > + + + + + )} + + ) : null; let showDialogControl: React.JSX.Element; if (showDialog) { @@ -73,69 +84,82 @@ export default function EditFieldImage(props: EditFieldImageProps) { } else if (_value.startsWith('_PRJ_NAME')) { _value = _value.replace('_PRJ_NAME', `../${props.adapterName}.${props.instance}/${props.projectName}/`); } - showDialogControl = setShowDialog(false)} - restrictToFolder={`${props.adapterName}.${props.instance}/${props.projectName}`} - allowNonRestricted - allowUpload - allowDownload - allowCreateFolder - allowDelete - allowView - showToolbar - imagePrefix="../" - selected={_value} - filterByType="images" - onOk={selectedOrArray => { - let selected: string | undefined | null = Array.isArray(selectedOrArray) ? selectedOrArray[0] : selectedOrArray; - const projectPrefix = `${props.adapterName}.${props.instance}/${props.projectName}/`; - if (selected?.startsWith(projectPrefix)) { - selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; - } else if (selected?.startsWith('/')) { - selected = `..${selected}`; - } else if (selected && !selected.startsWith('.')) { - selected = `../${selected}`; - } else if (!selected) { - selected = null; - } - props.change(selected); - setShowDialog(false); - }} - socket={props.socket as any as Connection} - />; + showDialogControl = ( + setShowDialog(false)} + restrictToFolder={`${props.adapterName}.${props.instance}/${props.projectName}`} + allowNonRestricted + allowUpload + allowDownload + allowCreateFolder + allowDelete + allowView + showToolbar + imagePrefix="../" + selected={_value} + filterByType="images" + onOk={selectedOrArray => { + let selected: string | undefined | null = Array.isArray(selectedOrArray) + ? selectedOrArray[0] + : selectedOrArray; + const projectPrefix = `${props.adapterName}.${props.instance}/${props.projectName}/`; + if (selected?.startsWith(projectPrefix)) { + selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; + } else if (selected?.startsWith('/')) { + selected = `..${selected}`; + } else if (selected && !selected.startsWith('.')) { + selected = `../${selected}`; + } else if (!selected) { + selected = null; + } + props.change(selected); + setShowDialog(false); + }} + socket={props.socket as any as Connection} + /> + ); } - return <> - props.change('')} size="small"> : null, - , - ], - }} - ref={textRef} - value={props.value} - onFocus={() => setTextDialogFocused(true)} - onBlur={() => setTextDialogFocused(false)} - onChange={e => props.change(e.target.value)} - /> - {urlPopper} - {showDialogControl} - ; + return ( + <> + props.change('')} + size="small" + > + + + ) : null, + , + ], + }} + ref={textRef} + value={props.value} + onFocus={() => setTextDialogFocused(true)} + onBlur={() => setTextDialogFocused(false)} + onChange={e => props.change(e.target.value)} + /> + {urlPopper} + {showDialogControl} + + ); } diff --git a/packages/iobroker.vis-2/src/src/Attributes/View/Items.tsx b/packages/iobroker.vis-2/src/src/Attributes/View/Items.tsx index 46d3f240f..38acad02b 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/View/Items.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/View/Items.tsx @@ -35,10 +35,12 @@ export interface Field { width?: number; value?: any; onChange?: (e: SelectChangeEvent) => void; - marks?: boolean | { - value: number; - label?: string; - }[]; + marks?: + | boolean + | { + value: number; + label?: string; + }[]; valueLabelDisplay?: 'on' | 'auto' | 'off'; } @@ -94,580 +96,620 @@ export function getFields( editMode: boolean, changeProject: (project: Project) => void, ): FieldGroup[] { - return [{ - label: 'CSS Common', - fields: [ - { - label: 'display', - attr: 'display', - type: 'select', - options: [ - { label: 'flex', value: 'flex' }, - { label: 'block', value: 'block' }, - ], - noTranslation: true, - title: 'For widgets with relative position', - }, - { label: 'Comment', attr: 'comment', notStyle: true }, - { label: 'CSS Class', attr: 'class', notStyle: true }, - { - label: 'Initial filter', attr: 'filterkey', notStyle: true, type: 'filter', - }, - { - label: 'Only for groups', - attr: 'group', - notStyle: true, - type: 'groups', - title: 'This view will be shown only to defined groups', - }, - { - label: 'Theme', - attr: 'theme', - notStyle: true, - type: 'select', - options: ViewTheme, - noTranslation: true, - }, - { - label: 'If user not in group', - attr: 'group_action', - notStyle: true, - type: 'select', - options: [ - { label: 'Disabled', value: 'disabled' }, - { label: 'Hide', value: 'hide' }, - ], - }, - ] as Field[], - }, - { - label: 'CSS background (background-...)', - fields: [ - { - label: 'Image', - attr: 'bg-image', - type: 'image', - hidden: 'data.useBackground || (data.style && (!!data.style.background_class || !!data.style["background-color"] || !!data.style["background-image"] || !!data.style["background-size"] || !!data.style["background-repeat"] || !!data.style["background-position"] || !!data.style["background-attachment"]))', - notStyle: true, - }, - { - label: 'Position left', - attr: 'bg-position-x', - type: 'slider', - hidden: '!data["bg-image"]', - notStyle: true, - min: -100, - max: 500, - }, - { - label: 'Position top', - attr: 'bg-position-y', - type: 'slider', - hidden: '!data["bg-image"]', - notStyle: true, - min: -100, - max: 500, - }, - { - label: 'Width', - attr: 'bg-width', - type: 'text', - hidden: '!data["bg-image"]', - notStyle: true, - }, - { - label: 'Height', - attr: 'bg-height', - type: 'text', - hidden: '!data["bg-image"]', - notStyle: true, - }, - { - label: 'Color', - attr: 'bg-color', - type: 'color', - hidden: '!data["bg-image"]', - notStyle: true, - }, - { - label: 'Background class', - type: 'select', - options: background, - attr: 'background_class', - // eslint-disable-next-line react/no-unstable-nested-components - itemModify: item => <> - - {I18n.t(item.label)} - , - renderValue: (value: string) => { - const backItem = background.find(item => item?.value === value); - return
- - {I18n.t(backItem?.label || value)} -
; - }, - hidden: '!!data["bg-image"]', - }, - { - label: 'One parameter', - type: 'checkbox', - attr: 'useBackground', - notStyle: true, - hidden: '!!data.background_class || !!data["bg-image"]', - }, - { - label: 'background', - attr: 'background', - hidden: '!data.useBackground || !!data.background_class || !!data["bg-image"]', - }, - { - label: '-color', - type: 'color', - attr: 'background-color', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - }, - { - label: '-image', - attr: 'background-image', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - }, - { - label: '-repeat', - type: 'autocomplete', - attr: 'background-repeat', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: [ - 'repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'initial', 'inherit', - ], - }, - { - label: '-attachment', - attr: 'background-attachment', - type: 'autocomplete', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: ['scroll', 'fixed', 'local', 'initial', 'inherit'], - }, - { - label: '-position', - attr: 'background-position', - type: 'autocomplete', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: ['left top', 'left center', 'left bottom', 'right top', 'right center', 'right bottom', 'center top', 'center center', 'center bottom', 'initial', 'inherit'], - }, - { - label: '-size', - attr: 'background-size', - type: 'autocomplete', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: ['auto', 'cover', 'contain', 'initial', 'inherit'], - }, - { - label: '-clip', - attr: 'background-clip', - type: 'autocomplete', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: ['border-box', 'padding-box', 'content-box', 'initial', 'inherit'], - }, - { - label: '-origin', - attr: 'background-origin', - type: 'autocomplete', - hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', - options: ['border-box', 'padding-box', 'content-box', 'initial', 'inherit'], - }, - ] as Field[], - }, - { - label: 'CSS Font & Text', - fields: [ - { label: 'color', type: 'color', attr: 'color' }, - { label: 'text-shadow', attr: 'text-shadow' }, - { label: 'font-family', attr: 'font-family' }, - { label: 'font-style', attr: 'font-style' }, - { label: 'font-variant', attr: 'font-variant' }, - { label: 'font-weight', attr: 'font-weight' }, - { label: 'font-size', attr: 'font-size' }, - { label: 'line-height', attr: 'line-height' }, - { label: 'letter-spacing', attr: 'letter-spacing' }, - { label: 'word-spacing', attr: 'word-spacing' }, - ] as Field[], - }, - { - label: 'Options', - fields: [ - { - type: 'checkbox', label: 'Default', attr: 'useAsDefault', notStyle: true, - }, - { - type: 'checkbox', label: 'Render always', attr: 'alwaysRender', notStyle: true, - }, - { - type: 'select', - label: 'Grid', - attr: 'snapType', - options: [ - { label: 'Disabled', value: 0 }, - { label: 'Elements', value: 1 }, - { label: 'Grid', value: 2 }, - ], - notStyle: true, - }, - { - type: 'color', - label: 'Grid color', - attr: 'snapColor', - hidden: 'data.snapType !== 2', - notStyle: true, - }, - { - type: 'number', - label: 'Grid size', - attr: 'gridSize', - notStyle: true, - hidden: 'data.snapType !== 2', - }, - { - type: 'select', - label: 'Resolution', - options: resolution, - width: 236, - value: resolutionSelect, - onChange: (e: React.ChangeEvent) => { - const project = JSON.parse(JSON.stringify(store.getState().visProject)); - if (e.target.value === 'none') { - delete project[selectedView].settings.sizex; - delete project[selectedView].settings.sizey; - } else if (e.target.value === 'user') { - project[selectedView].settings.sizex = project[selectedView].settings.sizex || 0; - project[selectedView].settings.sizey = project[selectedView].settings.sizey || 0; - const _resolutionSelect = `${project[selectedView].settings.sizex}x${project[selectedView].settings.sizey}`; - if (resolution.find(item => item.value === _resolutionSelect)) { - project[selectedView].settings.sizex++; - } - } else { - const match = e.target.value.match(/^([0-9]+)x([0-9]+)$/); - if (match) { - project[selectedView].settings.sizex = match[1]; - project[selectedView].settings.sizey = match[2]; + return [ + { + label: 'CSS Common', + fields: [ + { + label: 'display', + attr: 'display', + type: 'select', + options: [ + { label: 'flex', value: 'flex' }, + { label: 'block', value: 'block' }, + ], + noTranslation: true, + title: 'For widgets with relative position', + }, + { label: 'Comment', attr: 'comment', notStyle: true }, + { label: 'CSS Class', attr: 'class', notStyle: true }, + { + label: 'Initial filter', + attr: 'filterkey', + notStyle: true, + type: 'filter', + }, + { + label: 'Only for groups', + attr: 'group', + notStyle: true, + type: 'groups', + title: 'This view will be shown only to defined groups', + }, + { + label: 'Theme', + attr: 'theme', + notStyle: true, + type: 'select', + options: ViewTheme, + noTranslation: true, + }, + { + label: 'If user not in group', + attr: 'group_action', + notStyle: true, + type: 'select', + options: [ + { label: 'Disabled', value: 'disabled' }, + { label: 'Hide', value: 'hide' }, + ], + }, + ] as Field[], + }, + { + label: 'CSS background (background-...)', + fields: [ + { + label: 'Image', + attr: 'bg-image', + type: 'image', + hidden: 'data.useBackground || (data.style && (!!data.style.background_class || !!data.style["background-color"] || !!data.style["background-image"] || !!data.style["background-size"] || !!data.style["background-repeat"] || !!data.style["background-position"] || !!data.style["background-attachment"]))', + notStyle: true, + }, + { + label: 'Position left', + attr: 'bg-position-x', + type: 'slider', + hidden: '!data["bg-image"]', + notStyle: true, + min: -100, + max: 500, + }, + { + label: 'Position top', + attr: 'bg-position-y', + type: 'slider', + hidden: '!data["bg-image"]', + notStyle: true, + min: -100, + max: 500, + }, + { + label: 'Width', + attr: 'bg-width', + type: 'text', + hidden: '!data["bg-image"]', + notStyle: true, + }, + { + label: 'Height', + attr: 'bg-height', + type: 'text', + hidden: '!data["bg-image"]', + notStyle: true, + }, + { + label: 'Color', + attr: 'bg-color', + type: 'color', + hidden: '!data["bg-image"]', + notStyle: true, + }, + { + label: 'Background class', + type: 'select', + options: background, + attr: 'background_class', + // eslint-disable-next-line react/no-unstable-nested-components + itemModify: item => ( + <> + + {I18n.t(item.label)} + + ), + renderValue: (value: string) => { + const backItem = background.find(item => item?.value === value); + return ( +
+ + {I18n.t(backItem?.label || value)} +
+ ); + }, + hidden: '!!data["bg-image"]', + }, + { + label: 'One parameter', + type: 'checkbox', + attr: 'useBackground', + notStyle: true, + hidden: '!!data.background_class || !!data["bg-image"]', + }, + { + label: 'background', + attr: 'background', + hidden: '!data.useBackground || !!data.background_class || !!data["bg-image"]', + }, + { + label: '-color', + type: 'color', + attr: 'background-color', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + }, + { + label: '-image', + attr: 'background-image', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + }, + { + label: '-repeat', + type: 'autocomplete', + attr: 'background-repeat', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: ['repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'initial', 'inherit'], + }, + { + label: '-attachment', + attr: 'background-attachment', + type: 'autocomplete', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: ['scroll', 'fixed', 'local', 'initial', 'inherit'], + }, + { + label: '-position', + attr: 'background-position', + type: 'autocomplete', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: [ + 'left top', + 'left center', + 'left bottom', + 'right top', + 'right center', + 'right bottom', + 'center top', + 'center center', + 'center bottom', + 'initial', + 'inherit', + ], + }, + { + label: '-size', + attr: 'background-size', + type: 'autocomplete', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: ['auto', 'cover', 'contain', 'initial', 'inherit'], + }, + { + label: '-clip', + attr: 'background-clip', + type: 'autocomplete', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: ['border-box', 'padding-box', 'content-box', 'initial', 'inherit'], + }, + { + label: '-origin', + attr: 'background-origin', + type: 'autocomplete', + hidden: 'data.useBackground || !!data.background_class || !!data["bg-image"]', + options: ['border-box', 'padding-box', 'content-box', 'initial', 'inherit'], + }, + ] as Field[], + }, + { + label: 'CSS Font & Text', + fields: [ + { label: 'color', type: 'color', attr: 'color' }, + { label: 'text-shadow', attr: 'text-shadow' }, + { label: 'font-family', attr: 'font-family' }, + { label: 'font-style', attr: 'font-style' }, + { label: 'font-variant', attr: 'font-variant' }, + { label: 'font-weight', attr: 'font-weight' }, + { label: 'font-size', attr: 'font-size' }, + { label: 'line-height', attr: 'line-height' }, + { label: 'letter-spacing', attr: 'letter-spacing' }, + { label: 'word-spacing', attr: 'word-spacing' }, + ] as Field[], + }, + { + label: 'Options', + fields: [ + { + type: 'checkbox', + label: 'Default', + attr: 'useAsDefault', + notStyle: true, + }, + { + type: 'checkbox', + label: 'Render always', + attr: 'alwaysRender', + notStyle: true, + }, + { + type: 'select', + label: 'Grid', + attr: 'snapType', + options: [ + { label: 'Disabled', value: 0 }, + { label: 'Elements', value: 1 }, + { label: 'Grid', value: 2 }, + ], + notStyle: true, + }, + { + type: 'color', + label: 'Grid color', + attr: 'snapColor', + hidden: 'data.snapType !== 2', + notStyle: true, + }, + { + type: 'number', + label: 'Grid size', + attr: 'gridSize', + notStyle: true, + hidden: 'data.snapType !== 2', + }, + { + type: 'select', + label: 'Resolution', + options: resolution, + width: 236, + value: resolutionSelect, + onChange: (e: React.ChangeEvent) => { + const project = JSON.parse(JSON.stringify(store.getState().visProject)); + if (e.target.value === 'none') { + delete project[selectedView].settings.sizex; + delete project[selectedView].settings.sizey; + } else if (e.target.value === 'user') { + project[selectedView].settings.sizex = project[selectedView].settings.sizex || 0; + project[selectedView].settings.sizey = project[selectedView].settings.sizey || 0; + const _resolutionSelect = `${project[selectedView].settings.sizex}x${project[selectedView].settings.sizey}`; + if (resolution.find(item => item.value === _resolutionSelect)) { + project[selectedView].settings.sizex++; + } + } else { + const match = e.target.value.match(/^([0-9]+)x([0-9]+)$/); + if (match) { + project[selectedView].settings.sizex = match[1]; + project[selectedView].settings.sizey = match[2]; + } } - } - changeProject(project); + changeProject(project); + }, + notStyle: true, }, - notStyle: true, - }, - { - type: 'raw', - label: 'Width x height (px)', - hidden: 'data.sizex === undefined && data.sizey === undefined', - Component: - { - const project = JSON.parse(JSON.stringify(store.getState().visProject)); - project[selectedView].settings.sizex = e.target.value; - changeProject(project); - }} - /> - - { - const project = JSON.parse(JSON.stringify(store.getState().visProject)); - project[selectedView].settings.sizey = e.target.value; - changeProject(project); - }} - /> - , - notStyle: true, - }, - { - type: 'checkbox', - label: 'Limit screen', - attr: 'limitScreen', - hidden: 'data.sizex === undefined && data.sizey === undefined', - notStyle: true, - }, - { - type: 'text', - label: 'Limit only for instances', - attr: 'limitForInstances', - hidden: '!data.limitScreen', - title: 'Enter the browser instances divided by comma', - notStyle: true, - }, - { - type: 'checkbox', - label: 'Only for desktop', - attr: 'limitScreenDesktop', - hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', - notStyle: true, - }, - { - type: 'slider', - label: 'Limit border width', - attr: 'limitScreenBorderWidth', - min: 0, - max: 20, - hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', - notStyle: true, - }, - { - type: 'color', - label: 'Limit border color', - attr: 'limitScreenBorderColor', - hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen || !data.limitScreenBorderWidth', - notStyle: true, - }, - { - type: 'select', - label: 'Limit border style', - attr: 'limitScreenBorderStyle', - noTranslation: true, - options: [ - { label: 'solid', value: 'solid' }, - { label: 'dotted', value: 'dotted' }, - { label: 'dashed', value: 'dashed' }, - { label: 'double', value: 'double' }, - { label: 'groove', value: 'groove' }, - { label: 'ridge', value: 'ridge' }, - { label: 'inset', value: 'inset' }, - { label: 'outset', value: 'outset' }, - ], - hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen || !data.limitScreenBorderWidth', - notStyle: true, - }, - { - type: 'color', - label: 'Limit background color', - attr: 'limitScreenBackgroundColor', - hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', - notStyle: true, - }, - ] as Field[], - }, - { - label: 'Navigation', - fields: [ - { - type: 'checkbox', - label: 'Show navigation', - attr: 'navigation', - notStyle: true, - // applyToAll: true, - groupApply: true, - }, - { - type: 'text', label: 'Title', attr: 'navigationTitle', notStyle: true, hidden: '!data.navigation', - }, - { - type: 'number', label: 'Order', attr: 'navigationOrder', notStyle: true, hidden: '!data.navigation', - }, - { - // Icon for THIS page in navigation menu. It can be defined icon or image but not together - type: 'icon64', - label: 'Icon', - attr: 'navigationIcon', - notStyle: true, - hidden: '!data.navigation || data.navigationImage', - }, - { - // Image for THIS page in navigation menu. It can be defined icon or image but not together - type: 'image', - label: 'Image', - attr: 'navigationImage', - notStyle: true, - hidden: '!data.navigation || data.navigationIcon', - }, - { - // Menu orientation - type: 'select', - label: 'Orientation', - attr: 'navigationOrientation', - notStyle: true, - default: 'vertical', - hidden: '!data.navigation', - applyToAll: true, - options: [ - { value: 'vertical', label: 'Vertical' }, - { value: 'horizontal', label: 'Horizontal' }, - ], - }, - { - // By horizontal menu do not show text if icon provided - type: 'checkbox', - label: 'Only icon', - attr: 'navigationOnlyIcon', - title: 'By horizontal menu do not show text if icon provided', - notStyle: true, - default: true, - hidden: '!data.navigation || data.navigationOrientation !== "horizontal"', - applyToAll: true, - }, - { - type: 'color', - label: 'Background color', - attr: 'navigationBackground', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - type: 'color', - label: 'Background color if selected', - attr: 'navigationSelectedBackground', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - type: 'color', - label: 'Text color if selected', - attr: 'navigationSelectedColor', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - // Color of text in the upper/top corner if the header is defined - type: 'color', - label: 'Menu header text color', - attr: 'navigationHeaderTextColor', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - }, - { - type: 'color', - label: 'Text color', - attr: 'navigationColor', - notStyle: true, - hidden: '!data.navigation', - applyToAll: true, - }, - { - type: 'checkbox', - label: 'Hide menu', - attr: 'navigationHideMenu', - notStyle: true, - title: 'Show only toolbar on the top and hide menu itself', - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - type: 'color', - label: 'Chevron icon color', - attr: 'navigationChevronColor', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', - applyToAll: true, - }, - { - type: 'checkbox', - label: 'Hide after selection', - attr: 'navigationHideOnSelection', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', - applyToAll: true, - }, - { - type: 'text', - label: 'Menu header text', - attr: 'navigationHeaderText', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - type: 'checkbox', - label: 'Do not hide menu', - attr: 'navigationNoHide', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', - applyToAll: true, - }, - { - type: 'checkbox', - label: 'Show background of button', - attr: 'navigationButtonBackground', - notStyle: true, - hidden: '!data.navigation || data.navigationNoHide || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - { - type: 'text', - label: 'Menu width', - attr: 'navigationWidth', - notStyle: true, - hidden: '!data.navigation || data.navigationOrientation === "horizontal"', - applyToAll: true, - }, - ] as Field[], - }, - { - label: 'App bar', - hidden: '!!data.navigation && data.navigationOrientation === "horizontal"', - fields: [ - { - type: 'checkbox', - label: 'Show app bar', - attr: 'navigationBar', - notStyle: true, - default: true, - // applyToAll: true, - groupApply: true, - }, - { - type: 'color', - label: 'Bar color', - attr: 'navigationBarColor', - notStyle: true, - hidden: '!data.navigationBar', - applyToAll: true, - }, - { - type: 'text', - label: 'Bar text', - attr: 'navigationBarText', - notStyle: true, - hidden: '!data.navigationBar', - applyToAll: true, - }, - { - type: 'icon64', - label: 'Bar icon', - attr: 'navigationBarIcon', - notStyle: true, - hidden: '!data.navigationBar || !!data.navigationBarImage', - applyToAll: true, - }, - { - type: 'image', - label: 'Bar image', - attr: 'navigationBarImage', - notStyle: true, - hidden: '!data.navigationBar || !!data.navigationBarIcon', - applyToAll: true, - }, - ] as Field[], - }, - { - label: 'Responsive settings', - fields: [ - /* + { + type: 'raw', + label: 'Width x height (px)', + hidden: 'data.sizex === undefined && data.sizey === undefined', + Component: ( + + { + const project = JSON.parse(JSON.stringify(store.getState().visProject)); + project[selectedView].settings.sizex = e.target.value; + changeProject(project); + }} + /> + + { + const project = JSON.parse(JSON.stringify(store.getState().visProject)); + project[selectedView].settings.sizey = e.target.value; + changeProject(project); + }} + /> + + ), + notStyle: true, + }, + { + type: 'checkbox', + label: 'Limit screen', + attr: 'limitScreen', + hidden: 'data.sizex === undefined && data.sizey === undefined', + notStyle: true, + }, + { + type: 'text', + label: 'Limit only for instances', + attr: 'limitForInstances', + hidden: '!data.limitScreen', + title: 'Enter the browser instances divided by comma', + notStyle: true, + }, + { + type: 'checkbox', + label: 'Only for desktop', + attr: 'limitScreenDesktop', + hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', + notStyle: true, + }, + { + type: 'slider', + label: 'Limit border width', + attr: 'limitScreenBorderWidth', + min: 0, + max: 20, + hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', + notStyle: true, + }, + { + type: 'color', + label: 'Limit border color', + attr: 'limitScreenBorderColor', + hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen || !data.limitScreenBorderWidth', + notStyle: true, + }, + { + type: 'select', + label: 'Limit border style', + attr: 'limitScreenBorderStyle', + noTranslation: true, + options: [ + { label: 'solid', value: 'solid' }, + { label: 'dotted', value: 'dotted' }, + { label: 'dashed', value: 'dashed' }, + { label: 'double', value: 'double' }, + { label: 'groove', value: 'groove' }, + { label: 'ridge', value: 'ridge' }, + { label: 'inset', value: 'inset' }, + { label: 'outset', value: 'outset' }, + ], + hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen || !data.limitScreenBorderWidth', + notStyle: true, + }, + { + type: 'color', + label: 'Limit background color', + attr: 'limitScreenBackgroundColor', + hidden: '(data.sizex === undefined && data.sizey === undefined) || !data.limitScreen', + notStyle: true, + }, + ] as Field[], + }, + { + label: 'Navigation', + fields: [ + { + type: 'checkbox', + label: 'Show navigation', + attr: 'navigation', + notStyle: true, + // applyToAll: true, + groupApply: true, + }, + { + type: 'text', + label: 'Title', + attr: 'navigationTitle', + notStyle: true, + hidden: '!data.navigation', + }, + { + type: 'number', + label: 'Order', + attr: 'navigationOrder', + notStyle: true, + hidden: '!data.navigation', + }, + { + // Icon for THIS page in navigation menu. It can be defined icon or image but not together + type: 'icon64', + label: 'Icon', + attr: 'navigationIcon', + notStyle: true, + hidden: '!data.navigation || data.navigationImage', + }, + { + // Image for THIS page in navigation menu. It can be defined icon or image but not together + type: 'image', + label: 'Image', + attr: 'navigationImage', + notStyle: true, + hidden: '!data.navigation || data.navigationIcon', + }, + { + // Menu orientation + type: 'select', + label: 'Orientation', + attr: 'navigationOrientation', + notStyle: true, + default: 'vertical', + hidden: '!data.navigation', + applyToAll: true, + options: [ + { value: 'vertical', label: 'Vertical' }, + { value: 'horizontal', label: 'Horizontal' }, + ], + }, + { + // By horizontal menu do not show text if icon provided + type: 'checkbox', + label: 'Only icon', + attr: 'navigationOnlyIcon', + title: 'By horizontal menu do not show text if icon provided', + notStyle: true, + default: true, + hidden: '!data.navigation || data.navigationOrientation !== "horizontal"', + applyToAll: true, + }, + { + type: 'color', + label: 'Background color', + attr: 'navigationBackground', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + type: 'color', + label: 'Background color if selected', + attr: 'navigationSelectedBackground', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + type: 'color', + label: 'Text color if selected', + attr: 'navigationSelectedColor', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + // Color of text in the upper/top corner if the header is defined + type: 'color', + label: 'Menu header text color', + attr: 'navigationHeaderTextColor', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + }, + { + type: 'color', + label: 'Text color', + attr: 'navigationColor', + notStyle: true, + hidden: '!data.navigation', + applyToAll: true, + }, + { + type: 'checkbox', + label: 'Hide menu', + attr: 'navigationHideMenu', + notStyle: true, + title: 'Show only toolbar on the top and hide menu itself', + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + type: 'color', + label: 'Chevron icon color', + attr: 'navigationChevronColor', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', + applyToAll: true, + }, + { + type: 'checkbox', + label: 'Hide after selection', + attr: 'navigationHideOnSelection', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', + applyToAll: true, + }, + { + type: 'text', + label: 'Menu header text', + attr: 'navigationHeaderText', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + type: 'checkbox', + label: 'Do not hide menu', + attr: 'navigationNoHide', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal" || data.navigationHideMenu', + applyToAll: true, + }, + { + type: 'checkbox', + label: 'Show background of button', + attr: 'navigationButtonBackground', + notStyle: true, + hidden: '!data.navigation || data.navigationNoHide || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + { + type: 'text', + label: 'Menu width', + attr: 'navigationWidth', + notStyle: true, + hidden: '!data.navigation || data.navigationOrientation === "horizontal"', + applyToAll: true, + }, + ] as Field[], + }, + { + label: 'App bar', + hidden: '!!data.navigation && data.navigationOrientation === "horizontal"', + fields: [ + { + type: 'checkbox', + label: 'Show app bar', + attr: 'navigationBar', + notStyle: true, + default: true, + // applyToAll: true, + groupApply: true, + }, + { + type: 'color', + label: 'Bar color', + attr: 'navigationBarColor', + notStyle: true, + hidden: '!data.navigationBar', + applyToAll: true, + }, + { + type: 'text', + label: 'Bar text', + attr: 'navigationBarText', + notStyle: true, + hidden: '!data.navigationBar', + applyToAll: true, + }, + { + type: 'icon64', + label: 'Bar icon', + attr: 'navigationBarIcon', + notStyle: true, + hidden: '!data.navigationBar || !!data.navigationBarImage', + applyToAll: true, + }, + { + type: 'image', + label: 'Bar image', + attr: 'navigationBarImage', + notStyle: true, + hidden: '!data.navigationBar || !!data.navigationBarIcon', + applyToAll: true, + }, + ] as Field[], + }, + { + label: 'Responsive settings', + fields: [ + /* { type: 'select', label: 'Direction', @@ -731,33 +773,34 @@ export function getFields( ], }, */ - { - type: 'slider', - label: 'Column width', - attr: 'columnWidth', - min: 200, - max: 2000, - step: 10, - notStyle: true, - }, - { - type: 'slider', - label: 'Column gap', - attr: 'columnGap', - min: 0, - max: 200, - step: 1, - notStyle: true, - }, - { - type: 'slider', - label: 'Row gap', - attr: 'rowGap', - min: 0, - max: 200, - step: 1, - notStyle: true, - }, - ] as Field[], - }]; + { + type: 'slider', + label: 'Column width', + attr: 'columnWidth', + min: 200, + max: 2000, + step: 10, + notStyle: true, + }, + { + type: 'slider', + label: 'Column gap', + attr: 'columnGap', + min: 0, + max: 200, + step: 1, + notStyle: true, + }, + { + type: 'slider', + label: 'Row gap', + attr: 'rowGap', + min: 0, + max: 200, + step: 1, + notStyle: true, + }, + ] as Field[], + }, + ]; } diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/TextDialog.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/TextDialog.tsx index 300d09ea3..deec602b6 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/TextDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/TextDialog.tsx @@ -14,32 +14,34 @@ interface TextDialogProps { value: string; } -const TextDialog = (props: TextDialogProps) => { +const TextDialog = (props: TextDialogProps): React.JSX.Element => { const [value, changeValue] = useState(''); useEffect(() => { changeValue(props.value); }, [props.open]); - return props.open ? props.onChange(value)} - onClose={props.onClose} - minWidth={800} - actionDisabled={value === props.value} - > - changeValue(newValue)} - /> - : null; + return props.open ? ( + props.onChange(value)} + onClose={props.onClose} + minWidth={800} + actionDisabled={value === props.value} + > + changeValue(newValue)} + /> + + ) : null; }; export default TextDialog; diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetBindingField.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetBindingField.tsx index 34ec83e5a..b448cabbc 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetBindingField.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetBindingField.tsx @@ -13,31 +13,24 @@ import 'moment/locale/uk'; import 'moment/locale/zh-cn'; import { - Button, Checkbox, + Button, + Checkbox, Dialog, DialogActions, DialogContent, - DialogTitle, FormControlLabel, - IconButton, InputAdornment, + DialogTitle, + FormControlLabel, + IconButton, + InputAdornment, Switch, TextField, } from '@mui/material'; -import { - Cancel, Check, - Clear, Link as LinkIcon, -} from '@mui/icons-material'; +import { Cancel, Check, Clear, Link as LinkIcon } from '@mui/icons-material'; -import type { LegacyConnection, Connection } from '@iobroker/adapter-react-v5'; -import { - I18n, - SelectID, -} from '@iobroker/adapter-react-v5'; +import { I18n, SelectID, type LegacyConnection, type Connection } from '@iobroker/adapter-react-v5'; -import type { - AnyWidgetId, Project, - VisBindingOperationArgument, VisTheme, -} from '@iobroker/types-vis-2'; +import type { AnyWidgetId, Project, VisBindingOperationArgument, VisTheme } from '@iobroker/types-vis-2'; import { store, recalculateFields } from '@/Store'; @@ -175,7 +168,10 @@ class WidgetBindingField extends Component | null { + static getDerivedStateFromProps( + props: WidgetBindingFieldProps, + state: WidgetBindingFieldState, + ): Partial | null { let value = props.widget[props.isStyle ? 'style' : 'data']?.[props.field.name] || ''; if (value === undefined || value === null) { value = ''; @@ -190,7 +186,7 @@ class WidgetBindingField extends Component it.token?.includes(':')); } - async calculateValue(value: any) { + async calculateValue(value: any): Promise<{ calculatedEditValue: string; values: Record }> { this.visFormatUtils = this.visFormatUtils || new VisFormatUtils({ vis: window.vis }); if (value === undefined || value === null) { - return ''; + return { calculatedEditValue: '', values: {} }; } value = value.toString(); @@ -225,7 +221,9 @@ class WidgetBindingField extends Component { - const name = `${stateOids[i]}.${attr}`; - if (oids.find(it => it.visOid === name || it.operations?.find(op => Array.isArray(op?.arg) && op?.arg.find((arg: VisBindingOperationArgument) => arg.visOid === name)))) { - values[name] = (state as Record)[attr]; - } - }); + state && + Object.keys(state).forEach(attr => { + const name = `${stateOids[i]}.${attr}`; + if ( + oids.find( + it => + it.visOid === name || + it.operations?.find( + op => + Array.isArray(op?.arg) && + op?.arg.find((arg: VisBindingOperationArgument) => arg.visOid === name), + ), + ) + ) { + values[name] = (state as Record)[attr]; + } + }); } } @@ -277,7 +291,8 @@ class WidgetBindingField extends Component -

Special bindings

-

There are a number of different internal bindings to provide additional information in views:

-
    -
  • - this.insertInText('username')}>username - - shows logged-in user -
  • -
  • - this.insertInText('view')}>view - - name of actual view -
  • -
  • - this.insertInText('wname')}>wname - - widget name -
  • -
  • - this.insertInText('widget')}>widget - - - is an object with all data of widget. Can be used only in JS part, like - {a:a;widget.data.name} - -
  • -
  • - this.insertInText('wid')}>wid - - name of actual widget -
  • -
  • - this.insertInText('language')}>language - - can be - de - , - en - or - ru - . -
  • -
  • - this.insertInText('instance')}>instance - - browser instance -
  • -
  • - this.insertInText('login')}>login - - if login required or not (e.g., to show/hide logout button) -
  • -
  • - this.insertInText('local_')}>local_* - - - if state name is started from - local_ - it will not be reported to ioBroker but will update all widgets, that depends on this state. (Local variable for current browser session) - -
  • -
- ; + renderSpecialNames(): React.JSX.Element { + return ( +
+

Special bindings

+

There are a number of different internal bindings to provide additional information in views:

+
    +
  • + this.insertInText('username')} + > + username + + - shows logged-in user +
  • +
  • + this.insertInText('view')} + > + view + + - name of actual view +
  • +
  • + this.insertInText('wname')} + > + wname + + - widget name +
  • +
  • + this.insertInText('widget')} + > + widget + + + - is an object with all data of widget. Can be used only in JS part, like + {a:a;widget.data.name} + +
  • +
  • + this.insertInText('wid')} + > + wid + + - name of actual widget +
  • +
  • + this.insertInText('language')} + > + language + + - can be + de,en + or + ru. +
  • +
  • + this.insertInText('instance')} + > + instance + + - browser instance +
  • +
  • + this.insertInText('login')} + > + login + + - if login required or not (e.g., to show/hide logout button) +
  • +
  • + this.insertInText('local_')} + > + local_* + + + - if state name is started from + local_ + + it will not be reported to ioBroker but will update all widgets, that depends on this + state. (Local variable for current browser session) + + +
  • +
+
+ ); } - static renderHelpForNewStyle() { - return
-

Bindings of objects

-

Normally, most of the widgets have ObjectID attribute and this attribute can be bound with some value of object ID.

-

But there is another option for how to bind *any* attribute of widget to some ObjectID.

+ static renderHelpForNewStyle(): React.JSX.Element { + return ( +
+

Bindings of objects

+

+ Normally, most of the widgets have ObjectID attribute and this attribute can be bound with some + value of object ID. +

+

But there is another option for how to bind *any* attribute of widget to some ObjectID.

-

- Just write into attribute - {object.id} - and it will be bound to this object's value. -

-

If you use the special format, you can even make some simple operations with it, e.g., multiplying or formatting.

+

+ Just write into attribute + {object.id} + and it will be bound to this object's value. +

+

+ If you use the special format, you can even make some simple operations with it, e.g., multiplying + or formatting. +

-

E.g., to calculate the hypotenuse of a triangle:

+

E.g., to calculate the hypotenuse of a triangle:

-

{h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;Math.max(20, Math.sqrt(h*h + w*w))}

-

will be interpreted as function:

+

+ {h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;Math.max(20, Math.sqrt(h*h + + w*w))} +

+

will be interpreted as function:

-

- value = await (async function () { -
- var h = (await getState('javascript.0.myCustom.height')).val; -
- var w = (await getState('javascript.0.myCustom.width')).val; -
- return Math.max(20, Math.sqrt(h * h + w * w)); -
- })(); -

+

+ value = await (async function () { +
+ + var h = (await getState('javascript.0.myCustom.height')).val; + +
+ + var w = (await getState('javascript.0.myCustom.width')).val; + +
+ return Math.max(20, Math.sqrt(h * h + w * w)); +
+ })(); +

-

or

+

or

-

- {h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;h*w} - will just multiply height with width. -

+

+ + {h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;h*w} + + will just multiply height with width. +

-

You can use *any* javascript (browser) functions. Arguments must be defined with ':', if not, it will be interpreted as formula.

+

+ You can use *any* javascript (browser) functions. Arguments must be defined with ':', if + not, it will be interpreted as formula. +

-

Take care about types. All of them are defined as strings. To be sure, that value will be treated as number use parseFloat function.

+

+ Take care about types. All of them are defined as strings. To be sure, that value will be treated as + number use parseFloat function. +

-

So our Hypotenuse calculation will be:

-

- {h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;Math.max(20, Math.sqrt(Math.pow(parseFloat(h), 2) + Math.pow(parseFloat(w), 2)))} -

-
; +

So our Hypotenuse calculation will be:

+

+ {h:javascript.0.myCustom.height;w:javascript.0.myCustom.width;Math.max(20, + Math.sqrt(Math.pow(parseFloat(h), 2) + Math.pow(parseFloat(w), 2)))} +

+
+ ); } - renderHelpForOldStyle() { - return
-

Deprecated format

-

Patten has the following format:

+ renderHelpForOldStyle(): React.JSX.Element { + return ( +
+

Deprecated format

+

Patten has the following format:

-

- {objectID;operation1;operation2;...} -

+

+ {objectID;operation1;operation2;...} +

-

The following operations are supported:

-
    -
  • - this.insertInText('*', { - oldStyle: true, - desc: 'Multiply with N', - args: [{ type: 'number', label: 'N' }], - })} - > - *(N) - - - multiplying. Argument must be in brackets, like - "*(4)" - . - this sample, we multiply the value with 4. -
  • -
  • - this.insertInText('+', { - oldStyle: true, - desc: 'Add to N', - args: [{ type: 'number', label: 'N' }], - })} - > - +(N) - - - add. Argument must be in brackets, like - "+(4.5)" - . - In this sample we add to value 4.5. -
  • -
  • - this.insertInText('-', { - oldStyle: true, - desc: 'Subtract N from value', - args: [{ type: 'number', label: 'N' }], - })} - > - -(N) - - - subtract. Argument must be in brackets, like - "-(-674.5)" - . - In this sample we subtract from value -674.5. -
  • -
  • - this.insertInText('/', { - oldStyle: true, - desc: 'Divide by D', - args: [{ type: 'number', label: 'D' }], - })} - > - /(D) - - - dividing. Argument must be in brackets, like - "/(0.5)" - . - In this sample, we divide the value by 0.5. -
  • -
  • - this.insertInText('%', { - oldStyle: true, - desc: 'Modulo with M', - args: [{ type: 'number', label: 'M' }], - })} - > - %(M) - - - modulo. Argument must be in brackets, like - "%(5)" - . - In this sample, we take modulo of 5. -
  • -
  • - this.insertInText('round', { - oldStyle: true, - desc: 'Round to integer', - })} - > - round - - - round the value. -
  • -
  • - this.insertInText('round', { - oldStyle: true, - desc: 'Round with R places after comma', - args: [{ type: 'number', label: 'R' }], - })} - > - round(R) - - - round the value with N places after point, e.g., - "34.678;round(1) => 34.7" -
  • -
  • - this.insertInText('hex', { - oldStyle: true, - desc: 'convert value to hexadecimal value', - })} - > - hex - - hex - - convert value to hexadecimal value. All letters are lower cased. -
  • -
  • - this.insertInText('hex2', { - oldStyle: true, - desc: 'Convert value to hexadecimal value. If value less 16, so the leading zero will be added', - })} - > - hex2 - - hex2 - - convert value to hexadecimal value. All letters are lower cased. If value less 16, so the leading zero will be added. -
  • -
  • - this.insertInText('HEX', { - oldStyle: true, - desc: 'Convert value to hexadecimal value. All letters are upper cased', - })} - > - HEX - - HEX - - same as hex, but upper-cased. -
  • -
  • - this.insertInText('HEX2', { - oldStyle: true, - desc: 'Convert value to hexadecimal value. All letters are upper cased. If value less 16, so the leading zero will be added', - })} - > - HEX - - HEX2 - - same as hex2, but upper-cased. -
  • -
  • - this.insertInText('min', { - oldStyle: true, - desc: 'if value is less than N, take the N, else take the value', - args: [{ type: 'number', label: 'N' }], - })} - > - min(N) - - - if value is less than N, take the N, else value -
  • -
  • - this.insertInText('max', { - oldStyle: true, - desc: 'if value is greater than M, take the M, else take the value', - args: [{ type: 'number', label: 'M' }], - })} - > - max(N) - - - if value is greater than M, take the M, else value -
  • -
  • - this.insertInText('sqrt', { - oldStyle: true, - desc: 'square root', - })} - > - sqrt - - - square root -
  • -
  • - this.insertInText('pow', { - oldStyle: true, - desc: 'power of N', - args: [{ type: 'number', label: 'n' }], - })} - > - pow(n) - - - power of N. -
  • -
  • - this.insertInText('pow', { - oldStyle: true, - desc: 'power of 2', - })} - > - pow - - - power of 2. -
  • -
  • - this.insertInText('floor', { - oldStyle: true, - desc: 'Math.floor', - })} - > - floor - - - Math.floor -
  • -
  • - this.insertInText('ceil', { - oldStyle: true, - desc: 'Math.ceil', - })} - > - ceil - - - Math.ceil -
  • -
  • - this.insertInText('random', { - oldStyle: true, - desc: 'Math.random', - })} - > - random - - this.insertInText('random', { - oldStyle: true, - desc: 'Math.random', - args: [{ type: 'number', label: 'R' }], - })} - > - random(R) - - - Math.random() * R, or just Math.random() if no argument -
  • -
  • - this.insertInText('formatValue', { - oldStyle: true, - desc: 'format value according to system settings and use decimals', - args: [{ type: 'number', label: 'decimals' }], - })} - > - formatValue(decimals) - - - format value according to system settings and use decimals -
  • -
  • - this.insertInText('date', { - oldStyle: true, - desc: 'format value as date. The format is like "YYYY-MM-DD hh:mm:ss.sss"', - args: [{ type: 'string', label: 'format' }], - })} - > - date(format) - - - format value as date. The format is like: - "YYYY-MM-DD hh:mm:ss.sss" - . Format is the same as in - - iobroker.javascript - - . - If no format given, so the system date format will be used. -
  • -
  • - this.insertInText('momentDate', { - oldStyle: true, - desc: 'format value as date using Moment.js', - link: 'https://momentjs.com/docs/#/displaying/format/', - args: [ - { type: 'string', label: 'format' }, - { type: 'boolean', label: 'Use today or yesterday' }, - ], - })} - > - momentDate(format, useTodayOrYesterday) - - - format value as date using Moment.js. - - formats must be entered according to the moment.js library - - . - With 'useTodayOrYesterday=true' the 'moment.js' format 'ddd'/'dddd' are overwritten with today / yesterday -
  • -
  • - this.insertInText('array', { - oldStyle: true, - desc: 'returns the element in given array according to index (converted from value)', - args: [{ type: 'string', label: 'array elements divided by comma' }], - })} - > - array(element1,element2[,element3,element4]) - - - returns the element of index. e.g.: - {id.ack;array(ack is false,ack is true)} -
  • -
+

The following operations are supported:

+
    +
  • + + this.insertInText('*', { + oldStyle: true, + desc: 'Multiply with N', + args: [{ type: 'number', label: 'N' }], + }) + } + > + *(N) + + - multiplying. Argument must be in brackets, like + "*(4)". + this sample, we multiply the value with 4. +
  • +
  • + + this.insertInText('+', { + oldStyle: true, + desc: 'Add to N', + args: [{ type: 'number', label: 'N' }], + }) + } + > + +(N) + + - add. Argument must be in brackets, like + "+(4.5)". + In this sample we add to value 4.5. +
  • +
  • + + this.insertInText('-', { + oldStyle: true, + desc: 'Subtract N from value', + args: [{ type: 'number', label: 'N' }], + }) + } + > + -(N) + + - subtract. Argument must be in brackets, like + "-(-674.5)". + In this sample we subtract from value -674.5. +
  • +
  • + + this.insertInText('/', { + oldStyle: true, + desc: 'Divide by D', + args: [{ type: 'number', label: 'D' }], + }) + } + > + /(D) + + - dividing. Argument must be in brackets, like + "/(0.5)". + In this sample, we divide the value by 0.5. +
  • +
  • + + this.insertInText('%', { + oldStyle: true, + desc: 'Modulo with M', + args: [{ type: 'number', label: 'M' }], + }) + } + > + %(M) + + - modulo. Argument must be in brackets, like + "%(5)". + In this sample, we take modulo of 5. +
  • +
  • + + this.insertInText('round', { + oldStyle: true, + desc: 'Round to integer', + }) + } + > + round + + - round the value. +
  • +
  • + + this.insertInText('round', { + oldStyle: true, + desc: 'Round with R places after comma', + args: [{ type: 'number', label: 'R' }], + }) + } + > + round(R) + + - round the value with N places after point, e.g., + "34.678;round(1) => 34.7" +
  • +
  • + + this.insertInText('hex', { + oldStyle: true, + desc: 'convert value to hexadecimal value', + }) + } + > + hex + + hex- convert value to hexadecimal value. All letters are lower cased. +
  • +
  • + + this.insertInText('hex2', { + oldStyle: true, + desc: 'Convert value to hexadecimal value. If value less 16, so the leading zero will be added', + }) + } + > + hex2 + + hex2- convert value to hexadecimal value. All letters are lower cased. If value less 16, + so the leading zero will be added. +
  • +
  • + + this.insertInText('HEX', { + oldStyle: true, + desc: 'Convert value to hexadecimal value. All letters are upper cased', + }) + } + > + HEX + + HEX- same as hex, but upper-cased. +
  • +
  • + + this.insertInText('HEX2', { + oldStyle: true, + desc: 'Convert value to hexadecimal value. All letters are upper cased. If value less 16, so the leading zero will be added', + }) + } + > + HEX + + HEX2- same as hex2, but upper-cased. +
  • +
  • + + this.insertInText('min', { + oldStyle: true, + desc: 'if value is less than N, take the N, else take the value', + args: [{ type: 'number', label: 'N' }], + }) + } + > + min(N) + + - if value is less than N, take the N, else value +
  • +
  • + + this.insertInText('max', { + oldStyle: true, + desc: 'if value is greater than M, take the M, else take the value', + args: [{ type: 'number', label: 'M' }], + }) + } + > + max(N) + + - if value is greater than M, take the M, else value +
  • +
  • + + this.insertInText('sqrt', { + oldStyle: true, + desc: 'square root', + }) + } + > + sqrt + + - square root +
  • +
  • + + this.insertInText('pow', { + oldStyle: true, + desc: 'power of N', + args: [{ type: 'number', label: 'n' }], + }) + } + > + pow(n) + + - power of N. +
  • +
  • + + this.insertInText('pow', { + oldStyle: true, + desc: 'power of 2', + }) + } + > + pow + + - power of 2. +
  • +
  • + + this.insertInText('floor', { + oldStyle: true, + desc: 'Math.floor', + }) + } + > + floor + + - Math.floor +
  • +
  • + + this.insertInText('ceil', { + oldStyle: true, + desc: 'Math.ceil', + }) + } + > + ceil + + - Math.ceil +
  • +
  • + + this.insertInText('random', { + oldStyle: true, + desc: 'Math.random', + }) + } + > + random + + + this.insertInText('random', { + oldStyle: true, + desc: 'Math.random', + args: [{ type: 'number', label: 'R' }], + }) + } + > + random(R) + + - Math.random() * R, or just Math.random() if no argument +
  • +
  • + + this.insertInText('formatValue', { + oldStyle: true, + desc: 'format value according to system settings and use decimals', + args: [{ type: 'number', label: 'decimals' }], + }) + } + > + formatValue(decimals) + + - format value according to system settings and use decimals +
  • +
  • + + this.insertInText('date', { + oldStyle: true, + desc: 'format value as date. The format is like "YYYY-MM-DD hh:mm:ss.sss"', + args: [{ type: 'string', label: 'format' }], + }) + } + > + date(format) + + - format value as date. The format is like: + "YYYY-MM-DD hh:mm:ss.sss". Format is the same as in + + iobroker.javascript + + .If no format given, so the system date format will be used. +
  • +
  • + + this.insertInText('momentDate', { + oldStyle: true, + desc: 'format value as date using Moment.js', + link: 'https://momentjs.com/docs/#/displaying/format/', + args: [ + { type: 'string', label: 'format' }, + { type: 'boolean', label: 'Use today or yesterday' }, + ], + }) + } + > + momentDate(format, useTodayOrYesterday) + + - format value as date using Moment.js. + + formats must be entered according to the moment.js library + + . + + With 'useTodayOrYesterday=true' the 'moment.js' format + 'ddd'/'dddd' are overwritten with today / yesterday + +
  • +
  • + + this.insertInText('array', { + oldStyle: true, + desc: 'returns the element in given array according to index (converted from value)', + args: [{ type: 'string', label: 'array elements divided by comma' }], + }) + } + > + array(element1,element2[,element3,element4]) + + - returns the element of index. e.g.: + + {id.ack;array(ack is false,ack is true)} + +
  • +
-

You can use this pattern in any text, like

-

- My calculations with {objectID1;operation1;operation2;...} are {objectID2;operation3;operation4;...} -

-

or color calculations:

-

- #{objectRed;/(100);*(255);HEX2}{objectGreen;HEX2}{objectBlue;HEX2} -

-

- To show timestamp of object write - .ts - or - .lc - (for last change) at the end of object id, e.g.: -

-

- Last change: {objectRed.lc;date(hh:mm)} -

-
; +

You can use this pattern in any text, like

+

+ My calculations with {objectID1;operation1;operation2;...} are + {objectID2;operation3;operation4;...} +

+

or color calculations:

+

+ #{objectRed;/(100);*(255);HEX2}{objectGreen;HEX2}{objectBlue;HEX2} +

+

+ To show timestamp of object write + .ts + or + .lc + (for last change) at the end of object id, e.g.: +

+

Last change: {objectRed.lc;date(hh:mm)}

+
+ ); } - getSelectedText() { + getSelectedText(): string { return this.state.editValue.substring(this.inputRef.current.selectionStart, this.inputRef.current.selectionEnd); } - renderEditBindDialog() { + renderEditBindDialog(): React.JSX.Element | null { if (!this.state.showEditBindingDialog) { return null; } const varValuesKeys = this.state.values ? Object.keys(this.state.values) : []; - return - {I18n.t('Edit binding')} - -
- { - if (e.key === 'Enter') { - this.setState({ showEditBindingDialog: false }, () => this.onChange(this.state.editValue)); + return ( + + {I18n.t('Edit binding')} + +
+ { + if (e.key === 'Enter') { + this.setState({ showEditBindingDialog: false }, () => + this.onChange(this.state.editValue), + ); + } + }} + InputProps={{ + endAdornment: this.state.editValue ? ( + + this.setState({ editValue: '' })} + edge="end" + > + + + + ) : null, + }} + style={{ width: 'calc(100% - 72px)' }} + onChange={(e: React.ChangeEvent): void => + this.setState({ editValue: e.target.value }, () => { + this.calculateTimeout && clearTimeout(this.calculateTimeout); + this.calculateTimeout = setTimeout(async () => { + this.calculateTimeout = null; + const { calculatedEditValue, values } = await this.calculateValue( + e.target.value, + ); + this.setState({ calculatedEditValue, values }); + }, 200); + }) } - }} - InputProps={{ - endAdornment: this.state.editValue ? - this.setState({ editValue: '' })} - edge="end" - > - - - : null, - }} - style={{ width: 'calc(100% - 72px)' }} - onChange={async e => this.setState({ editValue: e.target.value }, () => { - this.calculateTimeout && clearTimeout(this.calculateTimeout); - this.calculateTimeout = setTimeout(async () => { - this.calculateTimeout = null; - const { calculatedEditValue, values } = await this.calculateValue(e.target.value); - this.setState({ calculatedEditValue, values }); - }, 200); - })} - helperText={ - - {I18n.t('Calculate values')} - : - - {this.state.calculatedEditValue} - } - /> + helperText={ + + {I18n.t('Calculate values')}: + {this.state.calculatedEditValue} + + } + /> + +
+ {varValuesKeys.length ? ( +
+ {varValuesKeys.map(id => ( +
+ {id} + : + + {`${this.state.values[id] === null || this.state.values[id] === undefined ? 'null' : this.state.values[id].toString()} [${typeof this.state.values[id]}]`} + +
+ ))} +
+ ) : null} +
+
+ {I18n.t('Old style')} + this.setState({ newStyle: e.target.checked })} + /> + {I18n.t('New style')} +
+
+ {this.renderSpecialNames()} + {this.state.newStyle + ? WidgetBindingField.renderHelpForNewStyle() + : this.renderHelpForOldStyle()} +
+
+
+ -
- {varValuesKeys.length ?
- {varValuesKeys.map(id =>
- {id} - : - - {`${this.state.values[id] === null || this.state.values[id] === undefined ? 'null' : this.state.values[id].toString()} [${typeof this.state.values[id]}]`} - -
)} -
: null} -
-
- {I18n.t('Old style')} - this.setState({ newStyle: e.target.checked })} - /> - {I18n.t('New style')} -
-
- {this.renderSpecialNames()} - {this.state.newStyle ? WidgetBindingField.renderHelpForNewStyle() : this.renderHelpForOldStyle()} -
-
-
- - - - -
; + + + + ); } static isInBrackets(text: string, pos: number): boolean { @@ -917,139 +1070,159 @@ class WidgetBindingField extends Component 0; } - renderDialogAskToModify() { + renderDialogAskToModify(): React.JSX.Element | null { if (!this.state.askToModify) { return null; } - return this.setState({ askToModify: null })} - > - {I18n.t('Edit binding')} - -
- {I18n.t('Do you want to modify the value or just use it as it is?')} -
-
- - - - -
; + return ( + this.setState({ askToModify: null })} + > + {I18n.t('Edit binding')} + +
{I18n.t('Do you want to modify the value or just use it as it is?')}
+
+ + + + +
+ ); } - renderDialogArguments() { + renderDialogArguments(): React.JSX.Element | null { if (!this.state.askForArguments) { return null; } - return this.setState({ askForArguments: null })} - key="argsDialog" - > - {I18n.t('Arguments')} - -
- {this.state.askForArguments.desc} -
- {this.state.askForArguments.args[0] ?
- { - if (e.key === 'Enter') { - const options = this.state.askForArguments; - options.args = null; - this.setState({ askForArguments: null }, () => - this.insertInText(options.text, options)); - } - }} - InputProps={{ - endAdornment: this.state.askForArguments.arg1 ? - this.setState({ - askForArguments: { - ...this.state.askForArguments, - arg1: '', - }, - })} - edge="end" - > - - - : null, + return ( + this.setState({ askForArguments: null })} + key="argsDialog" + > + {I18n.t('Arguments')} + +
{this.state.askForArguments.desc}
+ {this.state.askForArguments.args[0] ? ( +
+ { + if (e.key === 'Enter') { + const options = this.state.askForArguments; + options.args = null; + this.setState({ askForArguments: null }, () => + this.insertInText(options.text, options), + ); + } + }} + InputProps={{ + endAdornment: this.state.askForArguments.arg1 ? ( + + + this.setState({ + askForArguments: { + ...this.state.askForArguments, + arg1: '', + }, + }) + } + edge="end" + > + + + + ) : null, + }} + autoFocus + onChange={e => + this.setState({ + askForArguments: { ...this.state.askForArguments, arg1: e.target.value }, + }) + } + /> +
+ ) : null} + {this.state.askForArguments.args[1] ? ( +
+ + this.setState({ + askForArguments: { + ...this.state.askForArguments, + arg2: e.target.checked, + }, + }) + } + /> + } + /> +
+ ) : null} +
+ +
: null} - {this.state.askForArguments.args[1] ?
- this.setState({ askForArguments: { ...this.state.askForArguments, arg2: e.target.checked } })} - />} - /> -
: null} -
- - - - -
; + > + {I18n.t('Apply')} + + + + + ); } - async insertInText( - text: string, - options?: ModifyOptions, - ): Promise { + async insertInText(text: string, options?: ModifyOptions): Promise { options = options || {}; - const selectionStart = options.selectionStart === undefined ? this.inputRef.current.selectionStart : options.selectionStart; - const selectionEnd = options.selectionEnd === undefined ? this.inputRef.current.selectionEnd : options.selectionEnd; + const selectionStart = + options.selectionStart === undefined ? this.inputRef.current.selectionStart : options.selectionStart; + const selectionEnd = + options.selectionEnd === undefined ? this.inputRef.current.selectionEnd : options.selectionEnd; const isInBrackets = WidgetBindingField.isInBrackets(this.state.editValue, selectionStart); if (!options.oldStyle && options.modify === undefined && !isInBrackets) { @@ -1114,31 +1287,37 @@ class WidgetBindingField extends Component { - let selected; - if (Array.isArray(_selected)) { - selected = _selected[0]; - } else { - selected = _selected; - } - // insert on cursor and replace selected text - selected && await this.insertInText(selected, { selectionStart: this.state.selectionStart, selectionEnd: this.state.selectionEnd }); - }} - onClose={() => this.setState({ showSelectIdDialog: false })} - socket={this.props.socket as any as Connection} - />; + return ( + { + let selected; + if (Array.isArray(_selected)) { + selected = _selected[0]; + } else { + selected = _selected; + } + // insert on cursor and replace selected text + selected && + (await this.insertInText(selected, { + selectionStart: this.state.selectionStart, + selectionEnd: this.state.selectionEnd, + })); + }} + onClose={() => this.setState({ showSelectIdDialog: false })} + socket={this.props.socket as any as Connection} + /> + ); } - render() { + render(): React.JSX.Element[] { return [ { - const { calculatedEditValue, values } = await this.calculateValue(this.state.value); - this.setState({ - showEditBindingDialog: true, - editValue: this.state.value, - calculatedEditValue, - values, - newStyle: !this.detectOldBindingStyle(this.state.value), - }); - }} - > - - , + endAdornment: ( + + ), }} error={!!this.state.error} helperText={typeof this.state.error === 'string' ? I18n.t(this.state.error) : null} diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetCSS.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetCSS.tsx index d13dd0aba..dd097d648 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetCSS.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetCSS.tsx @@ -1,8 +1,6 @@ import React, { useRef, useState } from 'react'; -import { - Dialog, DialogTitle, DialogContent, DialogActions, Button, -} from '@mui/material'; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Close } from '@mui/icons-material'; @@ -19,42 +17,48 @@ interface WidgetCSSProps { widget: Widget; } -const WidgetCSS = (props: WidgetCSSProps) => { +const WidgetCSS = (props: WidgetCSSProps): React.JSX.Element => { const [value, setValue] = useState(props.widget.css || ''); const timeout = useRef(null); - return - {I18n.t('Widget CSS')} - - { - setValue(v); - if (timeout.current) { - clearTimeout(timeout.current); - timeout.current = null; - } - timeout.current = setTimeout(() => { - timeout.current = null; - props.onChange(v); - }, 1000); - }} - /> - - - - - ; + return ( + + {I18n.t('Widget CSS')} + + { + setValue(v); + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = null; + } + timeout.current = setTimeout(() => { + timeout.current = null; + props.onChange(v); + }, 1000); + }} + /> + + + + + + ); }; export default WidgetCSS; diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetField.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetField.tsx index 4bd96da20..559fc011c 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetField.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetField.tsx @@ -1,25 +1,35 @@ -import React, { - useEffect, useRef, useState, -} from 'react'; +import React, { useEffect, useRef, useState } from 'react'; -import type { SelectChangeEvent } from '@mui/material'; import { - Autocomplete, Box, Button, Checkbox, Fade, IconButton, Input, ListItemText, - ListSubheader, MenuItem, Paper, Popper, Select, Slider, TextField, FormControl, - FormHelperText, ListItemIcon, DialogActions, - Dialog, DialogTitle, DialogContent, DialogContentText, + Autocomplete, + Box, + Button, + Checkbox, + Fade, + IconButton, + Input, + ListItemText, + ListSubheader, + MenuItem, + Paper, + Popper, + Select, + Slider, + TextField, + FormControl, + FormHelperText, + ListItemIcon, + DialogActions, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + type SelectChangeEvent, } from '@mui/material'; -import { - InsertDriveFile as FileIcon, - Clear as ClearIcon, - Edit as EditIcon, - Check, - Close, -} from '@mui/icons-material'; +import { InsertDriveFile as FileIcon, Clear as ClearIcon, Edit as EditIcon, Check, Close } from '@mui/icons-material'; import { FaFolderOpen as FolderOpenedIcon } from 'react-icons/fa'; -import type { Connection, LegacyConnection, ThemeType } from '@iobroker/adapter-react-v5'; import { I18n, IconPicker, @@ -28,13 +38,18 @@ import { ColorPicker, SelectID, SelectFile as SelectFileDialog, + type ObjectBrowserCustomFilter, + type Connection, + type LegacyConnection, + type ThemeType, } from '@iobroker/adapter-react-v5'; import { findWidgetUsages } from '@/Vis/visUtils'; import { store, recalculateFields, selectWidget } from '@/Store'; import { deepClone } from '@/Utils/utils'; import type { - AnyWidgetId, Project, + AnyWidgetId, + Project, Widget, WidgetData, WidgetStyle, @@ -43,11 +58,6 @@ import type { VisTheme, } from '@iobroker/types-vis-2'; -import type { - ObjectBrowserCustomFilter, - ObjectBrowserType, -} from '@iobroker/adapter-react-v5/Components/types'; - import type { RxFieldOption, WidgetAttributeInfoStored, WidgetType } from '@/Vis/visWidgetsCatalog'; import commonStyles from '@/Utils/styles'; import TextDialog from './TextDialog'; @@ -74,7 +84,11 @@ function collectClasses(): Record { const _styles = ruleList[rule].selectorText.split(','); for (let s = 0; s < _styles.length; s++) { const subStyles = _styles[s].trim().split(' '); - const _style = subStyles[subStyles.length - 1].replace('::before', '').replace('::after', '').replace(':before', '').replace(':after', ''); + const _style = subStyles[subStyles.length - 1] + .replace('::before', '') + .replace('::after', '') + .replace(':before', '') + .replace(':after', ''); if (!_style || _style[0] !== '.' || _style.includes(':')) { continue; @@ -84,7 +98,7 @@ function collectClasses(): Record { name = name.replace(',', ''); name = name.replace(/^\./, ''); - const val = name; + const val = name; name = name.replace(/^hq-background-/, ''); name = name.replace(/^hq-/, ''); name = name.replace(/^ui-/, ''); @@ -101,8 +115,11 @@ function collectClasses(): Record { if (!result[val]) { if (subStyles.length > 1) { result[val] = { + name, + file: fff, // @ts-expect-error style does exist - name, file: fff, attrs: ruleList[rule].style, parentClass: subStyles[0].replace('.', ''), + attrs: ruleList[rule].style, + parentClass: subStyles[0].replace('.', ''), }; } else { // @ts-expect-error style does exist @@ -132,13 +149,13 @@ function getStylesOptions(options: { filterAttrs: string; removeName: string; styles?: Record; -}) { +}): Record { // Fill the list with styles const _internalList = window.collectClassesValue; - options.filterName = options.filterName || ''; + options.filterName = options.filterName || ''; options.filterAttrs = options.filterAttrs || ''; - options.filterFile = options.filterFile || ''; + options.filterFile = options.filterFile || ''; let gStyles: Record = {}; @@ -146,24 +163,24 @@ function getStylesOptions(options: { gStyles = { ...options.styles }; } else if (options.filterFile || options.filterName) { // IF filter defined - const filters = options.filterName ? options.filterName.split(' ') : null; - const attrs = options.filterAttrs ? options.filterAttrs.split(' ') : null; - const files = options.filterFile ? options.filterFile.split(' ') : ['']; + const filters = options.filterName ? options.filterName.split(' ') : null; + const attrs = options.filterAttrs ? options.filterAttrs.split(' ') : null; + const files = options.filterFile ? options.filterFile.split(' ') : ['']; Object.keys(_internalList).forEach((style: string) => files.forEach(file => { - if (!options.filterFile || - (_internalList[style].file && _internalList[style].file.includes(file)) - ) { + if (!options.filterFile || (_internalList[style].file && _internalList[style].file.includes(file))) { let isFound = !filters; - isFound = isFound || (!!filters.find(filter => style.includes(filter))); + isFound = isFound || !!filters.find(filter => style.includes(filter)); if (isFound) { isFound = !attrs; if (!isFound) { isFound = !!attrs.find((attr: string) => { - const t: string | number = (_internalList[style].attrs as Record)[attr] as string | number; + const t: string | number = ( + _internalList[style].attrs as Record + )[attr]; return t || t === 0; }); } @@ -176,13 +193,14 @@ function getStylesOptions(options: { n = n[0].toUpperCase() + n.substring(1).toLowerCase(); } gStyles[style] = { - name: n, - file: _internalList[style].file, + name: n, + file: _internalList[style].file, parentStyle: _internalList[style].parentStyle, }; } } - })); + }), + ); } else { gStyles = { ...gStyles, ..._internalList }; } @@ -201,7 +219,13 @@ const getViewOptions = ( }[] = [], parentId: string = null, level = 0, -) => { +): { + view?: string; + type: 'folder' | 'view'; + level: number; + folder?: { id: string; name: string; parentId: string }; + label?: string; +}[] => { project.___settings.folders .filter(folder => (folder.parentId || null) === parentId) .forEach(folder => { @@ -214,14 +238,17 @@ const getViewOptions = ( getViewOptions(project, options, folder.id, level + 1); }); - const keys = Object.keys(project) - .filter(view => (project[view].parentId || null) === parentId && !view.startsWith('__')); + const keys = Object.keys(project).filter( + view => (project[view].parentId || null) === parentId && !view.startsWith('__'), + ); keys.forEach(view => { options.push({ type: 'view', view, - label: project[view].settings?.navigationTitle ? `${project[view].settings.navigationTitle} (${view})` : view, + label: project[view].settings?.navigationTitle + ? `${project[view].settings.navigationTitle} (${view})` + : view, level: level + 1, }); }); @@ -232,7 +259,7 @@ const getViewOptions = ( // Optimize translation const wordsCache: Record = {}; -const t = (word: string, ...args: any[]) => { +const t = (word: string, ...args: any[]): string => { const hash = `${word}_${args.join(',')}`; if (!wordsCache[hash]) { wordsCache[hash] = I18n.t(word, ...args); @@ -250,7 +277,7 @@ function modifyWidgetUsages( // find where it is used const newProject = deepClone(project); const usedIn = findWidgetUsages(newProject, usedInView, usedWidgetId); - usedIn.forEach(usage => newProject[usage.view].widgets[usage.wid].data[usage.attr] = ''); + usedIn.forEach(usage => (newProject[usage.view].widgets[usage.wid].data[usage.attr] = '')); newProject[usedInView].widgets[inNewWidgetId].data[inAttr] = usedWidgetId; return newProject; @@ -281,24 +308,13 @@ interface WidgetFieldProps { theme: VisTheme; } -const WidgetField = (props: WidgetFieldProps) => { +const WidgetField = (props: WidgetFieldProps): string | React.JSX.Element | React.JSX.Element[] => { const [idDialog, setIdDialog] = useState(false); const [objectCache, setObjectCache] = useState(null); const [askForUsage, setAskForUsage] = useState(null); - const { - field, - widget, - adapterName, - instance, - projectName, - isDifferent, - error, - disabled, - index, - widgetId, - } = props; + const { field, widget, adapterName, instance, projectName, isDifferent, error, disabled, index, widgetId } = props; let customLegacyComponent = null; @@ -313,7 +329,7 @@ const WidgetField = (props: WidgetFieldProps) => { funcs.shift(); } - window._ = window.vis._; // for old widgets, else lodash overwrites it + window._ = window.vis._; // for old widgets, else lodash overwrites it window.vis.activeWidgets = [...props.selectedWidgets]; window.vis.activeView = props.selectedView; @@ -338,7 +354,11 @@ const WidgetField = (props: WidgetFieldProps) => { console.log(`No function: vis.binds.${funcs.join('.')}`); } } else if (funcs.length === 3) { - if (window.vis.binds[funcs[0]] && window.vis.binds[funcs[0]][funcs[1]] && typeof window.vis.binds[funcs[0]][funcs[1]][funcs[2]] === 'function') { + if ( + window.vis.binds[funcs[0]] && + window.vis.binds[funcs[0]][funcs[1]] && + typeof window.vis.binds[funcs[0]][funcs[1]][funcs[2]] === 'function' + ) { try { customLegacyComponent = window.vis.binds[funcs[0]][funcs[1]][funcs[2]](field.name, options); } catch (e) { @@ -355,19 +375,21 @@ const WidgetField = (props: WidgetFieldProps) => { } const [cachedValue, setCachedValue] = useState(''); - const [instances, setInstances] = useState<{ - id: string; - idShort: string; - name: string; - icon: string; - }[]>([]); + const [instances, setInstances] = useState< + { + id: string; + idShort: string; + name: string; + icon: string; + }[] + >([]); const cacheTimer = useRef>(null); const refCustom = useRef(); let onChangeTimeout: ReturnType; - const applyValue = (newValues: any) => { + const applyValue = (newValues: any): void => { const project = deepClone(store.getState().visProject); props.selectedWidgets.forEach((selectedWidget, i) => { const value: any = Array.isArray(newValues) && field.type !== 'groups' ? newValues[i] : newValues; @@ -394,22 +416,28 @@ const WidgetField = (props: WidgetFieldProps) => { } } if (field.onChange) { - field.onChange(field as RxWidgetInfoAttributesField, JSON.parse(JSON.stringify(data)), (newData: WidgetData) => { - const _project = JSON.parse(JSON.stringify(store.getState().visProject)); - _project[props.selectedView].widgets[selectedWidget].data = newData; - onChangeTimeout && clearTimeout(onChangeTimeout); - onChangeTimeout = setTimeout(() => { - onChangeTimeout = null; - props.changeProject(_project); - }, 100); - }, props.socket, index); + void field.onChange( + field as RxWidgetInfoAttributesField, + JSON.parse(JSON.stringify(data)), + (newData: WidgetData) => { + const _project = JSON.parse(JSON.stringify(store.getState().visProject)); + _project[props.selectedView].widgets[selectedWidget].data = newData; + onChangeTimeout && clearTimeout(onChangeTimeout); + onChangeTimeout = setTimeout(() => { + onChangeTimeout = null; + props.changeProject(_project); + }, 100); + }, + props.socket, + index, + ); } }); props.changeProject(project); }; - const change = (changeValue: any) => { + const change = (changeValue: any): void => { if (Array.isArray(changeValue) || field.immediateChange) { // apply immediately applyValue(changeValue); @@ -434,50 +462,44 @@ const WidgetField = (props: WidgetFieldProps) => { } if (field.type === 'instance' || field.type === 'history') { if (field.adapter === '_dataSources' || field.type === 'history') { - props.socket.getAdapterInstances('') - .then(_instances => { - const inst = _instances - .filter(obj => obj.common.getHistory) - .map(obj => ({ - id: obj._id.replace('system.adapter.', ''), - idShort: obj._id.split('.').pop(), - name: obj.common.name, - icon: obj.common.icon, - })) - .sort((a, b) => - (a.name > b.name ? 1 : (a.name < b.name ? -1 : 0))); - setInstances(inst); - }); + void props.socket.getAdapterInstances('').then(_instances => { + const inst = _instances + .filter(obj => obj.common.getHistory) + .map(obj => ({ + id: obj._id.replace('system.adapter.', ''), + idShort: obj._id.split('.').pop(), + name: obj.common.name, + icon: obj.common.icon, + })) + .sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)); + setInstances(inst); + }); } else if (Array.isArray(field.adapters)) { - props.socket.getAdapterInstances('') - .then(_instances => { - const inst = _instances - .filter(obj => field.adapters.includes(obj.common.name)) - .map(obj => ({ - id: obj._id.replace('system.adapter.', ''), - idShort: obj._id.split('.').pop(), - name: obj.common.name, - icon: obj.common.icon, - })) - .sort((a, b) => - (a.name > b.name ? 1 : (a.name < b.name ? -1 : 0))); - setInstances(inst); - }); + void props.socket.getAdapterInstances('').then(_instances => { + const inst = _instances + .filter(obj => field.adapters.includes(obj.common.name)) + .map(obj => ({ + id: obj._id.replace('system.adapter.', ''), + idShort: obj._id.split('.').pop(), + name: obj.common.name, + icon: obj.common.icon, + })) + .sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)); + setInstances(inst); + }); } else { - props.socket.getAdapterInstances(field.adapter || '') - .then(_instances => { - const inst = _instances - .map(obj => ({ - id: obj._id.replace('system.adapter.', ''), - idShort: obj._id.split('.').pop(), - name: obj.common.name, - icon: obj.common.icon, - })) - .sort((a, b) => - (a.name > b.name ? 1 : (a.name < b.name ? -1 : 0))); - - setInstances(inst); - }); + void props.socket.getAdapterInstances(field.adapter || '').then(_instances => { + const inst = _instances + .map(obj => ({ + id: obj._id.replace('system.adapter.', ''), + idShort: obj._id.split('.').pop(), + name: obj.common.name, + icon: obj.common.icon, + })) + .sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)); + + setInstances(inst); + }); } } }, [propValue]); @@ -499,27 +521,43 @@ const WidgetField = (props: WidgetFieldProps) => { const [textDialogFocused, setTextDialogFocused] = useState(false); const [textDialogEnabled, setTextDialogEnabled] = useState(true); - const urlPopper = (!field.type || field.type === 'number' || field.type === 'password' || field.type === 'image') && !disabled ? - {({ TransitionProps }) => - - - setTextDialogEnabled(false)}> - - - - } - : null; + const urlPopper = + (!field.type || field.type === 'number' || field.type === 'password' || field.type === 'image') && !disabled ? ( + + {({ TransitionProps }) => ( + + + + setTextDialogEnabled(false)} + > + + + + + )} + + ) : null; // part for customLegacyComponent useEffect(() => { @@ -532,41 +570,43 @@ const WidgetField = (props: WidgetFieldProps) => { if (askForUsage) { const usages = findWidgetUsages(store.getState().visProject, props.selectedView, askForUsage.wid); // show dialog with usage - return setAskForUsage(null)} - > - {I18n.t('Usage of widget %s', askForUsage.wid)} - - - {I18n.t('This widget is already used in "%s"', usages[0].wid)} -
- {I18n.t('Should it be moved to new place?')} -
-
- - - - -
; + return ( + setAskForUsage(null)} + > + {I18n.t('Usage of widget %s', askForUsage.wid)} + + + {I18n.t('This widget is already used in "%s"', usages[0].wid)} +
+ {I18n.t('Should it be moved to new place?')} +
+
+ + + + +
+ ); } // start the rendering of different types of fields @@ -574,56 +614,67 @@ const WidgetField = (props: WidgetFieldProps) => { if (customLegacyComponent) { // console.log(customLegacyComponent.input); if (customLegacyComponent.button) { - return
- {/* eslint-disable-next-line react/no-danger */} -
- -
; + customLegacyComponent.button.click.call(e.target); + }} + > + ... + +
+ ); } // eslint-disable-next-line react/no-danger - return
; + return ( +
+ ); } if (field.type === 'id' || field.type === 'hid') { if (value && (!objectCache || value !== objectCache._id)) { - props.socket.getObject(value as string) - .then(objectData => - setObjectCache(objectData)) + props.socket + .getObject(value as string) + .then(objectData => setObjectCache(objectData)) .catch(() => setObjectCache(null)); } if (objectCache && !value) { @@ -644,13 +695,13 @@ const WidgetField = (props: WidgetFieldProps) => { ) { // detect role if ( - (field.filter as string).includes('.') || - (field.filter as string).startsWith('level') || - (field.filter as string).startsWith('value') + field.filter.includes('.') || + field.filter.startsWith('level') || + field.filter.startsWith('value') ) { filters = { role: field.filter }; } else { - customFilter = { type: field.filter }; + customFilter = { type: field.filter as ioBroker.ObjectType }; } } else if (field.filter) { if (typeof field.filter === 'function') { @@ -661,72 +712,88 @@ const WidgetField = (props: WidgetFieldProps) => { } } - return <> - setIdDialog(true)} - > - ... - , - }} - error={!!error} - helperText={typeof error === 'string' ? I18n.t(error) : null} - disabled={disabled} - value={value} - onChange={e => change(e.target.value)} - /> -
- {objectCache ? (typeof objectCache.common.name === 'object' ? objectCache.common.name[I18n.lang] : objectCache.common.name) : null} -
- {idDialog && !disabled ? change(selected)} - onClose={() => setIdDialog(false)} - socket={props.socket as any as Connection} - types={field.filter === 'chart' || field.filter === 'channel' || field.filter === 'device' ? [field.filter] as ObjectBrowserType[] : null} - filters={filters} - expertMode={field.filter === 'chart' ? true : undefined} - customFilter={customFilter} - /> : null} - ; + return ( + <> + setIdDialog(true)} + > + ... + + ), + }} + error={!!error} + helperText={typeof error === 'string' ? I18n.t(error) : null} + disabled={disabled} + value={value} + onChange={e => change(e.target.value)} + /> +
+ {objectCache + ? typeof objectCache.common.name === 'object' + ? objectCache.common.name[I18n.lang] + : objectCache.common.name + : null} +
+ {idDialog && !disabled ? ( + change(selected)} + onClose={() => setIdDialog(false)} + socket={props.socket as any as Connection} + types={ + field.filter === 'chart' || field.filter === 'channel' || field.filter === 'device' + ? ([field.filter] as ioBroker.ObjectType[]) + : null + } + filters={filters} + expertMode={field.filter === 'chart' ? true : undefined} + customFilter={customFilter} + /> + ) : null} + + ); } if (field.type === 'checkbox') { - return - { - store.dispatch(recalculateFields(true)); - change(e.target.checked); - }} - /> - {typeof error === 'string' ? {I18n.t(error)} : null} - ; + return ( + + { + store.dispatch(recalculateFields(true)); + change(e.target.checked); + }} + /> + {typeof error === 'string' ? {I18n.t(error)} : null} + + ); } if (field.type === 'image') { let _value: string; if (idDialog) { - _value = value as string || ''; + _value = (value as string) || ''; if (_value.startsWith('../')) { _value = _value.substring(3); } else if (_value.startsWith('_PRJ_NAME/')) { @@ -734,56 +801,69 @@ const WidgetField = (props: WidgetFieldProps) => { } } - return <> - setIdDialog(true)}>..., - }} - ref={textRef} - value={value} - onFocus={() => setTextDialogFocused(true)} - onBlur={() => setTextDialogFocused(false)} - onChange={e => change(e.target.value)} - /> - {urlPopper} - {idDialog ? setIdDialog(false)} - restrictToFolder={`${adapterName}.${instance}/${projectName}`} - allowNonRestricted - allowUpload - allowDownload - allowCreateFolder - allowDelete - allowView - showToolbar - imagePrefix="../" - selected={_value} - filterByType="images" - theme={props.theme} - onOk={_selected => { - let selected = Array.isArray(_selected) ? _selected[0] : _selected; - const projectPrefix = `${adapterName}.${instance}/${projectName}/`; - if (selected.startsWith(projectPrefix)) { - selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; - } else if (selected.startsWith('/')) { - selected = `..${selected}`; - } else if (!selected.startsWith('.')) { - selected = `../${selected}`; - } - change(selected); - setIdDialog(false); - }} - socket={props.socket as any as Connection} - /> : null} - ; + return ( + <> + setIdDialog(true)} + > + ... + + ), + }} + ref={textRef} + value={value} + onFocus={() => setTextDialogFocused(true)} + onBlur={() => setTextDialogFocused(false)} + onChange={e => change(e.target.value)} + /> + {urlPopper} + {idDialog ? ( + setIdDialog(false)} + restrictToFolder={`${adapterName}.${instance}/${projectName}`} + allowNonRestricted + allowUpload + allowDownload + allowCreateFolder + allowDelete + allowView + showToolbar + imagePrefix="../" + selected={_value} + filterByType="images" + theme={props.theme} + onOk={_selected => { + let selected = Array.isArray(_selected) ? _selected[0] : _selected; + const projectPrefix = `${adapterName}.${instance}/${projectName}/`; + if (selected.startsWith(projectPrefix)) { + selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; + } else if (selected.startsWith('/')) { + selected = `..${selected}`; + } else if (!selected.startsWith('.')) { + selected = `../${selected}`; + } + change(selected); + setIdDialog(false); + }} + socket={props.socket as any as Connection} + /> + ) : null} + + ); } if (field.type === 'dimension') { @@ -814,135 +894,162 @@ const WidgetField = (props: WidgetFieldProps) => { } } - const strValue: string = typeof value === 'number' ? value.toString() : value as string; + const strValue: string = typeof value === 'number' ? value.toString() : (value as string); if (options.length && value !== '' && !options.includes(strValue)) { options.push(strValue); } - return change(aVal))} - renderInput={params => change(aVal)} + renderInput={params => ( + { + if (unit !== '%') { + props.onPxToPercent(props.selectedWidgets, field.name, newValues => + change(newValues[0]), + ); + } else { + props.onPercentToPx(props.selectedWidgets, field.name, newValues => + change(newValues[0]), + ); + } + }} + > + {unit} + + ) : null, + }} + value={value} + onChange={e => change(e.target.value)} + /> + )} + /> + ); + } + return ( + { if (unit !== '%') { - props.onPxToPercent(props.selectedWidgets, field.name, newValues => change(newValues[0])); + props.onPxToPercent(props.selectedWidgets, field.name, newValues => + change(newValues[0]), + ); } else { - props.onPercentToPx(props.selectedWidgets, field.name, newValues => change(newValues[0])); + props.onPercentToPx(props.selectedWidgets, field.name, newValues => + change(newValues[0]), + ); } }} > {unit} - : null, - }} - value={value} - onChange={e => change(e.target.value)} - />} - />; - } - return { - if (unit !== '%') { - props.onPxToPercent(props.selectedWidgets, field.name, newValues => change(newValues[0])); - } else { - props.onPercentToPx(props.selectedWidgets, field.name, newValues => change(newValues[0])); - } - }} - > - {unit} - : null, - }} - value={value} - onChange={e => change(e.target.value)} - />; + + ) : null, + }} + value={value} + onChange={e => change(e.target.value)} + /> + ); } if (field.type === 'color') { - return change(color)} - />; + return ( + change(color)} + /> + ); } // @ts-expect-error legacy parameter if (field.type === 'eff_opt') { - return <> - {field.type} - / - {value} - ; + return ( + <> + {field.type}/{value} + + ); } if (field.type === 'slider') { // make space before a slider element, as if it is at a minimum it overlaps with the label - return
-
- change(newValue)} - value={typeof value === 'number' ? value : 0} - min={field.min} - max={field.max} - step={field.step} - marks={field.marks} - valueLabelDisplay={field.valueLabelDisplay} - /> - 100000 ? 70 : (field.max > 10000 ? 60 : 50) }} - value={value} - disabled={disabled} - size="small" - onChange={e => (e.target.value === '' ? change('') : change(parseFloat(e.target.value)))} - sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} - inputProps={{ - step: field.step, - min: field.min, - max: field.max, - type: 'number', - }} - /> -
; + return ( +
+
+ change(newValue)} + value={typeof value === 'number' ? value : 0} + min={field.min} + max={field.max} + step={field.step} + marks={field.marks} + valueLabelDisplay={field.valueLabelDisplay} + /> + 100000 ? 70 : field.max > 10000 ? 60 : 50, + }} + value={value} + disabled={disabled} + size="small" + onChange={e => (e.target.value === '' ? change('') : change(parseFloat(e.target.value)))} + sx={{ ...commonStyles.clearPadding, ...commonStyles.fieldContent }} + inputProps={{ + step: field.step, + min: field.min, + max: field.max, + type: 'number', + }} + /> +
+ ); } - if (field.type === 'select' || + if ( + field.type === 'select' || field.type === 'nselect' || field.type === 'fontname' || // @ts-expect-error legacy parameter @@ -981,24 +1088,35 @@ const WidgetField = (props: WidgetFieldProps) => { // take widgets from all views let wOptions: { wid: string; view: string; tpl: string; name: string }[] = []; if (field.all) { - Object.keys(store.getState().visProject).forEach(view => - store.getState().visProject[view].widgets && Object.keys(store.getState().visProject[view].widgets) - .filter((wid: AnyWidgetId) => - (field.withGroups || !store.getState().visProject[view].widgets[wid].grouped) && - (field.withSelf || wid !== widgetId) && - (!field.hideUsed || !store.getState().visProject[view].widgets[wid].usedInView)) - .forEach((wid: AnyWidgetId) => wOptions.push({ - wid, - view, - tpl: store.getState().visProject[view].widgets[wid].tpl, - name: store.getState().visProject[view].widgets[wid].name, - }))); + Object.keys(store.getState().visProject).forEach( + view => + store.getState().visProject[view].widgets && + Object.keys(store.getState().visProject[view].widgets) + .filter( + (wid: AnyWidgetId) => + (field.withGroups || !store.getState().visProject[view].widgets[wid].grouped) && + (field.withSelf || wid !== widgetId) && + (!field.hideUsed || !store.getState().visProject[view].widgets[wid].usedInView), + ) + .forEach((wid: AnyWidgetId) => + wOptions.push({ + wid, + view, + tpl: store.getState().visProject[view].widgets[wid].tpl, + name: store.getState().visProject[view].widgets[wid].name, + }), + ), + ); } else { wOptions = Object.keys(store.getState().visProject[props.selectedView].widgets) - .filter((wid: AnyWidgetId) => - (field.withGroups || !store.getState().visProject[props.selectedView].widgets[wid].grouped) && - (field.withSelf || wid !== widgetId) && - (!field.hideUsed || !store.getState().visProject[props.selectedView].widgets[wid].usedInView)) + .filter( + (wid: AnyWidgetId) => + (field.withGroups || + !store.getState().visProject[props.selectedView].widgets[wid].grouped) && + (field.withSelf || wid !== widgetId) && + (!field.hideUsed || + !store.getState().visProject[props.selectedView].widgets[wid].usedInView), + ) .map((wid: AnyWidgetId) => ({ wid, view: props.selectedView, @@ -1030,179 +1148,235 @@ const WidgetField = (props: WidgetFieldProps) => { const withIcons = !!options.find(item => (item as RxFieldOption)?.icon); - return 1)} + value={value} + defaultValue={field.default} + sx={{ + ...commonStyles.clearPadding, + '& .MuiSelect-select': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + onChange={(e: SelectChangeEvent) => { + if ( + field.type === 'widget' && + field.checkUsage && + e.target.value && + store.getState().visProject[props.selectedView].widgets[e.target.value as AnyWidgetId] + ?.usedInWidget + ) { + // Show dialog + setAskForUsage({ + wid: e.target.value, + cb: () => { + const project = modifyWidgetUsages( + store.getState().visProject, + props.selectedView, + e.target.value as AnyWidgetId, + props.selectedWidgets[0], + field.name, + ); + props.changeProject(project); + store.dispatch(recalculateFields(true)); + }, + }); + } else { + change(e.target.value); + store.dispatch(recalculateFields(true)); } - return text; - } - return field.type === 'select' && !field.noTranslation ? t(_value) : _value; - }} - fullWidth - > - {options.map((selectItem, i) => { + if (typeof options[0] === 'object') { + const item: RxFieldOption | null = options.find( + o => (o as RxFieldOption).value === _value, + ) as RxFieldOption; + const text = item + ? field.type === 'select' && !field.noTranslation + ? t(item.label) + : item.label + : _value; + if (withIcons && item.icon) { + return ( + <> + + {text} + + ); + } + return text; + } + return field.type === 'select' && !field.noTranslation ? t(_value) : _value; + }} + fullWidth > - {(selectItem as RxFieldOption).icon ? - - - : - (withIcons ?
: null)} - - {selectItem === '' ? - {t('attr_none')} - : - (field.type === 'select' && !field.noTranslation ? - (typeof selectItem === 'object' ? - {field.noTranslation ? (selectItem as RxFieldOption).label : t((selectItem as RxFieldOption).label)} : t(selectItem as string) - ) : (typeof selectItem === 'object' ? - {(selectItem as RxFieldOption).label} : (selectItem as string) - ))} - - )} - ; + {options.map((selectItem, i) => ( + + {(selectItem as RxFieldOption).icon ? ( + + + + ) : withIcons ? ( + +
+ + ) : null} + + {selectItem === '' ? ( + {t('attr_none')} + ) : field.type === 'select' && !field.noTranslation ? ( + typeof selectItem === 'object' ? ( + + {field.noTranslation ? selectItem.label : t(selectItem.label)} + + ) : ( + t(selectItem) + ) + ) : typeof selectItem === 'object' ? ( + + {selectItem.label} + + ) : ( + selectItem + )} + + + ))} + + ); } if (field.type === 'select-views') { - const options = getViewOptions(store.getState().visProject, [], null, 0) - .filter(option => option.type === 'folder' || option.view !== props.selectedView); - - const views: string[] = value as any as string[] || []; - - return { + if (Array.isArray(selected)) { + return selected.join(', '); + } + return selected; + }} + sx={{ + ...commonStyles.clearPadding, + '& .MuiSelect-select': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + onChange={e => { + if (Array.isArray(e.target.value)) { + const values = e.target.value.filter(tt => tt); + if (values.length) { + change(e.target.value.filter(tt => tt).join(',')); + } else { + change(null); + } } else { - change(null); + change(e.target.value); } - } else { - change(e.target.value); - } - }} - fullWidth - > - {options.map((option, key) => (option.type === 'view' ? - - - - {field.multiple !== false ? - : null} - - - - : - - - {option.folder.name} - ))} - ; + }} + fullWidth + > + {options.map((option, key) => + option.type === 'view' ? ( + + + + {field.multiple !== false ? : null} + + + + ) : ( + + + {option.folder.name} + + ), + )} + + ); } if (field.type === 'groups') { - const groups: string[] = value as any as string[] || []; - return ; + {Object.values(props.userGroups).map((group, i) => ( + + + + + ))} + + ); } - if (field.type === 'auto' || field.type === 'class' || field.type === 'filters') { + if (field.type === 'auto' || field.type === 'class' || field.type === 'filters') { let options = field.options; if (field.type === 'class') { options = Object.keys(window.collectClassesValue).filter(cssClass => cssClass.match(/^vis-style-/)); @@ -1214,133 +1388,159 @@ const WidgetField = (props: WidgetFieldProps) => { options = (options as RxFieldOption[]).map(item => item.value); } - return change(inputValue)} - onChange={(_e, inputValue) => change(inputValue)} - sx={{ - '& .MuiInput-root': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - renderOption={field.name === 'font-family' || field.name === 'lc-font-family' ? - (optionProps, option) =>
  • - {option} -
  • : null} - renderInput={params => } - />; + options={(options as string[]) || []} + inputValue={(value as string) || ''} + value={value || ''} + onInputChange={(_e, inputValue) => change(inputValue)} + onChange={(_e, inputValue) => change(inputValue)} + sx={{ + '& .MuiInput-root': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + renderOption={ + field.name === 'font-family' || field.name === 'lc-font-family' + ? (optionProps, option) => ( +
  • + {option} +
  • + ) + : null + } + renderInput={params => ( + + )} + /> + ); } - if (field.type === 'views') { + if (field.type === 'views') { const options = getViewOptions(store.getState().visProject); - return change(inputValue)} - onChange={(_e, inputValue) => { - if (typeof inputValue === 'object' && inputValue !== null) { - inputValue = inputValue.type === 'view' ? inputValue.view : inputValue.folder.name; - } - change(inputValue); - }} - sx={{ - '& .MuiInput-root': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - getOptionLabel={option => { - if (typeof option === 'string') { - return option; - } - return option.type === 'view' ? option.label : option.folder.name; - }} - getOptionDisabled={option => option.type === 'folder'} - renderOption={(optionProps, option) => - (option.type === 'view' ? - - - {option.label} - - : - - - {option.folder.name} - )} - renderInput={params => } - />; + // placeholder={isDifferent ? t('different') : null} + options={options || []} + inputValue={(value as string) || ''} + value={(value as string) || ''} + onInputChange={(_e, inputValue) => change(inputValue)} + onChange={(_e, inputValue) => { + if (typeof inputValue === 'object' && inputValue !== null) { + inputValue = inputValue.type === 'view' ? inputValue.view : inputValue.folder.name; + } + change(inputValue); + }} + sx={{ + '& .MuiInput-root': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + getOptionLabel={option => { + if (typeof option === 'string') { + return option; + } + return option.type === 'view' ? option.label : option.folder.name; + }} + getOptionDisabled={option => option.type === 'folder'} + renderOption={(optionProps, option) => + option.type === 'view' ? ( + + + {option.label} + + ) : ( + + + {option.folder.name} + + ) + } + renderInput={params => ( + + )} + /> + ); } if (field.type === 'style') { const stylesOptions = getStylesOptions({ - filterFile: field.filterFile, - filterName: field.filterName, + filterFile: field.filterFile, + filterName: field.filterName, filterAttrs: field.filterAttrs, - removeName: field.removeName, + removeName: field.removeName, }); - return ; + {Object.keys(stylesOptions).map((styleName, i) => ( + + + + + {t(stylesOptions[styleName].name)} + + ))} + + ); } if (field.type === 'custom') { @@ -1352,14 +1552,13 @@ const WidgetField = (props: WidgetFieldProps) => { newData => { const _project = deepClone(store.getState().visProject); props.selectedWidgets.forEach(selectedWidget => { - Object.keys(newData) - .forEach(attr => { - if (newData[attr] === null) { - delete _project[props.selectedView].widgets[selectedWidget].data[attr]; - } else { - _project[props.selectedView].widgets[selectedWidget].data[attr] = newData[attr]; - } - }); + Object.keys(newData).forEach(attr => { + if (newData[attr] === null) { + delete _project[props.selectedView].widgets[selectedWidget].data[attr]; + } else { + _project[props.selectedView].widgets[selectedWidget].data[attr] = newData[attr]; + } + }); }); props.changeProject(_project); store.dispatch(recalculateFields(true)); @@ -1375,199 +1574,244 @@ const WidgetField = (props: WidgetFieldProps) => { }, selectedView: props.selectedView, selectedWidgets: props.selectedWidgets, - selectedWidget: props.selectedWidgets.length === 1 ? props.selectedWidgets[0] : props.selectedWidgets as any as AnyWidgetId, + selectedWidget: + props.selectedWidgets.length === 1 + ? props.selectedWidgets[0] + : (props.selectedWidgets as any as AnyWidgetId), }, ); } catch (e) { console.error(`Cannot render custom field ${field.name}: ${e}`); } } else { - return <> - {field.type} - / - {value} - ; + return ( + <> + {field.type}/{value} + + ); } } if (field.type === 'instance' || field.type === 'history') { - return ; - } - - if (field.type === 'icon') { - return change(fileBlob)} - previewStyle={commonStyles.iconPreview} - />; - } - - if (field.type === 'icon64') { - return
    - change(e.target.value)} - InputProps={{ - endAdornment: value ? change('')} - > - - : null, - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + defaultValue={field.default} + sx={{ + ...commonStyles.clearPadding, + '& .MuiSelect-select': { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, }} - /> - - {idDialog && { - setIdDialog(false); - if (icon !== null) { - change(icon); - } - }} - />} -
    ; + {instances.map(_instance => ( + + + + + {field.isShort ? _instance.idShort : _instance.id} + + ))} + + ); } - if (field.type === 'html' || field.type === 'json' || (field.type === 'text' && field.noButton === false)) { - return <> - change(e.target.value)} - InputProps={{ - endAdornment: field.noButton ? null : , - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - rows={2} + onChange={fileBlob => change(fileBlob)} + previewStyle={commonStyles.iconPreview} /> - {idDialog ? change(newValue)} - onClose={() => setIdDialog(false)} - themeType={props.themeType} - type={field.type} - /> : null} - ; + ); } - if (!field.type || field.type === 'number' || field.type === 'password' || field.type === 'text' || field.type === 'url') { - return <> - setTextDialogFocused(true)} - onBlur={() => { - setTextDialogFocused(false); - if (field.type === 'number' && value) { - let _value: number = value as number; - if (typeof _value === 'string') { - _value = parseFloat(_value); + if (field.type === 'icon64') { + return ( +
    + change(e.target.value)} + InputProps={{ + endAdornment: value ? ( + change('')} + > + + + ) : null, + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + /> + + {idDialog && ( + { + setIdDialog(false); + if (icon !== null) { + change(icon); + } + }} + /> + )} +
    + ); + } + + if (field.type === 'html' || field.type === 'json' || (field.type === 'text' && field.noButton === false)) { + return ( + <> + change(e.target.value)} + InputProps={{ + endAdornment: field.noButton ? null : ( + + ), + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + rows={2} + /> + {idDialog ? ( + change(newValue)} + onClose={() => setIdDialog(false)} + themeType={props.themeType} + type={field.type} + /> + ) : null} + + ); + } + + if ( + !field.type || + field.type === 'number' || + field.type === 'password' || + field.type === 'text' || + field.type === 'url' + ) { + return ( + <> + setTextDialogFocused(true)} + onBlur={() => { + setTextDialogFocused(false); + if (field.type === 'number' && value) { + let _value: number = value as number; + if (typeof _value === 'string') { + _value = parseFloat(_value); + if (field.min !== undefined) { + if (_value < field.min) { + _value = field.min; + } + } + if (field.max !== undefined) { + if (_value > field.max) { + _value = field.max; + } + } + change(_value); + return; + } if (field.min !== undefined) { if (_value < field.min) { - _value = field.min; + change(field.min); + return; } } if (field.max !== undefined) { if (_value > field.max) { - _value = field.max; + change(field.max); } } - change(_value); - return; - } - if (field.min !== undefined) { - if (_value < field.min) { - change(field.min); - return; - } } - if (field.max !== undefined) { - if (_value > field.max) { - change(field.max); - } - } - } - }} - placeholder={isDifferent ? t('different') : null} - InputProps={{ - endAdornment: field.clearButton && cachedValue !== null && cachedValue !== undefined ? change(null)}> - - : null, - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - value={value} - onChange={e => change(e.target.value)} - type={field.type ? field.type : 'text'} - // eslint-disable-next-line react/jsx-no-duplicate-props - inputProps={{ - min: field.min, - max: field.max, - step: field.step, - }} - /> - {urlPopper} - ; + }} + placeholder={isDifferent ? t('different') : null} + InputProps={{ + endAdornment: + field.clearButton && cachedValue !== null && cachedValue !== undefined ? ( + change(null)} + > + + + ) : null, + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + value={value} + onChange={e => change(e.target.value)} + type={field.type ? field.type : 'text'} + // eslint-disable-next-line react/jsx-no-duplicate-props + inputProps={{ + min: field.min, + max: field.max, + step: field.step, + }} + /> + {urlPopper} + + ); } return `${field.type}/${value}`; diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetJS.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetJS.tsx index 1207b5d98..d7d1ed19a 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetJS.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/WidgetJS.tsx @@ -1,8 +1,6 @@ import React, { useRef, useState } from 'react'; -import { - Dialog, DialogTitle, DialogContent, DialogActions, Button, -} from '@mui/material'; +import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material'; import { Close } from '@mui/icons-material'; @@ -19,42 +17,48 @@ interface WidgetJSProps { widget: Widget; } -const WidgetJS = (props: WidgetJSProps) => { +const WidgetJS = (props: WidgetJSProps): React.JSX.Element => { const [value, setValue] = useState(props.widget.js || ''); const timeout = useRef(null); - return - {I18n.t('Widget\'s script')} - - { - setValue(v); - if (timeout.current) { - clearTimeout(timeout.current); - timeout.current = null; - } - timeout.current = setTimeout(() => { - timeout.current = null; - props.onChange(v); - }, 1000); - }} - /> - - - - - ; + return ( + + {I18n.t("Widget's script")} + + { + setValue(v); + if (timeout.current) { + clearTimeout(timeout.current); + timeout.current = null; + } + timeout.current = setTimeout(() => { + timeout.current = null; + props.onChange(v); + }, 1000); + }} + /> + + + + + + ); }; export default WidgetJS; diff --git a/packages/iobroker.vis-2/src/src/Attributes/Widget/index.tsx b/packages/iobroker.vis-2/src/src/Attributes/Widget/index.tsx index 077995383..fea1d05e5 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/Widget/index.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/Widget/index.tsx @@ -1,9 +1,15 @@ import React, { Component } from 'react'; import { - Accordion, AccordionDetails, AccordionSummary, - Checkbox, Divider, Button, IconButton, - Tooltip, Box, + Accordion, + AccordionDetails, + AccordionSummary, + Checkbox, + Divider, + Button, + IconButton, + Tooltip, + Box, } from '@mui/material'; import { @@ -18,32 +24,31 @@ import { Delete, Add, LinkOff, - Link as LinkIcon, ContentCopy, + Link as LinkIcon, + ContentCopy, } from '@mui/icons-material'; -import { I18n, Icon, Utils } from '@iobroker/adapter-react-v5'; -import type { - LegacyConnection, - ThemeType, -} from '@iobroker/adapter-react-v5'; +import { I18n, Icon, Utils, type LegacyConnection, type ThemeType } from '@iobroker/adapter-react-v5'; -import { - store, recalculateFields, updateWidget, selectWidget, -} from '@/Store'; +import { store, recalculateFields, updateWidget, selectWidget } from '@/Store'; -import type { - WidgetAttributeInfoStored, WidgetAttributeIterable, - WidgetAttributesGroupInfoStored, WidgetType, -} from '@/Vis/visWidgetsCatalog'; import { - getWidgetTypes, parseAttributes, + type WidgetAttributeInfoStored, + type WidgetAttributeIterable, + type WidgetAttributesGroupInfoStored, + type WidgetType, + getWidgetTypes, + parseAttributes, } from '@/Vis/visWidgetsCatalog'; import { deepClone } from '@/Utils/utils'; import type { AnyWidgetId, Project, Widget as SingleGroupWidget, - VisTheme, WidgetData, WidgetStyle, GroupData, + VisTheme, + WidgetData, + WidgetStyle, + GroupData, RxWidgetInfoGroup, } from '@iobroker/types-vis-2'; @@ -298,13 +303,13 @@ class Widget extends Component { accordionOpen = JSON.parse(accordionOpenStr) as Record; // convert from old Object.keys(accordionOpen).forEach(key => { - if (accordionOpen[key] as any === true || accordionOpen[key] === 1) { + if ((accordionOpen[key] as any) === true || accordionOpen[key] === 1) { accordionOpen[key] = 1; } else { accordionOpen[key] = 0; } }); - } catch (e) { + } catch { accordionOpen = {}; } } else { @@ -354,15 +359,23 @@ class Widget extends Component { }, { name: 'visibility', - fields: [{ name: 'visibility-oid', type: 'id' }, + fields: [ + { name: 'visibility-oid', type: 'id' }, { - name: 'visibility-cond', type: 'select', options: ['==', '!=', '<=', '>=', '<', '>', 'consist', 'not consist', 'exist', 'not exist'], default: '==', + name: 'visibility-cond', + type: 'select', + options: ['==', '!=', '<=', '>=', '<', '>', 'consist', 'not consist', 'exist', 'not exist'], + default: '==', }, { name: 'visibility-val', default: 1 }, { name: 'visibility-groups', type: 'groups' }, { - name: 'visibility-groups-action', type: 'select', options: ['hide', 'disabled'], default: 'hide', - }], + name: 'visibility-groups-action', + type: 'select', + options: ['hide', 'disabled'], + default: 'hide', + }, + ], }, ]; } @@ -396,16 +409,31 @@ class Widget extends Component { }, { name: `signals-val-${i}`, default: true }, { - name: `signals-icon-${i}`, type: 'image', default: '', hidden: `!!data["signals-smallIcon-${i}"]`, // `${adapterName}/signals/lowbattery.png` }, + name: `signals-icon-${i}`, + type: 'image', + default: '', + hidden: `!!data["signals-smallIcon-${i}"]`, // `${adapterName}/signals/lowbattery.png` }, }, { - name: `signals-smallIcon-${i}`, type: 'icon64', default: '', label: `signals-smallIcon-${i}`, hidden: `!!data["signals-icon-${i}"]`, + name: `signals-smallIcon-${i}`, + type: 'icon64', + default: '', + label: `signals-smallIcon-${i}`, + hidden: `!!data["signals-icon-${i}"]`, }, { - name: `signals-color-${i}`, type: 'color', default: '', hidden: `!data["signals-smallIcon-${i}"] && !data["signals-text-${i}"]`, + name: `signals-color-${i}`, + type: 'color', + default: '', + hidden: `!data["signals-smallIcon-${i}"] && !data["signals-text-${i}"]`, }, // `${adapterName}/signals/lowbattery.png` }, { - name: `signals-icon-size-${i}`, type: 'slider', min: 1, max: 120, step: 1, default: 0, + name: `signals-icon-size-${i}`, + type: 'slider', + min: 1, + max: 120, + step: 1, + default: 0, }, { name: `signals-icon-style-${i}` }, { name: `signals-text-${i}` }, @@ -413,10 +441,20 @@ class Widget extends Component { { name: `signals-text-class-${i}` }, { name: `signals-blink-${i}`, type: 'checkbox', default: false }, { - name: `signals-horz-${i}`, type: 'slider', min: -20, max: 120, step: 1, default: 0, + name: `signals-horz-${i}`, + type: 'slider', + min: -20, + max: 120, + step: 1, + default: 0, }, { - name: `signals-vert-${i}`, type: 'slider', min: -20, max: 120, step: 1, default: 0, + name: `signals-vert-${i}`, + type: 'slider', + min: -20, + max: 120, + step: 1, + default: 0, }, { name: `signals-hide-edit-${i}`, type: 'checkbox', default: false }, ]; @@ -440,46 +478,152 @@ class Widget extends Component { { name: 'width', type: 'dimension' }, { name: 'height', type: 'dimension' }, { - name: 'z-index', type: 'number', min: -200, max: 200, + name: 'z-index', + type: 'number', + min: -200, + max: 200, + }, + { + name: 'overflow-x', + type: 'nselect', + options: ['', 'visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit'], + }, + { + name: 'overflow-y', + type: 'nselect', + options: ['', 'visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit'], }, - { name: 'overflow-x', type: 'nselect', options: ['', 'visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit'] }, - { name: 'overflow-y', type: 'nselect', options: ['', 'visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit'] }, { name: 'opacity', type: 'text' }, - { name: 'cursor', type: 'auto', options: ['alias', 'all-scroll', 'auto', 'cell', 'col-resize', 'context-menu', 'copy', 'crosshair', 'default', 'e-resize', 'ew-resize', 'grab', 'grabbing', 'help', 'move', 'n-resize', 'ne-resize', 'nesw-resize', 'ns-resize', 'nw-resize', 'nwse-resize', 'no-drop', 'none', 'not-allowed', 'pointer', 'progress', 'row-resize', 's-resize', 'se-resize', 'sw-resize', 'text', 'vertical-text', 'w-resize', 'wait', 'zoom-in', 'zoom-out', 'initial', 'inherit'] }, + { + name: 'cursor', + type: 'auto', + options: [ + 'alias', + 'all-scroll', + 'auto', + 'cell', + 'col-resize', + 'context-menu', + 'copy', + 'crosshair', + 'default', + 'e-resize', + 'ew-resize', + 'grab', + 'grabbing', + 'help', + 'move', + 'n-resize', + 'ne-resize', + 'nesw-resize', + 'ns-resize', + 'nw-resize', + 'nwse-resize', + 'no-drop', + 'none', + 'not-allowed', + 'pointer', + 'progress', + 'row-resize', + 's-resize', + 'se-resize', + 'sw-resize', + 'text', + 'vertical-text', + 'w-resize', + 'wait', + 'zoom-in', + 'zoom-out', + 'initial', + 'inherit', + ], + }, { name: 'transform' }, ], }, { name: 'css_font_text', isStyle: true, - fields: [{ name: 'color', type: 'color' }, - { name: 'text-align', type: 'nselect', options: ['', 'left', 'right', 'center', 'justify', 'initial', 'inherit'] }, + fields: [ + { name: 'color', type: 'color' }, + { + name: 'text-align', + type: 'nselect', + options: ['', 'left', 'right', 'center', 'justify', 'initial', 'inherit'], + }, { name: 'text-shadow' }, { name: 'font-family', type: 'auto', options: fonts, }, - { name: 'font-style', type: 'nselect', options: ['', 'normal', 'italic', 'oblique', 'initial', 'inherit'] }, - { name: 'font-variant', type: 'nselect', options: ['', 'normal', 'small-caps', 'initial', 'inherit'] }, - { name: 'font-weight', type: 'auto', options: ['normal', 'bold', 'bolder', 'lighter', 'initial', 'inherit'] }, - { name: 'font-size', type: 'auto', options: ['medium', 'xx-small', 'x-small', 'small', 'large', 'x-large', 'xx-large', 'smaller', 'larger', 'initial', 'inherit'] }, + { + name: 'font-style', + type: 'nselect', + options: ['', 'normal', 'italic', 'oblique', 'initial', 'inherit'], + }, + { + name: 'font-variant', + type: 'nselect', + options: ['', 'normal', 'small-caps', 'initial', 'inherit'], + }, + { + name: 'font-weight', + type: 'auto', + options: ['normal', 'bold', 'bolder', 'lighter', 'initial', 'inherit'], + }, + { + name: 'font-size', + type: 'auto', + options: [ + 'medium', + 'xx-small', + 'x-small', + 'small', + 'large', + 'x-large', + 'xx-large', + 'smaller', + 'larger', + 'initial', + 'inherit', + ], + }, { name: 'line-height' }, { name: 'letter-spacing' }, - { name: 'word-spacing' }], + { name: 'word-spacing' }, + ], }, { name: 'css_background', isStyle: true, - fields: [{ name: 'background' }, + fields: [ + { name: 'background' }, { name: 'background-color', type: 'color' }, { name: 'background-image' }, - { name: 'background-repeat', type: 'nselect', options: ['', 'repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'initial', 'inherit'] }, - { name: 'background-attachment', type: 'nselect', options: ['', 'scroll', 'fixed', 'local', 'initial', 'inherit'] }, + { + name: 'background-repeat', + type: 'nselect', + options: ['', 'repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'initial', 'inherit'], + }, + { + name: 'background-attachment', + type: 'nselect', + options: ['', 'scroll', 'fixed', 'local', 'initial', 'inherit'], + }, { name: 'background-position' }, { name: 'background-size' }, - { name: 'background-clip', type: 'nselect', options: ['', 'border-box', 'padding-box', 'content-box', 'initial', 'inherit'] }, - { name: 'background-origin', type: 'nselect', options: ['', 'padding-box', 'border-box', 'content-box', 'initial', 'inherit'] }], + { + name: 'background-clip', + type: 'nselect', + options: ['', 'border-box', 'padding-box', 'content-box', 'initial', 'inherit'], + }, + { + name: 'background-origin', + type: 'nselect', + options: ['', 'padding-box', 'border-box', 'content-box', 'initial', 'inherit'], + }, + ], }, { name: 'css_border', @@ -487,14 +631,31 @@ class Widget extends Component { fields: [ // { name: 'box-sizing', type: 'nselect', options: ['', 'border-box', 'content-box'] }, { name: 'border-width' }, - { name: 'border-style', type: 'nselect', options: ['', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset', 'hidden'] }, + { + name: 'border-style', + type: 'nselect', + options: [ + '', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset', + 'hidden', + ], + }, { name: 'border-color', type: 'color' }, - { name: 'border-radius' }], + { name: 'border-radius' }, + ], }, { name: 'css_shadow_padding', isStyle: true, - fields: [{ name: 'padding' }, + fields: [ + { name: 'padding' }, { name: 'padding-left' }, { name: 'padding-top' }, { name: 'padding-right' }, @@ -503,56 +664,141 @@ class Widget extends Component { { name: 'margin-left' }, { name: 'margin-top' }, { name: 'margin-right' }, - { name: 'margin-bottom' }], + { name: 'margin-bottom' }, + ], }, { name: 'last_change', fields: [ { name: 'lc-oid', type: 'id' }, { - name: 'lc-type', type: 'select', options: ['last-change', 'timestamp'], default: 'last-change', + name: 'lc-type', + type: 'select', + options: ['last-change', 'timestamp'], + default: 'last-change', }, { name: 'lc-is-interval', type: 'checkbox', default: true }, { name: 'lc-is-moment', type: 'checkbox', default: false }, { - name: 'lc-format', type: 'auto', options: ['YYYY.MM.DD hh:mm:ss', 'DD.MM.YYYY hh:mm:ss', 'YYYY.MM.DD', 'DD.MM.YYYY', 'YYYY/MM/DD hh:mm:ss', 'YYYY/MM/DD', 'hh:mm:ss'], default: '', + name: 'lc-format', + type: 'auto', + options: [ + 'YYYY.MM.DD hh:mm:ss', + 'DD.MM.YYYY hh:mm:ss', + 'YYYY.MM.DD', + 'DD.MM.YYYY', + 'YYYY/MM/DD hh:mm:ss', + 'YYYY/MM/DD', + 'hh:mm:ss', + ], + default: '', }, { - name: 'lc-position-vert', type: 'select', options: ['top', 'middle', 'bottom'], default: 'top', + name: 'lc-position-vert', + type: 'select', + options: ['top', 'middle', 'bottom'], + default: 'top', }, { - name: 'lc-position-horz', type: 'select', options: ['left', /* 'middle', */'right'], default: 'right', + name: 'lc-position-horz', + type: 'select', + options: ['left', /* 'middle', */ 'right'], + default: 'right', }, { - name: 'lc-offset-vert', type: 'slider', min: -120, max: 120, step: 1, default: 0, + name: 'lc-offset-vert', + type: 'slider', + min: -120, + max: 120, + step: 1, + default: 0, }, { - name: 'lc-offset-horz', type: 'slider', min: -120, max: 120, step: 1, default: 0, + name: 'lc-offset-horz', + type: 'slider', + min: -120, + max: 120, + step: 1, + default: 0, }, { - name: 'lc-font-size', type: 'auto', options: ['', 'medium', 'xx-small', 'x-small', 'small', 'large', 'x-large', 'xx-large', 'smaller', 'larger', 'initial', 'inherit'], default: '12px', + name: 'lc-font-size', + type: 'auto', + options: [ + '', + 'medium', + 'xx-small', + 'x-small', + 'small', + 'large', + 'x-large', + 'xx-large', + 'smaller', + 'larger', + 'initial', + 'inherit', + ], + default: '12px', }, { - name: 'lc-font-family', type: 'auto', default: '', options: fonts, + name: 'lc-font-family', + type: 'auto', + default: '', + options: fonts, }, { - name: 'lc-font-style', type: 'auto', options: ['', 'normal', 'italic', 'oblique', 'initial', 'inherit'], default: '', + name: 'lc-font-style', + type: 'auto', + options: ['', 'normal', 'italic', 'oblique', 'initial', 'inherit'], + default: '', }, { name: 'lc-bkg-color', type: 'color', default: '' }, { name: 'lc-color', type: 'color', default: '' }, { name: 'lc-border-width', default: '0' }, { - name: 'lc-border-style', type: 'auto', options: ['', 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset', 'initial', 'inherit'], default: '', + name: 'lc-border-style', + type: 'auto', + options: [ + '', + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset', + 'initial', + 'inherit', + ], + default: '', }, { name: 'lc-border-color', type: 'color', default: '' }, { - name: 'lc-border-radius', type: 'slider', min: 0, max: 20, step: 1, default: 10, + name: 'lc-border-radius', + type: 'slider', + min: 0, + max: 20, + step: 1, + default: 10, }, { - name: 'lc-padding', type: 'slider', min: 0, max: 20, step: 1, default: 3, + name: 'lc-padding', + type: 'slider', + min: 0, + max: 20, + step: 1, + default: 3, }, { - name: 'lc-zindex', type: 'slider', min: -10, max: 20, step: 1, default: 0, + name: 'lc-zindex', + type: 'slider', + min: -10, + max: 20, + step: 1, + default: 0, }, ], }, @@ -568,7 +814,7 @@ class Widget extends Component { funcText: string | ((data: Record, index: number, style: React.CSSProperties) => boolean), project: Project, selectedView: string, - selectedWidgets: (AnyWidgetId)[], + selectedWidgets: AnyWidgetId[], index: number, ): boolean { try { @@ -604,14 +850,14 @@ class Widget extends Component { return false; } - componentDidMount() { + componentDidMount(): void { if (this.state.widgetsLoaded) { this.setState({ widgetTypes: getWidgetTypes() }, () => this.recalculateFields()); } this.setAccordionState(); } - static getDerivedStateFromProps(props: WidgetProps, state: WidgetState) { + static getDerivedStateFromProps(props: WidgetProps, state: WidgetState): Partial | null { let newState: Partial | null = null; if (props.widgetsLoaded && !state.widgetsLoaded) { newState = {}; @@ -623,7 +869,7 @@ class Widget extends Component { return newState; } - recalculateFields() { + recalculateFields(): void { if (!this.state.widgetTypes) { return; } @@ -633,41 +879,51 @@ class Widget extends Component { let widget: SingleGroupWidget; let widgetType: WidgetType | undefined; const commonFields: Record = {}; - const commonGroups: { common: number; [groupName: string]: number } = { common: this.props.selectedWidgets.length }; + const commonGroups: { common: number; [groupName: string]: number } = { + common: this.props.selectedWidgets.length, + }; const selectedWidgetsFields: WidgetAttributesGroupInfoStored[][] = []; - widgets && this.props.selectedWidgets.forEach((wid, widgetIndex) => { - widget = widgets[wid]; - if (!widget) { - return; - } + widgets && + this.props.selectedWidgets.forEach((wid, widgetIndex) => { + widget = widgets[wid]; + if (!widget) { + return; + } - widgetType = this.state.widgetTypes.find(type => type.name === widget.tpl); - if (!widgetType) { - return; - } + widgetType = this.state.widgetTypes.find(type => type.name === widget.tpl); + if (!widgetType) { + return; + } - let params: string | RxWidgetInfoGroup[]; - if (typeof widgetType.params === 'function') { - params = widgetType.params(widget.data, null, { - views: store.getState().visProject, - view: this.props.selectedView, - socket: this.props.socket, - themeType: this.props.themeType, - projectName: this.props.projectName, - adapterName: this.props.adapterName, - instance: this.props.instance, - id: wid, - widget, - }); - } else { - params = widgetType.params as string | RxWidgetInfoGroup[]; - } + let params: string | RxWidgetInfoGroup[]; + if (typeof widgetType.params === 'function') { + params = widgetType.params(widget.data, null, { + views: store.getState().visProject, + view: this.props.selectedView, + socket: this.props.socket, + themeType: this.props.themeType, + projectName: this.props.projectName, + adapterName: this.props.adapterName, + instance: this.props.instance, + id: wid, + widget, + }); + } else { + params = widgetType.params as string | RxWidgetInfoGroup[]; + } - const fields: null | WidgetAttributesGroupInfoStored[] = parseAttributes(params, widgetIndex, commonGroups, commonFields, widgetType.set, widget.data); + const fields: null | WidgetAttributesGroupInfoStored[] = parseAttributes( + params, + widgetIndex, + commonGroups, + commonFields, + widgetType.set, + widget.data, + ); - fields && selectedWidgetsFields.push(fields); - }); + fields && selectedWidgetsFields.push(fields); + }); let fields: WidgetAttributesGroupInfoStored[]; const bindFields: string[] = []; @@ -676,14 +932,16 @@ class Widget extends Component { const newState: Partial = {}; if (this.props.selectedWidgets.length > 1) { - fields = selectedWidgetsFields[0]?.filter(group => { - if (commonGroups[group.name] < this.props.selectedWidgets.length) { - return false; - } - group.fields = group.fields.filter(field => - commonFields[`${group.name}.${field.name}`] === this.props.selectedWidgets.length); - return true; - }) || []; + fields = + selectedWidgetsFields[0]?.filter(group => { + if (commonGroups[group.name] < this.props.selectedWidgets.length) { + return false; + } + group.fields = group.fields.filter( + field => commonFields[`${group.name}.${field.name}`] === this.props.selectedWidgets.length, + ); + return true; + }) || []; this.props.selectedWidgets.forEach((wid, widgetIndex) => { const currentWidget = widgets[wid]; @@ -700,23 +958,35 @@ class Widget extends Component { isDifferent[field] = true; } }); - const anyStyle: Record = commonValues.style as Record; + const anyStyle: Record = + commonValues.style as Record; Object.keys(anyStyle).forEach(field => { - if (anyStyle[field] !== (currentWidget.style as Record)[field]) { + if ( + anyStyle[field] !== + (currentWidget.style as Record)[field] + ) { anyStyle[field] = null; isDifferent[field] = true; } }); - currentWidget.data.bindings?.forEach(attr => !bindFields.includes(`data_${attr}`) && bindFields.push(`data_${attr}`)); - currentWidget.style.bindings?.forEach(attr => !bindFields.includes(`style_${attr}`) && bindFields.push(`style_${attr}`)); + currentWidget.data.bindings?.forEach( + attr => !bindFields.includes(`data_${attr}`) && bindFields.push(`data_${attr}`), + ); + currentWidget.style.bindings?.forEach( + attr => !bindFields.includes(`style_${attr}`) && bindFields.push(`style_${attr}`), + ); } }); } else { fields = selectedWidgetsFields[0]; - widgets[this.props.selectedWidgets[0]].data.bindings?.forEach(attr => !bindFields.includes(`data_${attr}`) && bindFields.push(`data_${attr}`)); - widgets[this.props.selectedWidgets[0]].style.bindings?.forEach(attr => !bindFields.includes(`style_${attr}`) && bindFields.push(`style_${attr}`)); + widgets[this.props.selectedWidgets[0]].data.bindings?.forEach( + attr => !bindFields.includes(`data_${attr}`) && bindFields.push(`data_${attr}`), + ); + widgets[this.props.selectedWidgets[0]].style.bindings?.forEach( + attr => !bindFields.includes(`style_${attr}`) && bindFields.push(`style_${attr}`), + ); } newState.bindFields = bindFields.sort(); @@ -738,7 +1008,13 @@ class Widget extends Component { } signalsCount = i + 1; if (signalsCount > 1) { - store.dispatch(updateWidget({ widgetId: this.props.selectedWidgets[0], viewId: this.props.selectedView, data: { ...widget, data: { ...widget.data, 'signals-count': signalsCount } } })); + store.dispatch( + updateWidget({ + widgetId: this.props.selectedWidgets[0], + viewId: this.props.selectedView, + data: { ...widget, data: { ...widget.data, 'signals-count': signalsCount } }, + }), + ); } } else { signalsCount = parseInt(widgetData['signals-count'], 10); @@ -761,25 +1037,23 @@ class Widget extends Component { singleName: group.singleName, })); - newState.fields = [ - ...this.fieldsBefore, - ...customGroups, - ...fieldsAfter, - ...[fieldsSignals], - ]; + newState.fields = [...this.fieldsBefore, ...customGroups, ...fieldsAfter, ...[fieldsSignals]]; } - widgets && newState.fields?.forEach((group: PaletteGroup) => { - const type = group.isStyle ? 'style' : 'data'; - const found = this.props.selectedWidgets.find(selectedWidget => { - const fieldFound = group.fields.find(field => { - const fieldValue: string | number | boolean | null | undefined = (widgets[selectedWidget][type] as Record)[field.name]; - return fieldValue !== undefined; + widgets && + newState.fields?.forEach((group: PaletteGroup) => { + const type = group.isStyle ? 'style' : 'data'; + const found = this.props.selectedWidgets.find(selectedWidget => { + const fieldFound = group.fields.find(field => { + const fieldValue: string | number | boolean | null | undefined = ( + widgets[selectedWidget][type] as Record + )[field.name]; + return fieldValue !== undefined; + }); + return fieldFound !== undefined; }); - return fieldFound !== undefined; + group.hasValues = found !== undefined; }); - group.hasValues = found !== undefined; - }); newState.transitionTime = 0; @@ -787,7 +1061,7 @@ class Widget extends Component { store.dispatch(recalculateFields(false)); } - componentWillUnmount() { + componentWillUnmount(): void { this.recalculateTimer && clearTimeout(this.recalculateTimer); this.recalculateTimer = null; @@ -795,7 +1069,7 @@ class Widget extends Component { this.triggerTimer = null; } - renderHeader(widgets: Record) { + renderHeader(widgets: Record): React.JSX.Element { let list; // If selected only one widget, show its icon if (this.props.selectedWidgets.length === 1) { @@ -834,21 +1108,36 @@ class Widget extends Component { if (_widgetType?.preview?.startsWith('; + img = ( + {this.props.selectedWidgets[0]} + ); } - } else if (_widgetType?.preview && IMAGE_TYPES.find(ext => _widgetType.preview.toLowerCase().endsWith(ext))) { - img = {this.props.selectedWidgets[0]}; + } else if ( + _widgetType?.preview && + IMAGE_TYPES.find(ext => _widgetType.preview.toLowerCase().endsWith(ext)) + ) { + img = ( + {this.props.selectedWidgets[0]} + ); } if (!img) { - img = ; + img = ( + + ); } let widgetBackColor; @@ -867,65 +1156,86 @@ class Widget extends Component { setLabel = `${widgets[this.props.selectedWidgets[0]].marketplace.name}`; widgetLabel = `${I18n.t('version')} ${widgets[this.props.selectedWidgets[0]].marketplace.version}`; } - list =
    - {widgetIcon ?
    {img}
    : null} -
    {this.props.selectedWidgets[0]}
    -
    -
    - {setLabel} + list = ( +
    + {widgetIcon ?
    {img}
    : null} +
    {this.props.selectedWidgets[0]}
    +
    +
    + {setLabel} +
    +
    {widgetLabel}
    -
    {widgetLabel}
    + {!widgets[this.props.selectedWidgets[0]].marketplace && ( + <> + {window.location.port === '3000' ? ( + + ) : null} + {window.location.port === '3000' ? ( + + ) : null} + {this.state.cssDialogOpened ? ( + this.setState({ cssDialogOpened: false })} + widget={widgets[this.props.selectedWidgets[0]]} + onChange={value => { + const project = deepClone(store.getState().visProject); + project[this.props.selectedView].widgets[this.props.selectedWidgets[0]].css = + value; + this.props.changeProject(project); + }} + themeType={this.props.themeType} + editMode={this.props.editMode} + /> + ) : null} + {this.state.jsDialogOpened ? ( + this.setState({ jsDialogOpened: false })} + widget={widgets[this.props.selectedWidgets[0]]} + onChange={value => { + const project = deepClone(store.getState().visProject); + project[this.props.selectedView].widgets[this.props.selectedWidgets[0]].js = + value; + this.props.changeProject(project); + }} + themeType={this.props.themeType} + editMode={this.props.editMode} + /> + ) : null} + + )}
    - {!widgets[this.props.selectedWidgets[0]].marketplace && <> - {window.location.port === '3000' ? : null} - {window.location.port === '3000' ? : null} - {this.state.cssDialogOpened ? this.setState({ cssDialogOpened: false })} - widget={widgets[this.props.selectedWidgets[0]]} - onChange={value => { - const project = deepClone(store.getState().visProject); - project[this.props.selectedView].widgets[this.props.selectedWidgets[0]].css = value; - this.props.changeProject(project); - }} - themeType={this.props.themeType} - editMode={this.props.editMode} - /> : null} - {this.state.jsDialogOpened ? this.setState({ jsDialogOpened: false })} - widget={widgets[this.props.selectedWidgets[0]]} - onChange={value => { - const project = deepClone(store.getState().visProject); - project[this.props.selectedView].widgets[this.props.selectedWidgets[0]].js = value; - this.props.changeProject(project); - }} - themeType={this.props.themeType} - editMode={this.props.editMode} - /> : null} - } -
    ; + ); } else { list = this.props.selectedWidgets.join(', '); } - return
    - {list} -
    ; + return ( +
    + {list} +
    + ); } - setAccordionState(accordionOpen?: { [groupName: string]: 0 | 1 | 2 }, cb?: () => void) { + setAccordionState(accordionOpen?: { [groupName: string]: 0 | 1 | 2 }, cb?: () => void): void { if (!this.state.fields) { return; } const _accordionOpen = accordionOpen || this.state.accordionOpen; - const allOpened = !this.state.fields.find(group => _accordionOpen[group.name] === 0 || _accordionOpen[group.name] === 2); + const allOpened = !this.state.fields.find( + group => _accordionOpen[group.name] === 0 || _accordionOpen[group.name] === 2, + ); const allClosed = !this.state.fields.find(group => _accordionOpen[group.name] === 1); if (this.props.isAllClosed !== allClosed) { @@ -944,7 +1254,7 @@ class Widget extends Component { setTimeout(() => this.setState({ transitionTime: 200 }), 500); } - componentDidUpdate() { + componentDidUpdate(): void { // scale the old style HTML widget icon if (this.imageRef.current?.children[0]) { const height = this.imageRef.current.children[0].clientHeight; @@ -954,12 +1264,7 @@ class Widget extends Component { } } - onGroupMove( - e: React.MouseEvent, - index: number, - iterable: WidgetAttributeIterable, - direction: GroupAction, - ) { + onGroupMove(e: React.MouseEvent, index: number, iterable: WidgetAttributeIterable, direction: GroupAction): void { e.stopPropagation(); const project = deepClone(store.getState().visProject); const oldGroup = this.state.fields.find(f => f.name === `${iterable.group}-${index}`); @@ -969,7 +1274,7 @@ class Widget extends Component { // if deletion if (direction === 'delete') { if (iterable.indexTo) { - const lastGroup = this.state.fields.find(f => f.singleName === iterable.group && f.iterable?.isLast); + const lastGroup = this.state.fields.find(f => f.singleName === iterable.group && f.iterable?.isLast); for (let idx = index; idx < lastGroup.index; idx++) { const idxGroup = this.state.fields.find(f => f.name === `${iterable.group}-${idx}`); const idxGroupPlus = this.state.fields.find(f => f.name === `${iterable.group}-${idx + 1}`); @@ -977,8 +1282,10 @@ class Widget extends Component { this.props.selectedWidgets.forEach(wid => { const widgetData = _widgets[wid].data; // move all fields of the group to -1 - idxGroup.fields.forEach((_attr, i) => - widgetData[idxGroup.fields[i].name] = widgetData[idxGroupPlus.fields[i].name]); + idxGroup.fields.forEach( + (_attr, i) => + (widgetData[idxGroup.fields[i].name] = widgetData[idxGroupPlus.fields[i].name]), + ); // move the group-used flag widgetData[`g_${iterable.group}-${idx}`] = widgetData[`g_${iterable.group}-${idx + 1}`]; @@ -1087,7 +1394,9 @@ class Widget extends Component { // order all attributes for better readability const oldWidgetData = _widgets[wid].data; const widgetData: GroupData | WidgetData = {}; - Object.keys(oldWidgetData).sort().forEach(key => widgetData[key] = oldWidgetData[key]); + Object.keys(oldWidgetData) + .sort() + .forEach(key => (widgetData[key] = oldWidgetData[key])); _widgets[wid].data = widgetData; // switch all fields of the group @@ -1118,143 +1427,192 @@ class Widget extends Component { } } - renderGroupHeader(group: PaletteGroup) { - return :
    } - > -
    :
    } > -
    - {ICONS[`group.${group.singleName || group.name}`] ? ICONS[`group.${group.singleName || group.name}`] : null} - {group.label ? - I18n.t(group.label) + (group.index !== undefined ? ` [${group.index}]` : '') - : - (window.vis._(`group_${group.singleName || group.name}`) + (group.index !== undefined ? ` [${group.index}]` : ''))} -
    - {group.iterable ? <> -
    - {group.iterable.indexTo ? - this.onGroupMove(e, group.index, group.iterable, 'clone')} - > - - - : null} - {group.iterable.indexTo ? - this.onGroupMove(e, group.index, group.iterable, 'delete')} - > - - - : null} - {group.iterable.isFirst ? -
    : - - this.onGroupMove(e, group.index, group.iterable, 'up')} - > - - - } - {group.iterable.isLast ? - (group.iterable.indexTo ? - this.onGroupMove(e, group.index, group.iterable, 'add')} - > - - - :
    ) - : - - this.onGroupMove(e, group.index, group.iterable, 'down')} - > - - - } - : null} -
    - { - if (group.hasValues) { - const type = group.isStyle ? 'style' : 'data'; - // check is any attribute from this group is used - let found = false; - for (const selectedWidget of this.props.selectedWidgets) { - for (const groupField of group.fields) { - const value: number | string | boolean | null | undefined = (store.getState().visProject[this.props.selectedView].widgets[selectedWidget][type] as Record)[groupField.name]; - if (value !== null && value !== undefined) { - found = true; - break; +
    +
    + {ICONS[`group.${group.singleName || group.name}`] + ? ICONS[`group.${group.singleName || group.name}`] + : null} + {group.label + ? I18n.t(group.label) + (group.index !== undefined ? ` [${group.index}]` : '') + : window.vis._(`group_${group.singleName || group.name}`) + + (group.index !== undefined ? ` [${group.index}]` : '')} +
    + {group.iterable ? ( + <> +
    + {group.iterable.indexTo ? ( + + this.onGroupMove(e, group.index, group.iterable, 'clone')} + > + + + + ) : null} + {group.iterable.indexTo ? ( + + this.onGroupMove(e, group.index, group.iterable, 'delete')} + > + + + + ) : null} + {group.iterable.isFirst ? ( +
    + ) : ( + + this.onGroupMove(e, group.index, group.iterable, 'up')} + > + + + + )} + {group.iterable.isLast ? ( + group.iterable.indexTo ? ( + + this.onGroupMove(e, group.index, group.iterable, 'add')} + > + + + + ) : ( +
    + ) + ) : ( + + this.onGroupMove(e, group.index, group.iterable, 'down')} + > + + + + )} + + ) : null} +
    + { + if (group.hasValues) { + const type = group.isStyle ? 'style' : 'data'; + // check is any attribute from this group is used + let found = false; + for (const selectedWidget of this.props.selectedWidgets) { + for (const groupField of group.fields) { + const value: number | string | boolean | null | undefined = ( + store.getState().visProject[this.props.selectedView].widgets[ + selectedWidget + ][type] as Record + )[groupField.name]; + if (value !== null && value !== undefined) { + found = true; + break; + } } } - } - if (found) { - this.setState({ clearGroup: group }); + if (found) { + this.setState({ clearGroup: group }); + } else { + this.onGroupDelete(group); + } } else { - this.onGroupDelete(group); - } - } else { - const project = deepClone(store.getState().visProject); - const type = group.isStyle ? 'style' : 'data'; - this.props.selectedWidgets.forEach(wid => { - group.fields.forEach(field => { - const styleOrData: Record = project[this.props.selectedView].widgets[wid][type] as Record; - if (styleOrData[field.name] === undefined) { - styleOrData[field.name] = field.default || null; - } + const project = deepClone(store.getState().visProject); + const type = group.isStyle ? 'style' : 'data'; + this.props.selectedWidgets.forEach(wid => { + group.fields.forEach(field => { + const styleOrData: Record< + string, + number | string | boolean | null | undefined + > = project[this.props.selectedView].widgets[wid][type] as Record< + string, + number | string | boolean | null | undefined + >; + if (styleOrData[field.name] === undefined) { + styleOrData[field.name] = field.default || null; + } + }); + project[this.props.selectedView].widgets[wid].data[`g_${group.name}`] = true; }); - project[this.props.selectedView].widgets[wid].data[`g_${group.name}`] = true; - }); - const accordionOpen = { ...this.state.accordionOpen }; - accordionOpen[group.name] = 1; - this.setAccordionState(accordionOpen, () => this.props.changeProject(project)); - } - e.stopPropagation(); - store.dispatch(recalculateFields(true)); - }} - size="small" - sx={Utils.getStyle(this.props.theme, commonStyles.fieldContent, commonStyles.clearPadding, styles.checkBox)} - /> + const accordionOpen = { ...this.state.accordionOpen }; + accordionOpen[group.name] = 1; + this.setAccordionState(accordionOpen, () => this.props.changeProject(project)); + } + e.stopPropagation(); + store.dispatch(recalculateFields(true)); + }} + size="small" + sx={Utils.getStyle( + this.props.theme, + commonStyles.fieldContent, + commonStyles.clearPadding, + styles.checkBox, + )} + /> +
    -
    - ; + + ); } - changeBinding(isStyle: boolean, attr: string) { + changeBinding(isStyle: boolean, attr: string): void { const project = deepClone(store.getState().visProject); const type = isStyle ? 'style' : 'data'; for (const wid of this.props.selectedWidgets) { @@ -1270,7 +1628,11 @@ class Widget extends Component { store.dispatch(recalculateFields(true)); } - renderFieldRow(group: PaletteGroup, field: WidgetAttributeInfoStored, fieldIndex: number) { + renderFieldRow( + group: PaletteGroup, + field: WidgetAttributeInfoStored, + fieldIndex: number, + ): React.JSX.Element | null { if (!field) { return null; } @@ -1283,45 +1645,85 @@ class Widget extends Component { if (field.hidden === true) { return null; } - if (Widget.checkFunction(field.hidden, store.getState().visProject, this.props.selectedView, this.props.selectedWidgets, field.index)) { + if ( + Widget.checkFunction( + field.hidden, + store.getState().visProject, + this.props.selectedView, + this.props.selectedWidgets, + field.index, + ) + ) { return null; } } if (field.type === 'help') { - return - - {field.noTranslation ? field.text : I18n.t(field.text)} + return ( + + + {field.noTranslation ? field.text : I18n.t(field.text)} + - ; + ); } if (field.type === 'delimiter') { - return - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - ; + return ( + + + + ); } if (field.error) { - error = Widget.checkFunction(field.error, store.getState().visProject, this.props.selectedView, this.props.selectedWidgets, field.index); + error = Widget.checkFunction( + field.error, + store.getState().visProject, + this.props.selectedView, + this.props.selectedWidgets, + field.index, + ); } if (field.disabled) { if (field.disabled === true) { disabled = true; } else { - disabled = !!Widget.checkFunction(field.disabled, store.getState().visProject, this.props.selectedView, this.props.selectedWidgets, field.index); + disabled = !!Widget.checkFunction( + field.disabled, + store.getState().visProject, + this.props.selectedView, + this.props.selectedWidgets, + field.index, + ); } } let label; if (field.label === '') { label = ''; - // @ts-expect-error deprecated + // @ts-expect-error deprecated } else if (field.title) { // @ts-expect-error deprecated label = field.title; } else if (field.label) { label = I18n.t(field.label); } else { - label = window.vis._(field.singleName || field.name) + (field.index !== undefined ? ` [${field.index}]` : ''); + label = + window.vis._(field.singleName || field.name) + (field.index !== undefined ? ` [${field.index}]` : ''); } const labelStyle: React.CSSProperties = {}; @@ -1336,171 +1738,223 @@ class Widget extends Component { labelStyle.fontStyle = 'italic'; } - const isBoundField = this.state.bindFields.includes(group.isStyle ? `style_${field.name}` : `data_${field.name}`); + const isBoundField = this.state.bindFields.includes( + group.isStyle ? `style_${field.name}` : `data_${field.name}`, + ); // @ts-expect-error fix later if (field.type === 'delimiter') { - return - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - - ; + return ( + + + + + + ); } - return + return ( - {ICONS[field.singleName || field.name] ? ICONS[field.singleName || field.name] : null} - {label} - {field.type === 'image' && !this.state.isDifferent[field.name] && selectedWidget?.data[field.name] ? -
    - { - (e.target as HTMLImageElement).onerror = null; - (e.target as HTMLImageElement).style.display = 'none'; - }} - alt={field.name} - /> -
    : null} - {(field.type !== 'custom' || field.label) && !field.noBinding ? (isBoundField ? - - this.props.editMode && this.changeBinding(group.isStyle, field.name)} - style={{ ...styles.bindIcon, cursor: disabled ? 'default' : undefined }} - /> - : - - this.props.editMode && this.changeBinding(group.isStyle, field.name)} - /> - ) : null} - {group.isStyle ? - this.props.cssClone(field.name, newValue => { - if (newValue !== null && newValue !== undefined) { - const project = deepClone(store.getState().visProject); - this.props.selectedWidgets.forEach(wid => { - if (project[this.props.selectedView].widgets[wid]) { - project[this.props.selectedView].widgets[wid].style = project[this.props.selectedView].widgets[wid].style || {}; - (project[this.props.selectedView].widgets[wid].style as Record)[field.name] = newValue; - } - }); - this.props.changeProject(project); - } - })} - /> - : null} - {field.tooltip ? : null} -
    - -
    - {isBoundField ? - 1 ? this.state.commonValues : selectedWidget} - isStyle={group.isStyle} - selectedView={this.props.selectedView} - selectedWidgets={this.props.selectedWidgets} - isDifferent={this.state.isDifferent[field.name]} - socket={this.props.socket} - changeProject={this.props.changeProject} - /> - : 1 ? this.state.commonValues as SingleGroupWidget : selectedWidget} - widgetId={this.props.selectedWidgets.length > 1 ? null : this.props.selectedWidgets[0]} - isStyle={group.isStyle} - index={group.index} - isDifferent={this.state.isDifferent[field.name]} - selectedView={this.props.selectedView} - socket={this.props.socket} - changeProject={this.props.changeProject} - fonts={this.props.fonts} - adapterName={this.props.adapterName} - instance={this.props.instance} - projectName={this.props.projectName} - onPxToPercent={this.props.onPxToPercent} - onPercentToPx={this.props.onPercentToPx} - userGroups={this.props.userGroups} - error={error} - selectedWidgets={this.props.selectedWidgets} - />} -
    + + {ICONS[field.singleName || field.name] ? ICONS[field.singleName || field.name] : null} + {label} + {field.type === 'image' && + !this.state.isDifferent[field.name] && + selectedWidget?.data[field.name] ? ( +
    + { + (e.target as HTMLImageElement).onerror = null; + (e.target as HTMLImageElement).style.display = 'none'; + }} + alt={field.name} + /> +
    + ) : null} + {(field.type !== 'custom' || field.label) && !field.noBinding ? ( + isBoundField ? ( + + this.props.editMode && this.changeBinding(group.isStyle, field.name)} + style={{ ...styles.bindIcon, cursor: disabled ? 'default' : undefined }} + /> + + ) : ( + + this.props.editMode && this.changeBinding(group.isStyle, field.name)} + /> + + ) + ) : null} + {group.isStyle ? ( + + + this.props.cssClone(field.name, newValue => { + if (newValue !== null && newValue !== undefined) { + const project = deepClone(store.getState().visProject); + this.props.selectedWidgets.forEach(wid => { + if (project[this.props.selectedView].widgets[wid]) { + project[this.props.selectedView].widgets[wid].style = + project[this.props.selectedView].widgets[wid].style || {}; + ( + project[this.props.selectedView].widgets[wid].style as Record< + string, + string | number | boolean | null + > + )[field.name] = newValue; + } + }); + this.props.changeProject(project); + } + }) + } + /> + + ) : null} + {field.tooltip ? : null} +
    + +
    + {isBoundField ? ( + 1 ? this.state.commonValues : selectedWidget + } + isStyle={group.isStyle} + selectedView={this.props.selectedView} + selectedWidgets={this.props.selectedWidgets} + isDifferent={this.state.isDifferent[field.name]} + socket={this.props.socket} + changeProject={this.props.changeProject} + /> + ) : ( + 1 + ? (this.state.commonValues as SingleGroupWidget) + : selectedWidget + } + widgetId={this.props.selectedWidgets.length > 1 ? null : this.props.selectedWidgets[0]} + isStyle={group.isStyle} + index={group.index} + isDifferent={this.state.isDifferent[field.name]} + selectedView={this.props.selectedView} + socket={this.props.socket} + changeProject={this.props.changeProject} + fonts={this.props.fonts} + adapterName={this.props.adapterName} + instance={this.props.instance} + projectName={this.props.projectName} + onPxToPercent={this.props.onPxToPercent} + onPercentToPx={this.props.onPercentToPx} + userGroups={this.props.userGroups} + error={error} + selectedWidgets={this.props.selectedWidgets} + /> + )} +
    +
    -
    ; + ); } - renderGroupBody(group: PaletteGroup) { + renderGroupBody(group: PaletteGroup): React.JSX.Element { if (this.state.accordionOpen[group.name] === 0 || !group.hasValues) { return null; } - return - - - {group.fields.map((field, fieldIndex) => this.renderFieldRow(group, field, fieldIndex))} - -
    -
    ; + return ( + + + + {group.fields.map((field, fieldIndex) => this.renderFieldRow(group, field, fieldIndex))} + +
    +
    + ); } - renderGroup(group: PaletteGroup) { - return { - const accordionOpen = { ...this.state.accordionOpen }; - accordionOpen[group.name] = expanded ? 1 : 2; - this.setAccordionState(accordionOpen, () => { - if (!expanded) { - setTimeout(() => { - const _accordionOpen = { ...this.state.accordionOpen }; - _accordionOpen[group.name] = 0; - }, 200); - } - }); - }} - TransitionProps={{ timeout: this.state.transitionTime }} - > - {this.renderGroupHeader(group)} - {this.renderGroupBody(group)} - ; + renderGroup(group: PaletteGroup): React.JSX.Element { + return ( + { + const accordionOpen = { ...this.state.accordionOpen }; + accordionOpen[group.name] = expanded ? 1 : 2; + this.setAccordionState(accordionOpen, () => { + if (!expanded) { + setTimeout(() => { + const _accordionOpen = { ...this.state.accordionOpen }; + _accordionOpen[group.name] = 0; + }, 200); + } + }); + }} + TransitionProps={{ timeout: this.state.transitionTime }} + > + {this.renderGroupHeader(group)} + {this.renderGroupBody(group)} + + ); } - onGroupDelete(group: PaletteGroup) { + onGroupDelete(group: PaletteGroup): void { const project = deepClone(store.getState().visProject); const type = group.isStyle ? 'style' : 'data'; this.props.selectedWidgets.forEach(wid => { @@ -1514,7 +1968,7 @@ class Widget extends Component { store.dispatch(recalculateFields(true)); } - render() { + render(): React.JSX.Element | React.JSX.Element[] | null { if (store.getState().recalculateFields && !this.recalculateTimer) { this.recalculateTimer = setTimeout(() => { this.recalculateTimer = null; @@ -1529,85 +1983,118 @@ class Widget extends Component { // check that for all selected widgets the widget type loaded and exists const widgets = store.getState().visProject[this.props.selectedView]?.widgets; let widgetsExist = 0; - widgets && this.props.selectedWidgets.forEach(selectedWidget => { - if (widgets[selectedWidget] && this.state.widgetTypes.find(type => type.name === widgets[selectedWidget].tpl)) { - widgetsExist++; - } - }); + widgets && + this.props.selectedWidgets.forEach(selectedWidget => { + if ( + widgets[selectedWidget] && + this.state.widgetTypes.find(type => type.name === widgets[selectedWidget].tpl) + ) { + widgetsExist++; + } + }); if (!widgets || this.props.selectedWidgets.length !== widgetsExist) { return null; } // detect triggers from parent to open all groups if (this.props.triggerAllOpened !== this.state.triggerAllOpened) { - this.triggerTimer = this.triggerTimer || setTimeout(() => { - this.triggerTimer = null; - const accordionOpen: { [groupName: string]: 0 | 1 | 2 } = {}; - this.state.fields?.forEach(group => accordionOpen[group.name] = 1); - this.setState({ triggerAllOpened: this.props.triggerAllOpened }, () => this.setAccordionState(accordionOpen)); - }, 50); + this.triggerTimer = + this.triggerTimer || + setTimeout(() => { + this.triggerTimer = null; + const accordionOpen: { [groupName: string]: 0 | 1 | 2 } = {}; + this.state.fields?.forEach(group => (accordionOpen[group.name] = 1)); + this.setState({ triggerAllOpened: this.props.triggerAllOpened }, () => + this.setAccordionState(accordionOpen), + ); + }, 50); } // detect triggers from parent to close all groups if (this.props.triggerAllClosed !== this.state.triggerAllClosed) { - this.triggerTimer = this.triggerTimer || setTimeout(() => { - this.triggerTimer = null; - const accordionOpen: { [groupName: string]: 0 | 1 | 2 } = {}; - this.state.fields?.forEach(group => accordionOpen[group.name] = 0); - this.setState({ triggerAllClosed: this.props.triggerAllClosed }, () => this.setAccordionState(accordionOpen)); - }, 50); + this.triggerTimer = + this.triggerTimer || + setTimeout(() => { + this.triggerTimer = null; + const accordionOpen: { [groupName: string]: 0 | 1 | 2 } = {}; + this.state.fields?.forEach(group => (accordionOpen[group.name] = 0)); + this.setState({ triggerAllClosed: this.props.triggerAllClosed }, () => + this.setAccordionState(accordionOpen), + ); + }, 50); } let jsonCustomFields = null; if (this.state.showWidgetCode) { try { jsonCustomFields = JSON.stringify(this.state.customFields, null, 2); - } catch (e) { + } catch { // ignore } } return [ this.renderHeader(widgets), - this.state.fields ?
    - {this.state.fields.map(group => { - if (group.hidden) { - if (group.hidden === true) { - return null; - } - if (Widget.checkFunction(group.hidden, store.getState().visProject, this.props.selectedView, this.props.selectedWidgets, group.index)) { - return null; + this.state.fields ? ( +
    + {this.state.fields.map(group => { + if (group.hidden) { + if (group.hidden === true) { + return null; + } + if ( + Widget.checkFunction( + group.hidden, + store.getState().visProject, + this.props.selectedView, + this.props.selectedWidgets, + group.index, + ) + ) { + return null; + } } - } - return this.renderGroup(group); - })} + return this.renderGroup(group); + })} - {this.state.clearGroup ? this.setState({ clearGroup: null })} - open={!0} - action={() => this.onGroupDelete(this.state.clearGroup)} - actionTitle="Clear" - > - {I18n.t('Fields of group will be cleaned')} - : null} - - - - {this.state.showWidgetCode ?
    -                    {JSON.stringify(selectWidget(store.getState(), this.props.selectedView, this.props.selectedWidgets[0]), null, 2)}
    -                    {jsonCustomFields}
    -                
    : null} -
    : null, + {this.state.clearGroup ? ( + this.setState({ clearGroup: null })} + open={!0} + action={() => this.onGroupDelete(this.state.clearGroup)} + actionTitle="Clear" + > + {I18n.t('Fields of group will be cleaned')} + + ) : null} + + + + {this.state.showWidgetCode ? ( +
    +                            {JSON.stringify(
    +                                selectWidget(store.getState(), this.props.selectedView, this.props.selectedWidgets[0]),
    +                                null,
    +                                2,
    +                            )}
    +                            {jsonCustomFields}
    +                        
    + ) : null} +
    + ) : null, ]; } } diff --git a/packages/iobroker.vis-2/src/src/Attributes/index.tsx b/packages/iobroker.vis-2/src/src/Attributes/index.tsx index b4b5fa12a..9a8b61f98 100644 --- a/packages/iobroker.vis-2/src/src/Attributes/index.tsx +++ b/packages/iobroker.vis-2/src/src/Attributes/index.tsx @@ -1,28 +1,18 @@ -import type { JSXElementConstructor, ReactNode } from 'react'; -import React, { - useEffect, - useState, -} from 'react'; +import React, { useEffect, useState, type JSXElementConstructor, type ReactNode } from 'react'; -import { - IconButton, - Tab, Tabs, Tooltip, Typography, -} from '@mui/material'; +import { IconButton, Tab, Tabs, Tooltip, Typography } from '@mui/material'; import { Clear as ClearIcon, UnfoldMore as UnfoldMoreIcon, - UnfoldLess as UnfoldLessIcon, ListAlt as IconAttributes, + UnfoldLess as UnfoldLessIcon, + ListAlt as IconAttributes, } from '@mui/icons-material'; -import { - I18n, Utils, type ThemeType, - type LegacyConnection, -} from '@iobroker/adapter-react-v5'; +import { I18n, Utils, type ThemeType, type LegacyConnection } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; import type { VisTheme } from '@iobroker/types-vis-2'; -import commonStyles from '@/Utils/styles'; import CSS from './CSS'; import Scripts from './Scripts'; import View from './View'; @@ -66,10 +56,10 @@ interface AttributesProps { theme: VisTheme; } -const Attributes = (props: AttributesProps) => { - const [selected, setSelected] = useState(window.localStorage.getItem('Attributes') - ? window.localStorage.getItem('Attributes') - : 'View'); +const Attributes = (props: AttributesProps): React.JSX.Element => { + const [selected, setSelected] = useState( + window.localStorage.getItem('Attributes') ? window.localStorage.getItem('Attributes') : 'View', + ); const [isAllOpened, setIsAllOpened] = useState(false); const [isAllClosed, setIsAllClosed] = useState(true); const [triggerAllOpened, setTriggerAllOpened] = useState(0); @@ -92,84 +82,114 @@ const Attributes = (props: AttributesProps) => { const TabContent: JSXElementConstructor | ((props_: Record) => ReactNode) = tabs[selected]; - return <> - - - {I18n.t('Attributes')} -
    - {selected === 'View' || selected === 'Widget' ?
    - {!isAllOpened ? + return ( + <> + + + {I18n.t('Attributes')} +
    + {selected === 'View' || selected === 'Widget' ? ( +
    + {!isAllOpened ? ( + + setTriggerAllOpened(triggerAllOpened + 1)} + > + + + + ) : ( + + + + )} + {!isAllClosed ? ( + + setTriggerAllClosed(triggerAllClosed + 1)}> + + + + ) : ( + + + + )} +
    + ) : null} + setTriggerAllOpened(triggerAllOpened + 1)} + onClick={() => props.onHide(true)} > - + - : } - {!isAllClosed ? - setTriggerAllClosed(triggerAllClosed + 1)}> - - - : } -
    : null} - - - props.onHide(true)} - > - - - -
    - - { - ['View', 'Widget', 'CSS', 'Scripts'].map(tab => { - setSelected(tab); - window.localStorage.setItem('Attributes', tab); - }} - />) - } - -
    - {selected === 'Widget' && - !(props.widgetsLoaded && props.selectedView && props.selectedWidgets?.length) ? - null : - } -
    - ; + + + + {['View', 'Widget', 'CSS', 'Scripts'].map(tab => ( + { + setSelected(tab); + window.localStorage.setItem('Attributes', tab); + }} + /> + ))} + +
    + {selected === 'Widget' && + !(props.widgetsLoaded && props.selectedView && props.selectedWidgets?.length) ? null : ( + + )} +
    + + ); }; export default Attributes; diff --git a/packages/iobroker.vis-2/src/src/Components/CodeDialog.tsx b/packages/iobroker.vis-2/src/src/Components/CodeDialog.tsx index 4ec53a582..29e766d02 100644 --- a/packages/iobroker.vis-2/src/src/Components/CodeDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Components/CodeDialog.tsx @@ -1,13 +1,8 @@ import React, { Component } from 'react'; -import { - Dialog, Button, DialogActions, DialogContent, DialogTitle, -} from '@mui/material'; +import { Dialog, Button, DialogActions, DialogContent, DialogTitle } from '@mui/material'; -import { - ContentCopy as IconCopy, - Close as CloseIcon, -} from '@mui/icons-material'; +import { ContentCopy as IconCopy, Close as CloseIcon } from '@mui/icons-material'; import { I18n, Utils } from '@iobroker/adapter-react-v5'; @@ -22,44 +17,46 @@ interface CodeDialogProps { } class CodeDialog extends Component { - render() { - return this.props.onClose()} - maxWidth="xl" - fullWidth - > - {this.props.title || I18n.t('Code')} - - - - - - - - ; + render(): React.JSX.Element { + return ( + this.props.onClose()} + maxWidth="xl" + fullWidth + > + {this.props.title || I18n.t('Code')} + + + + + + + + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Components/CreateFirstProjectDialog.tsx b/packages/iobroker.vis-2/src/src/Components/CreateFirstProjectDialog.tsx index 7b6aaa9de..b93b82a0a 100644 --- a/packages/iobroker.vis-2/src/src/Components/CreateFirstProjectDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Components/CreateFirstProjectDialog.tsx @@ -7,19 +7,21 @@ interface CreateFirstProjectDialogProps { addProject: (name: string) => void; } -const CreateFirstProjectDialog = (props: CreateFirstProjectDialogProps) => { +const CreateFirstProjectDialog = (props: CreateFirstProjectDialogProps): React.JSX.Element | null => { if (props.open) { return null; } - return props.addProject('Demo project')} - actionTitle="Yes" - closeTitle="No" - />; + return ( + props.addProject('Demo project')} + actionTitle="Yes" + closeTitle="No" + /> + ); }; export default CreateFirstProjectDialog; diff --git a/packages/iobroker.vis-2/src/src/Components/CustomAceEditor.tsx b/packages/iobroker.vis-2/src/src/Components/CustomAceEditor.tsx index 3a87b6fdb..c1cff554b 100644 --- a/packages/iobroker.vis-2/src/src/Components/CustomAceEditor.tsx +++ b/packages/iobroker.vis-2/src/src/Components/CustomAceEditor.tsx @@ -60,18 +60,18 @@ interface CustomAceEditorProps { focus?: boolean; } -export const CustomAceEditor = (props: CustomAceEditorProps) => { +export const CustomAceEditor = (props: CustomAceEditorProps): React.JSX.Element => { const refEditor = useRef(); useEffect(() => { let content: HTMLInputElement | null = null; let timer: ReturnType; - const keyDown = (e: KeyboardEvent) => { + const keyDown = (e: KeyboardEvent): void => { if (e.key === 'f' && e.ctrlKey) { // make translations timer = setInterval(() => { const parent = content.parentNode; - let el: HTMLInputElement = parent.querySelector('.ace_search_field') as HTMLInputElement; + let el: HTMLInputElement = parent?.querySelector('.ace_search_field'); if (el) { clearInterval(timer); timer = null; @@ -125,31 +125,33 @@ export const CustomAceEditor = (props: CustomAceEditorProps) => { }; }, []); - return
    - props.onChange(newValue)} - readOnly={props.readOnly || false} - focus={props.focus} - ref={props.refEditor} - highlightActiveLine - enableBasicAutocompletion - enableLiveAutocompletion - enableSnippets - /> -
    ; + return ( +
    + props.onChange(newValue)} + readOnly={props.readOnly || false} + focus={props.focus} + ref={props.refEditor} + highlightActiveLine + enableBasicAutocompletion + enableLiveAutocompletion + enableSnippets + /> +
    + ); }; export default CustomAceEditor; diff --git a/packages/iobroker.vis-2/src/src/Components/IOContextMenu.tsx b/packages/iobroker.vis-2/src/src/Components/IOContextMenu.tsx index 5882df2bc..31edae139 100644 --- a/packages/iobroker.vis-2/src/src/Components/IOContextMenu.tsx +++ b/packages/iobroker.vis-2/src/src/Components/IOContextMenu.tsx @@ -16,62 +16,71 @@ interface MenuItem { style?: React.CSSProperties; } -const contextMenuItems = (items: MenuItem[], open: boolean, onClose: () => void) => +const contextMenuItems = (items: MenuItem[], open: boolean, onClose: () => void): React.JSX.Element[] => items.map((item, key: number) => { if (!item || item.hide) { return null; } if (item.items) { - return {I18n.t(item.label)}} + parentMenuOpen={open} + onContextMenu={e => { + e.stopPropagation(); + e.preventDefault(); + }} + > + {contextMenuItems(item.items, open, onClose)} + + ); + } + + return ( + { + item.onClick && item.onClick(); + onClose(); + }} disabled={item.disabled} // @ts-expect-error we can provide an Element here too and it works - label={{I18n.t(item.label)}} - parentMenuOpen={open} - onContextMenu={e => { + label={[ + + {item.leftIcon} + {I18n.t(item.label)} + , + item.subLabel ? ( + + {item.subLabel} + + ) : null, + ]} + onContextMenu={(e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); + item.onClick && item.onClick(); + onClose(); }} - > - {contextMenuItems(item.items, open, onClose)} - ; - } - - return { - item.onClick && item.onClick(); - onClose(); - }} - disabled={item.disabled} - // @ts-expect-error we can provide an Element here too and it works - label={[ - - {item.leftIcon} - {I18n.t(item.label)} - , - item.subLabel ? - {item.subLabel} - : null, - ]} - onContextMenu={(e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - item.onClick && item.onClick(); - onClose(); - }} - />; + /> + ); }); interface IOContextMenuProps { @@ -80,10 +89,12 @@ interface IOContextMenuProps { menuItemsData: (position: { top: number; left: number }) => any; } -const IOContextMenu = (props: IOContextMenuProps) => { +const IOContextMenu = (props: IOContextMenuProps): React.JSX.Element => { const [menuPosition, setMenuPosition] = useState(null); - const handleRightClick: React.MouseEventHandler = async (event: React.MouseEvent) => { + const handleRightClick: React.MouseEventHandler = async ( + event: React.MouseEvent, + ) => { if (props.disabled || event.ctrlKey || event.shiftKey) { return; } @@ -100,17 +111,24 @@ const IOContextMenu = (props: IOContextMenuProps) => { }); }; - return
    - {props.children} - {menuPosition ? setMenuPosition(null)} - anchorReference="anchorPosition" - anchorPosition={menuPosition} + return ( +
    - {contextMenuItems(props.menuItemsData(menuPosition), !!menuPosition, () => setMenuPosition(null))} -
    : null} -
    ; + {props.children} + {menuPosition ? ( + setMenuPosition(null)} + anchorReference="anchorPosition" + anchorPosition={menuPosition} + > + {contextMenuItems(props.menuItemsData(menuPosition), !!menuPosition, () => setMenuPosition(null))} + + ) : null} +
    + ); }; export default IOContextMenu; diff --git a/packages/iobroker.vis-2/src/src/Components/IODialog.tsx b/packages/iobroker.vis-2/src/src/Components/IODialog.tsx index 8cecb3e84..f5c840bf9 100644 --- a/packages/iobroker.vis-2/src/src/Components/IODialog.tsx +++ b/packages/iobroker.vis-2/src/src/Components/IODialog.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import type { Breakpoint } from '@mui/system'; import { Close as CloseIcon } from '@mui/icons-material'; @@ -29,57 +27,61 @@ interface IODialogProps { noTranslation?: boolean; } -const IODialog = (props: IODialogProps) => (props.open ? - {props.noTranslation ? props.title : I18n.t(props.title)} - { - if (props.action) { - if (!props.actionDisabled && !props.keyboardDisabled) { - if (e.key === 'Enter') { - props.action(); - if (!props.actionNoClose) { - props.onClose(); +const IODialog = (props: IODialogProps): React.JSX.Element => + props.open ? ( + + {props.noTranslation ? props.title : I18n.t(props.title)} + { + if (props.action) { + if (!props.actionDisabled && !props.keyboardDisabled) { + if (e.key === 'Enter') { + props.action(); + if (!props.actionNoClose) { + props.onClose(); + } + } } } - } - } - }} - > - {props.children} - - - {props.dialogActions || null} - {props.actionTitle - ? : null} - - - : null); + {props.children} + + + {props.dialogActions || null} + {props.actionTitle ? ( + + ) : null} + + + + ) : null; export default IODialog; diff --git a/packages/iobroker.vis-2/src/src/Components/MaterialIconSelector.tsx b/packages/iobroker.vis-2/src/src/Components/MaterialIconSelector.tsx index 060d9ff78..5d5ad05b5 100644 --- a/packages/iobroker.vis-2/src/src/Components/MaterialIconSelector.tsx +++ b/packages/iobroker.vis-2/src/src/Components/MaterialIconSelector.tsx @@ -11,15 +11,14 @@ import { InputAdornment, Grid, Radio, - RadioGroup, TextField, LinearProgress, Pagination, Box, + RadioGroup, + TextField, + LinearProgress, + Pagination, + Box, } from '@mui/material'; -import { - Search as SearchIcon, - Close as ClearIcon, - Check as CheckIcon, - Delete as EraseIcon, -} from '@mui/icons-material'; +import { Search as SearchIcon, Close as ClearIcon, Check as CheckIcon, Delete as EraseIcon } from '@mui/icons-material'; import { I18n, Utils, Icon } from '@iobroker/adapter-react-v5'; import type { MaterialIconSelectorProps, VisTheme } from '@iobroker/types-vis-2'; @@ -48,7 +47,7 @@ const styles: Record = { backgroundColor: theme.palette.secondary.main, }, }), - icon: (theme: VisTheme) => ({ + icon: (theme: VisTheme): React.CSSProperties => ({ width: 48, height: 48, color: theme.palette.text.primary, @@ -81,13 +80,15 @@ interface MaterialIconSelectorState { class MaterialIconSelector extends Component { private list: Record = {}; - private index: { - name: string; - version: number; - categories: string[]; - tags: string[]; - unsupported_families?: string[]; - }[] | null = null; + private index: + | { + name: string; + version: number; + categories: string[]; + tags: string[]; + unsupported_families?: string[]; + }[] + | null = null; private filterTimer: ReturnType | null = null; @@ -113,54 +114,53 @@ class MaterialIconSelector extends Component { if (!this.list[type]) { await this.setStateAsync({ loading: true }); this.list[type] = true; if (type === 'customIcons') { try { - this.list[type] = await fetch(this.props.customIcons) - .then(res => res.json()); + this.list[type] = await fetch(this.props.customIcons).then(res => res.json()); } catch (e) { this.list[type] = {}; console.error(`Cannot load custom icons from ${this.props.customIcons}: ${e}`); } } else { - this.list[type] = await fetch(`./material-icons/${type}.json`) - .then(res => res.json()); + this.list[type] = await fetch(`./material-icons/${type}.json`).then(res => res.json()); } const icons = Object.keys(this.list[type]); for (let i = 0; i < icons.length; i++) { const icon = icons[i]; this.list[type][icon] = `data:image/svg+xml;base64,${this.list[type][icon]}`; } - this.setState({ - iconTypeLoaded: { ...this.state.iconTypeLoaded, [type]: true }, - filtered: [], - loading: false, - }, () => - this.applyFilter(true)); + this.setState( + { + iconTypeLoaded: { ...this.state.iconTypeLoaded, [type]: true }, + filtered: [], + loading: false, + }, + () => this.applyFilter(true), + ); } } - loadList() { + loadList(): void { if (!this.state.listLoaded) { - fetch('./material-icons/index.json') + void fetch('./material-icons/index.json') .then(res => res.json()) .then(async index => { this.index = index; await this.loadIconSet(this.state.iconType); - this.setState({ listLoaded: true }, () => - this.applyFilter(true)); + this.setState({ listLoaded: true }, () => this.applyFilter(true)); }); } } - componentWillUnmount() { + componentWillUnmount(): void { this.filterTimer && clearTimeout(this.filterTimer); } - applyFilter(filter?: string | boolean) { + applyFilter(filter?: string | boolean): void { let timeout = 200; if (filter === true) { timeout = 0; @@ -179,9 +179,12 @@ class MaterialIconSelector extends Component icon.includes(filter as string)); } else { filtered = this.index - .filter(icon => - !icon.unsupported_families?.includes(this.state.iconType) && - (icon.name.includes(filter as string) || icon.tags?.find(tag => tag.includes(filter as string)))) + .filter( + icon => + !icon.unsupported_families?.includes(this.state.iconType) && + (icon.name.includes(filter as string) || + icon.tags?.find(tag => tag.includes(filter as string))), + ) .map(icon => icon.name); } } else if (this.state.iconType === 'knx-uf') { @@ -190,7 +193,9 @@ class MaterialIconSelector extends Component !icon.unsupported_families || !icon.unsupported_families.includes(this.state.iconType)) + .filter( + icon => !icon.unsupported_families || !icon.unsupported_families.includes(this.state.iconType), + ) .map(icon => icon.name); } @@ -202,11 +207,11 @@ class MaterialIconSelector extends Component this.setState({ selectedIcon: icon })} - onDoubleClick={() => this.setState({ selectedIcon: icon }, () => this.onSelect())} - > - -
    {icon.replace(/_/g, ' ')}
    - ); + icons.push( + this.setState({ selectedIcon: icon })} + onDoubleClick={() => this.setState({ selectedIcon: icon }, () => this.onSelect())} + > + +
    {icon.replace(/_/g, ' ')}
    +
    , + ); } return icons; } - render() { - return - - - {this.state.iconType === 'knx-uf' ? 'KNX UF' : (this.state.iconType !== 'upload' ? 'Material' : '')} -  Icon Selector - - {this.state.iconType !== 'upload' ? - - , - endAdornment: - this.state.filter ? - { - window.localStorage.removeItem('vis.icon.filter'); - this.setState({ filter: '' }, () => this.applyFilter(true)); - }} - /> - : null, - }} - variant="standard" - onChange={e => { - window.localStorage.setItem('vis.icon.filter', e.target.value); - this.setState({ filter: e.target.value }, () => - this.applyFilter()); - }} - helperText={I18n.t('material_icons_result', this.state.filtered.length)} - /> : null} - - - {!this.props.iconType ?
    - - this.setState({ iconType: e.target.value })} - > - {ICON_TYPES.map(type => { - window.localStorage.setItem('vis.icon.type', type); - const newState: Partial = { iconType: type }; - if (type !== 'upload') { - await this.loadIconSet(type); - if (this.state.selectedIcon && !this.list[type][this.state.selectedIcon]) { - newState.selectedIcon = ''; - } - } else { - newState.selectedIcon = ''; - newState.maxPages = 0; - } - - this.setState(newState as MaterialIconSelectorState, () => this.applyFilter(true)); - }} - key={type} - value={type} - control={} - label={I18n.t(`material_icons_${type}`)} - sx={{ '.& MuiRadioGroup-label': styles.typeName }} - />)} - {this.props.customIcons ? { - window.localStorage.setItem('vis.icon.type', 'customIcons'); - await this.loadIconSet('customIcons'); - const newState: Partial = { iconType: 'customIcons' }; - if (this.state.selectedIcon && !this.list.customIcons[this.state.selectedIcon]) { - newState.selectedIcon = ''; - } - this.setState(newState as MaterialIconSelectorState); - }} - key="customIcons" - value="customIcons" - control={} - label={I18n.t('custom_icons')} - sx={{ '& .MuiFormControlLabel-label': styles.typeName }} - /> : null} - - -
    : null} -
    - {this.state.iconType === 'knx-uf' ? : null} - {this.state.iconType !== 'upload' && !this.state.loading && this.list[this.state.iconType] && this.list[this.state.iconType] !== true ?
    - - {this.renderIcons()} - -
    : (this.state.iconType !== 'upload' ? : null)} - {this.state.iconType === 'upload' ?
    -
    + + + {this.state.iconType === 'knx-uf' + ? 'KNX UF' + : this.state.iconType !== 'upload' + ? 'Material' + : ''} +  Icon Selector + + {this.state.iconType !== 'upload' ? ( + + + + ), + endAdornment: this.state.filter ? ( + + { + window.localStorage.removeItem('vis.icon.filter'); + this.setState({ filter: '' }, () => this.applyFilter(true)); + }} + /> + + ) : null, }} - > - {I18n.t('icon_upload_hint')} -
    - -
    - this.setState({ selectedIcon: data.toString() })} - maxSize={15_000} - accept={{ - 'image/png': ['.png'], - 'image/jpeg': ['.jpg'], - 'image/svg+xml': ['.svg'], - 'image/gif': ['.gif'], - 'image/apng': ['.apng'], - 'image/avif': ['.avif'], - 'image/webp': ['.webp'], + variant="standard" + onChange={e => { + window.localStorage.setItem('vis.icon.filter', e.target.value); + this.setState({ filter: e.target.value }, () => this.applyFilter()); }} + helperText={I18n.t('material_icons_result', this.state.filtered.length)} /> -
    : null} -
    -
    - - {this.state.maxPages > 1 && !this.state.loading ?
    - + + {!this.props.iconType ? ( +
    + + this.setState({ iconType: e.target.value })} + > + {ICON_TYPES.map(type => ( + { + window.localStorage.setItem('vis.icon.type', type); + const newState: Partial = { iconType: type }; + if (type !== 'upload') { + await this.loadIconSet(type); + if ( + this.state.selectedIcon && + !this.list[type][this.state.selectedIcon] + ) { + newState.selectedIcon = ''; + } + } else { + newState.selectedIcon = ''; + newState.maxPages = 0; + } + + this.setState(newState as MaterialIconSelectorState, () => + this.applyFilter(true), + ); + }} + key={type} + value={type} + control={} + label={I18n.t(`material_icons_${type}`)} + sx={{ '.& MuiRadioGroup-label': styles.typeName }} + /> + ))} + {this.props.customIcons ? ( + { + window.localStorage.setItem('vis.icon.type', 'customIcons'); + await this.loadIconSet('customIcons'); + const newState: Partial = { + iconType: 'customIcons', + }; + if ( + this.state.selectedIcon && + !this.list.customIcons[this.state.selectedIcon] + ) { + newState.selectedIcon = ''; + } + this.setState(newState as MaterialIconSelectorState); + }} + key="customIcons" + value="customIcons" + control={} + label={I18n.t('custom_icons')} + sx={{ '& .MuiFormControlLabel-label': styles.typeName }} + /> + ) : null} + + +
    + ) : null} +
    + {this.state.iconType === 'knx-uf' ? ( + + ) : null} + {this.state.iconType !== 'upload' && + !this.state.loading && + this.list[this.state.iconType] && + this.list[this.state.iconType] !== true ? ( +
    + + {this.renderIcons()} + +
    + ) : this.state.iconType !== 'upload' ? ( + + ) : null} + {this.state.iconType === 'upload' ? ( +
    +
    + {I18n.t('icon_upload_hint')} +
    + +
    + this.setState({ selectedIcon: data.toString() })} + maxSize={15_000} + accept={{ + 'image/png': ['.png'], + 'image/jpeg': ['.jpg'], + 'image/svg+xml': ['.svg'], + 'image/gif': ['.gif'], + 'image/apng': ['.apng'], + 'image/avif': ['.avif'], + 'image/webp': ['.webp'], + }} + /> +
    + ) : null} +
    +
    + + {this.state.maxPages > 1 && !this.state.loading ? ( +
    + this.setState({ page })} + /> +
    + ) : null} + {this.state.maxPages > 1 ?
    : null} + {this.props.value ? ( + + ) : null} +
    : null} - {this.state.maxPages > 1 ?
    : null} - {this.props.value ? : null} - - - -
    ; + onClick={() => this.onSelect()} + disabled={!this.state.selectedIcon} + startIcon={} + > + {I18n.t('Select')} + + + + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Components/UploadFile.tsx b/packages/iobroker.vis-2/src/src/Components/UploadFile.tsx index 8c455d344..c789e99d9 100644 --- a/packages/iobroker.vis-2/src/src/Components/UploadFile.tsx +++ b/packages/iobroker.vis-2/src/src/Components/UploadFile.tsx @@ -1,6 +1,5 @@ import React, { useState, useCallback } from 'react'; -import type { FileError } from 'react-dropzone'; -import { useDropzone } from 'react-dropzone'; +import { useDropzone, type FileError } from 'react-dropzone'; import { CircularProgress } from '@mui/material'; @@ -8,7 +7,20 @@ import { FolderZip } from '@mui/icons-material'; import { I18n, Utils, type ThemeType } from '@iobroker/adapter-react-v5'; -const IMAGE_TYPES = ['.png', '.jpg', '.svg', '.gif', '.apng', '.avif', '.webp']; +const IMAGE_TYPES = [ + '.png', + '.jpg', + '.svg', + '.gif', + '.apng', + '.avif', + '.webp', + '.jpeg', + '.bmp', + '.ico', + '.tiff', + '.tif', +]; interface UploadFileProps { onUpload: (fileName: string, fileData: string | ArrayBuffer) => void; @@ -19,43 +31,49 @@ interface UploadFileProps { maxSize?: number; } -const UploadFile = (props: UploadFileProps) => { +const UploadFile = (props: UploadFileProps): React.JSX.Element => { const [fileName, setFileName] = useState(''); const [fileData, setFileData] = useState(null); const [working, setWorking] = useState(false); const [error, setError] = useState(''); - const onDrop = useCallback((acceptedFiles: File[], fileRejections: { - file: File; - errors: FileError[]; - }[]) => { - if (acceptedFiles?.length) { - setWorking(true); - error && setError(''); - const reader = new FileReader(); - setFileName(acceptedFiles[0].name); + const onDrop = useCallback( + ( + acceptedFiles: File[], + fileRejections: { + file: File; + errors: FileError[]; + }[], + ) => { + if (acceptedFiles?.length) { + setWorking(true); + error && setError(''); + const reader = new FileReader(); + setFileName(acceptedFiles[0].name); - reader.onload = async (evt: ProgressEvent) => { - setWorking(false); - setFileData(evt.target.result); - props.onUpload(acceptedFiles[0].name, evt.target.result); - }; + reader.onload = async (evt: ProgressEvent): Promise => { + setWorking(false); + setFileData(evt.target.result); + props.onUpload(acceptedFiles[0].name, evt.target.result); + }; - reader.readAsDataURL(acceptedFiles[0]); - } - if (fileRejections?.length) { - fileRejections[0].errors.forEach(err => { - if (err.code === 'file-too-large') { - setError(I18n.t('File too large')); - } else if (err.code === 'file-invalid-type') { - setError(I18n.t('Invalid file type')); - } else { - setError(`Error: ${err.message}`); - } - setTimeout(() => error && setError(''), 3000); - }); - } - }, []); + reader.readAsDataURL(acceptedFiles[0]); + } + if (fileRejections?.length) { + fileRejections[0].errors.forEach(err => { + if (err.code === 'file-too-large') { + setError(I18n.t('File too large')); + } else if (err.code === 'file-invalid-type') { + setError(I18n.t('Invalid file type')); + } else { + setError(`Error: ${err.message}`); + } + setTimeout(() => error && setError(''), 3000); + }); + } + }, + [], + ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, @@ -64,50 +82,59 @@ const UploadFile = (props: UploadFileProps) => { accept: props.accept, }); - return
    - {error ?
    {error}
    : null} - {props.disabled || working ? null : } - {working ? : -

    - {fileName ? <> -

    {fileName}
    - {fileName.endsWith('.zip') ? : null} - {IMAGE_TYPES.find(ext => fileName.toLowerCase().endsWith(ext)) ? - uploaded : null} - {fileData ?
    - ( - {Utils.formatBytes(fileData.length)} - ) -
    : null} - : (props.instruction || `${I18n.t('Drop the files here ...')} ${props.maxSize ? I18n.t('(Maximal file size is %s)', Utils.formatBytes(props.maxSize)) : ''}`)} -

    } -
    ; + return ( +
    + {error ?
    {error}
    : null} + {props.disabled || working ? null : } + {working ? ( + + ) : ( +

    + {fileName ? ( + <> +

    {fileName}
    + {fileName.endsWith('.zip') ? : null} + {IMAGE_TYPES.find(ext => fileName.toLowerCase().endsWith(ext)) ? ( + uploaded + ) : null} + {fileData ? ( +
    ({Utils.formatBytes(fileData.length)})
    + ) : null} + + ) : ( + props.instruction || + `${I18n.t('Drop the files here ...')} ${props.maxSize ? I18n.t('(Maximal file size is %s)', Utils.formatBytes(props.maxSize)) : ''}` + )} +

    + )} +
    + ); }; export default UploadFile; diff --git a/packages/iobroker.vis-2/src/src/Components/WizardHelpers.tsx b/packages/iobroker.vis-2/src/src/Components/WizardHelpers.tsx index 78b655f23..71a388f2e 100644 --- a/packages/iobroker.vis-2/src/src/Components/WizardHelpers.tsx +++ b/packages/iobroker.vis-2/src/src/Components/WizardHelpers.tsx @@ -21,9 +21,10 @@ import { } from '@mui/icons-material'; import { TbVacuumCleaner } from 'react-icons/tb'; -import type { DetectOptions } from '@iobroker/type-detector'; -import ChannelDetector, { Types } from '@iobroker/type-detector'; + +import ChannelDetector, { Types, type DetectOptions } from '@iobroker/type-detector'; import type { LegacyConnection } from '@iobroker/adapter-react-v5'; + import { getNewWidgetIdNumber, getNewWidgetId } from '@/Utils/utils'; const deviceIcons = { @@ -55,20 +56,22 @@ const deviceIcons = { unknown: , }; -const allObjects = async (socket: LegacyConnection) => { +const allObjects = async (socket: LegacyConnection): Promise> => { const states = await socket.getObjectViewSystem('state', '', '\u9999'); const channels = await socket.getObjectViewSystem('channel', '', '\u9999'); const devices = await socket.getObjectViewSystem('device', '', '\u9999'); const folders = await socket.getObjectViewSystem('folder', '', '\u9999'); const enums = await socket.getObjectViewSystem('enum', '', '\u9999'); - return Object.values(states) - .concat(Object.values(channels)) - .concat(Object.values(devices)) - .concat(Object.values(folders)) - .concat(Object.values(enums)) - // eslint-disable-next-line - .reduce((obj: Record, item: ioBroker.Object) => (obj[item._id] = item, obj), {}); + return ( + Object.values(states) + .concat(Object.values(channels)) + .concat(Object.values(devices)) + .concat(Object.values(folders)) + .concat(Object.values(enums)) + // eslint-disable-next-line + .reduce((obj: Record, item: ioBroker.Object) => ((obj[item._id] = item), obj), {}) + ); }; function getObjectIcon(obj: ioBroker.Object, id: string, imagePrefix?: string): string { @@ -83,7 +86,11 @@ function getObjectIcon(obj: ioBroker.Object, id: string, imagePrefix?: string): if (cIcon.includes('.')) { let instance; if (obj.type === 'instance' || obj.type === 'adapter') { - src = `${imagePrefix}/adapter/${common.name}/${cIcon}`; + if (typeof common.name === 'object') { + src = `${imagePrefix}/adapter/${common.name.en}/${cIcon}`; + } else { + src = `${imagePrefix}/adapter/${common.name}/${cIcon}`; + } } else if (id && id.startsWith('system.adapter.')) { instance = id.split('.', 3); if (cIcon[0] === '/') { @@ -136,13 +143,16 @@ interface DetectorResult { devices: ObjectForDetector[]; } -const detectDevices = async (socket: LegacyConnection) => { - const devicesObject: Record = await allObjects(socket) as Record; +const detectDevices = async (socket: LegacyConnection): Promise => { + const devicesObject: Record = (await allObjects(socket)) as Record< + string, + ObjectForDetector + >; const keys = Object.keys(devicesObject).sort(); const detector = new ChannelDetector(); const usedIds: string[] = []; - const ignoreIndicators = ['UNREACH_STICKY']; // Ignore indicators by name + const ignoreIndicators = ['UNREACH_STICKY']; // Ignore indicators by name const excludedTypes: Types[] = [Types.info]; const enums: string[] = []; const rooms: string[] = []; @@ -217,10 +227,16 @@ const detectDevices = async (socket: LegacyConnection) => { if (devicesObject[stateId].type === 'channel' || devicesObject[stateId].type === 'state') { parts.pop(); channelId = parts.join('.'); - if (devicesObject[channelId] && (devicesObject[channelId].type === 'channel' || devicesObject[channelId].type === 'folder')) { + if ( + devicesObject[channelId] && + (devicesObject[channelId].type === 'channel' || devicesObject[channelId].type === 'folder') + ) { parts.pop(); deviceId = parts.join('.'); - if (!devicesObject[deviceId] || (devicesObject[deviceId].type !== 'device' && devicesObject[deviceId].type !== 'folder')) { + if ( + !devicesObject[deviceId] || + (devicesObject[deviceId].type !== 'device' && devicesObject[deviceId].type !== 'folder') + ) { deviceId = null; } } else { @@ -232,7 +248,10 @@ const detectDevices = async (socket: LegacyConnection) => { if ((devicesObject[roomId].common as ioBroker.EnumCommon).members.includes(stateId)) { return true; } - if (channelId && (devicesObject[roomId].common as ioBroker.EnumCommon).members.includes(channelId)) { + if ( + channelId && + (devicesObject[roomId].common as ioBroker.EnumCommon).members.includes(channelId) + ) { return true; } return deviceId && (devicesObject[roomId].common as ioBroker.EnumCommon).members.includes(deviceId); @@ -278,22 +297,39 @@ const detectDevices = async (socket: LegacyConnection) => { // read channel const parentObject = devicesObject[idArray.join('.')]; - if (parentObject && (parentObject.type === 'channel' || parentObject.type === 'device' || parentObject.type === 'folder')) { + if ( + parentObject && + (parentObject.type === 'channel' || + parentObject.type === 'device' || + parentObject.type === 'folder') + ) { deviceObj.common.name = parentObject.common?.name || deviceObj.common.name; if (parentObject.common.icon) { - deviceObj.common.icon = getObjectIcon(parentObject as ioBroker.Object, parentObject._id, '../..'); + deviceObj.common.icon = getObjectIcon( + parentObject as ioBroker.Object, + parentObject._id, + '../..', + ); } idArray.pop(); // read device const grandParentObject = devicesObject[idArray.join('.')]; if (grandParentObject?.type === 'device' && grandParentObject.common?.icon) { deviceObj.common.name = grandParentObject.common.name || deviceObj.common.name; - deviceObj.common.icon = getObjectIcon(grandParentObject as ioBroker.Object, grandParentObject._id, '../..'); + deviceObj.common.icon = getObjectIcon( + grandParentObject as ioBroker.Object, + grandParentObject._id, + '../..', + ); } } else { deviceObj.common.name = parentObject?.common?.name || deviceObj.common.name; if (parentObject?.common?.icon) { - deviceObj.common.icon = getObjectIcon(parentObject as ioBroker.Object, parentObject._id, '../..'); + deviceObj.common.icon = getObjectIcon( + parentObject as ioBroker.Object, + parentObject._id, + '../..', + ); } } } else { diff --git a/packages/iobroker.vis-2/src/src/Editor.tsx b/packages/iobroker.vis-2/src/src/Editor.tsx index a90c8d91e..e733d3a31 100644 --- a/packages/iobroker.vis-2/src/src/Editor.tsx +++ b/packages/iobroker.vis-2/src/src/Editor.tsx @@ -6,8 +6,20 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import ReactSplit, { SplitDirection } from '@devbookhq/splitter'; import { - IconButton, Paper, Popper, Tab, Tabs, Tooltip, LinearProgress, Button, - Dialog, DialogTitle, DialogActions, DialogContent, DialogContentText, Box, + IconButton, + Paper, + Popper, + Tab, + Tabs, + Tooltip, + LinearProgress, + Button, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + DialogContentText, + Box, } from '@mui/material'; import { @@ -29,20 +41,37 @@ import { Utils, Confirm as ConfirmDialog, Message as MessageDialog, - SelectFile as SelectFileDialog, Icon, + SelectFile as SelectFileDialog, + Icon, } from '@iobroker/adapter-react-v5'; import type { - AnyWidgetId, GroupData, GroupWidget, - GroupWidgetId, MarketplaceWidgetRevision, Project, - RxWidgetInfoGroup, SingleWidget, SingleWidgetId, - ViewSettings, Widget, WidgetData, WidgetSetName, WidgetStyle, + AnyWidgetId, + GroupData, + GroupWidget, + GroupWidgetId, + MarketplaceWidgetRevision, + Project, + RxWidgetInfoGroup, + SingleWidget, + SingleWidgetId, + ViewSettings, + Widget, + WidgetData, + WidgetSetName, + WidgetStyle, VisTheme, } from '@iobroker/types-vis-2'; import commonStyles from '@/Utils/styles'; import { recalculateFields, store, updateProject } from './Store'; import { - isGroup, getNewWidgetId, getNewGroupId, pasteGroup, unsyncMultipleWidgets, deepClone, pasteSingleWidget, + isGroup, + getNewWidgetId, + getNewGroupId, + pasteGroup, + unsyncMultipleWidgets, + deepClone, + pasteSingleWidget, } from './Utils/utils'; import Attributes from './Attributes'; @@ -87,7 +116,7 @@ const styles: Record = { display: 'flex', alignItems: 'center', }, - app: (theme: IobTheme) => ({ + app: (theme: IobTheme): React.CSSProperties => ({ backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, }), @@ -104,7 +133,7 @@ const styles: Record = { lineHeight: '36px', verticalAlign: 'top', }, - groupEditTab: (theme: VisTheme) => ({ + groupEditTab: (theme: VisTheme): React.CSSProperties => ({ color: theme.palette.mode === 'dark' ? '#bad700' : '#f3bf00', }), tabsName: { @@ -113,11 +142,11 @@ const styles: Record = { alignItems: 'center', textTransform: 'none', }, - viewTabs: (theme: VisTheme) => ({ + viewTabs: (theme: VisTheme): React.CSSProperties => ({ display: 'inline-block', ...theme.classes.viewTabs, }), - viewTab: (theme: VisTheme) => ({ + viewTab: (theme: VisTheme): React.CSSProperties => ({ padding: '6px 12px', ...theme.classes.viewTab, }), @@ -143,7 +172,7 @@ const styles: Record = { height: 24, marginRight: 8, }, - loadingText: (theme: VisTheme) => ({ + loadingText: (theme: VisTheme): React.CSSProperties => ({ position: 'absolute', height: 50, backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.8)', @@ -172,48 +201,68 @@ interface ViewDropProps { const ViewDrop: React.FC = props => { const targetRef = useRef(); - const [{ CanDrop, isOver }, drop] = useDrop<{ - widgetSet: WidgetSetName; - widgetType: WidgetType | MarketplaceWidgetRevision; - }, unknown, { - isOver: boolean; - CanDrop: boolean; - }>(() => ({ - accept: ['widget'], - drop(item, monitor) { - if (targetRef.current) { - if (item.widgetSet === '__marketplace') { - props.addMarketplaceWidget( - (item.widgetType as MarketplaceWidgetRevision).id, - monitor.getClientOffset().x - targetRef.current.getBoundingClientRect().x, - monitor.getClientOffset().y - targetRef.current.getBoundingClientRect().y, - ); - } else { - props.addWidget( - item.widgetType.name, - monitor.getClientOffset().x - targetRef.current.getBoundingClientRect().x, - monitor.getClientOffset().y - targetRef.current.getBoundingClientRect().y, - ); - } - } + const [{ CanDrop, isOver }, drop] = useDrop< + { + widgetSet: WidgetSetName; + widgetType: WidgetType | MarketplaceWidgetRevision; }, - canDrop: () => props.editMode, - collect: monitor => ({ - isOver: monitor.isOver(), - CanDrop: monitor.canDrop(), + unknown, + { + isOver: boolean; + CanDrop: boolean; + } + >( + () => ({ + accept: ['widget'], + drop(item, monitor) { + if (targetRef.current) { + if (item.widgetSet === '__marketplace') { + props.addMarketplaceWidget( + (item.widgetType as MarketplaceWidgetRevision).id, + monitor.getClientOffset().x - targetRef.current.getBoundingClientRect().x, + monitor.getClientOffset().y - targetRef.current.getBoundingClientRect().y, + ); + } else { + props.addWidget( + item.widgetType.name, + monitor.getClientOffset().x - targetRef.current.getBoundingClientRect().x, + monitor.getClientOffset().y - targetRef.current.getBoundingClientRect().y, + ); + } + } + }, + canDrop: () => props.editMode, + collect: monitor => ({ + isOver: monitor.isOver(), + CanDrop: monitor.canDrop(), + }), }), - }), [props.editMode]); - - return
    -
    - {props.children} + [props.editMode], + ); + + return ( +
    +
    + {props.children} +
    -
    ; + ); }; export interface EditorProps extends RuntimeProps { @@ -252,19 +301,26 @@ export interface EditorState extends RuntimeState { hideAttributes: boolean; toolbarHeight: 'narrow' | 'veryNarrow'; loadingText: string; - legacyFileSelector: { - callback: (data: { path: string; file: string }, userArg: any) => void; - options: { - path?: string; - userArg?: any; - }; - } | false; - + legacyFileSelector: + | { + callback: (data: { path: string; file: string }, userArg: any) => void; + options: { + path?: string; + userArg?: any; + }; + } + | false; } declare global { interface Window { - visAddWidget: (widgetType: string, x: number, y: number, data: Partial, style: Partial) => Promise; + visAddWidget: ( + widgetType: string, + x: number, + y: number, + data: Partial, + style: Partial, + ) => Promise; } } @@ -299,7 +355,10 @@ class Editor extends Runtime { if (window.location.search.includes('runtime') || !window.location.pathname.endsWith('edit.html')) { runtime = true; } - if (window.location.search.includes('edit') || (window.location.port.startsWith('300') && !window.location.search.includes('runtime'))) { + if ( + window.location.search.includes('edit') || + (window.location.port.startsWith('300') && !window.location.search.includes('runtime')) + ) { runtime = false; } @@ -364,7 +423,7 @@ class Editor extends Runtime { onBeforeUnload = (e: BeforeUnloadEvent) => { if (this.state.needSave) { this.needRestart = true; - e.returnValue = I18n.t('Project doesn\'t saved. Are you sure?'); + e.returnValue = I18n.t("Project doesn't saved. Are you sure?"); return e.returnValue; } @@ -402,11 +461,26 @@ class Editor extends Runtime { if (controlKey && e.key === 'a') { e.preventDefault(); if (this.state.selectedGroup) { - this.setSelectedWidgets((Object.keys(store.getState().visProject[this.state.selectedView].widgets) as AnyWidgetId[]) - .filter(widget => !store.getState().visProject[this.state.selectedView].widgets[widget].data.locked && store.getState().visProject[this.state.selectedView].widgets[widget].groupid === this.state.selectedGroup)); + this.setSelectedWidgets( + ( + Object.keys(store.getState().visProject[this.state.selectedView].widgets) as AnyWidgetId[] + ).filter( + widget => + !store.getState().visProject[this.state.selectedView].widgets[widget].data.locked && + store.getState().visProject[this.state.selectedView].widgets[widget].groupid === + this.state.selectedGroup, + ), + ); } else { - this.setSelectedWidgets((Object.keys(store.getState().visProject[this.state.selectedView].widgets) as AnyWidgetId[]) - .filter(widget => !store.getState().visProject[this.state.selectedView].widgets[widget].data.locked && !store.getState().visProject[this.state.selectedView].widgets[widget].grouped)); + this.setSelectedWidgets( + ( + Object.keys(store.getState().visProject[this.state.selectedView].widgets) as AnyWidgetId[] + ).filter( + widget => + !store.getState().visProject[this.state.selectedView].widgets[widget].data.locked && + !store.getState().visProject[this.state.selectedView].widgets[widget].grouped, + ), + ); } } if (e.key === 'Escape') { @@ -432,7 +506,7 @@ class Editor extends Runtime { }; setViewsManager = (isOpen: boolean) => { - if ((!!isOpen) !== this.state.viewsManager) { + if (!!isOpen !== this.state.viewsManager) { this.setState({ viewsManager: !!isOpen }); } }; @@ -441,13 +515,16 @@ class Editor extends Runtime { loadSelectedWidgets(selectedView: string) { selectedView = selectedView || this.state.selectedView; - const selectedWidgets: AnyWidgetId[] = JSON.parse(window.localStorage.getItem( - `${this.state.projectName}.${selectedView}.widgets`, - ) || '[]') || []; + const selectedWidgets: AnyWidgetId[] = + JSON.parse(window.localStorage.getItem(`${this.state.projectName}.${selectedView}.widgets`) || '[]') || []; // Check that all selectedWidgets exist for (let i = selectedWidgets.length - 1; i >= 0; i--) { - if (!store.getState().visProject[selectedView] || !store.getState().visProject[selectedView].widgets || !store.getState().visProject[selectedView].widgets[selectedWidgets[i]]) { + if ( + !store.getState().visProject[selectedView] || + !store.getState().visProject[selectedView].widgets || + !store.getState().visProject[selectedView].widgets[selectedWidgets[i]] + ) { selectedWidgets.splice(i, 1); } } @@ -455,7 +532,13 @@ class Editor extends Runtime { return selectedWidgets; } - addWidget = async (widgetType: string, x: number, y: number, data?: Partial, style?: Partial): Promise => { + addWidget = async ( + widgetType: string, + x: number, + y: number, + data?: Partial, + style?: Partial, + ): Promise => { const project = deepClone(store.getState().visProject); const widgets = project[this.state.selectedView].widgets; const newKey = getNewWidgetId(store.getState().visProject); @@ -482,7 +565,9 @@ class Editor extends Runtime { const tplWidget = widgetTypes.find(item => item.name === widgetType); // extract groups - const fields: WidgetAttributesGroupInfoStored[] = parseAttributes((tplWidget.params as string | RxWidgetInfoGroup[])); + const fields: WidgetAttributesGroupInfoStored[] = parseAttributes( + tplWidget.params as string | RxWidgetInfoGroup[], + ); fields.forEach(group => { if (group.fields) { @@ -526,12 +611,16 @@ class Editor extends Runtime { deleteWidgets = async () => this.setState({ deleteWidgetsDialog: true }); - updateWidgets = async (marketplaceWidget: MarketplaceWidgetRevision) => this.setState({ updateWidgetsDialog: marketplaceWidget }); + updateWidgets = async (marketplaceWidget: MarketplaceWidgetRevision) => + this.setState({ updateWidgetsDialog: marketplaceWidget }); - updateWidgetsAction = async (marketplace: MarketplaceWidgetRevision, widgets: { - name: string; - widgets: AnyWidgetId[]; - }[]) => { + updateWidgetsAction = async ( + marketplace: MarketplaceWidgetRevision, + widgets: { + name: string; + widgets: AnyWidgetId[]; + }[], + ) => { await this.installWidget(marketplace.widget_id, marketplace.id); const project = deepClone(store.getState().visProject); widgets.forEach(view => { @@ -590,8 +679,7 @@ class Editor extends Runtime { lockWidgets = async (type: 'lock' | 'unlock') => { const project = deepClone(store.getState().visProject); const widgets = project[this.state.selectedView].widgets; - this.state.selectedWidgets.forEach(selectedWidget => - widgets[selectedWidget].data.locked = type === 'lock'); + this.state.selectedWidgets.forEach(selectedWidget => (widgets[selectedWidget].data.locked = type === 'lock')); await this.changeProject(project); }; @@ -667,7 +755,10 @@ class Editor extends Runtime { for (const clipboardWidgetId of Object.keys(this.state.widgetsClipboard.widgets)) { const newWidget = deepClone(this.state.widgetsClipboard.widgets[clipboardWidgetId as AnyWidgetId]); - if (this.state.widgetsClipboard.type === 'copy' && this.state.selectedView === this.state.widgetsClipboard.view) { + if ( + this.state.widgetsClipboard.type === 'copy' && + this.state.selectedView === this.state.widgetsClipboard.view + ) { const boundingRect = Editor.getWidgetRelativeRect(clipboardWidgetId as AnyWidgetId); newWidget.style = this.pxToPercent(newWidget.style, { left: `${(boundingRect?.left ?? 0) + 10}px`, @@ -678,13 +769,21 @@ class Editor extends Runtime { if (isGroup(newWidget)) { newKey = pasteGroup({ - group: newWidget, widgets, offset: groupOffset, groupMembers: this.state.widgetsClipboard.groupMembers, project: store.getState().visProject, + group: newWidget, + widgets, + offset: groupOffset, + groupMembers: this.state.widgetsClipboard.groupMembers, + project: store.getState().visProject, }); groupOffset++; } else { newKey = pasteSingleWidget({ - widget: newWidget, offset: widgetOffset, project: store.getState().visProject, selectedGroup: this.state.selectedGroup, widgets, + widget: newWidget, + offset: widgetOffset, + project: store.getState().visProject, + selectedGroup: this.state.selectedGroup, + widgets, }); widgetOffset++; } @@ -712,11 +811,17 @@ class Editor extends Runtime { if (isGroup(newWidget)) { pasteGroup({ - group: newWidget, widgets, groupMembers: widgets, project: store.getState().visProject, + group: newWidget, + widgets, + groupMembers: widgets, + project: store.getState().visProject, }); } else { const newKey = pasteSingleWidget({ - widget: newWidget, project: store.getState().visProject, selectedGroup: this.state.selectedGroup, widgets, + widget: newWidget, + project: store.getState().visProject, + selectedGroup: this.state.selectedGroup, + widgets, }); newKeys.push(newKey); @@ -727,11 +832,28 @@ class Editor extends Runtime { this.setSelectedWidgets(newKeys); }; - alignWidgets = (type: 'left' | 'right' | 'top' | 'bottom' | 'horizontal-center' | 'vertical-center' | 'horizontal-equal' | 'vertical-equal' | 'width' | 'height') => { + alignWidgets = ( + type: + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'horizontal-center' + | 'vertical-center' + | 'horizontal-equal' + | 'vertical-equal' + | 'width' + | 'height', + ) => { const project = deepClone(store.getState().visProject); const widgets = project[this.state.selectedView].widgets; const newCoordinates = { - left: 0, top: 0, width: 0, height: 0, right: 0, bottom: 0, + left: 0, + top: 0, + width: 0, + height: 0, + right: 0, + bottom: 0, }; const selectedWidgets: { id: AnyWidgetId; @@ -748,28 +870,34 @@ class Editor extends Runtime { newCoordinates.left = selectedWidget.coordinate.left; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.left = `${newCoordinates.left}px`); + selectedWidgets.forEach(selectedWidget => (selectedWidget.widget.style.left = `${newCoordinates.left}px`)); } else if (type === 'right') { selectedWidgets.forEach(selectedWidget => { if (newCoordinates.right === 0 || selectedWidget.coordinate.right > newCoordinates.right) { newCoordinates.right = selectedWidget.coordinate.right; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.left = `${newCoordinates.right - selectedWidget.coordinate.width}px`); + selectedWidgets.forEach( + selectedWidget => + (selectedWidget.widget.style.left = `${newCoordinates.right - selectedWidget.coordinate.width}px`), + ); } else if (type === 'top') { selectedWidgets.forEach(selectedWidget => { if (newCoordinates.top === 0 || selectedWidget.coordinate.top < newCoordinates.top) { newCoordinates.top = selectedWidget.coordinate.top; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.top = `${newCoordinates.top}px`); + selectedWidgets.forEach(selectedWidget => (selectedWidget.widget.style.top = `${newCoordinates.top}px`)); } else if (type === 'bottom') { selectedWidgets.forEach(selectedWidget => { if (newCoordinates.bottom === 0 || selectedWidget.coordinate.bottom > newCoordinates.bottom) { newCoordinates.bottom = selectedWidget.coordinate.bottom; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.top = `${newCoordinates.bottom - selectedWidget.coordinate.height}px`); + selectedWidgets.forEach( + selectedWidget => + (selectedWidget.widget.style.top = `${newCoordinates.bottom - selectedWidget.coordinate.height}px`), + ); } else if (type === 'horizontal-center') { selectedWidgets.forEach(selectedWidget => { if (newCoordinates.left === 0 || selectedWidget.coordinate.left < newCoordinates.left) { @@ -779,7 +907,10 @@ class Editor extends Runtime { newCoordinates.right = selectedWidget.coordinate.right; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.left = `${(newCoordinates.left + (newCoordinates.right - newCoordinates.left) / 2) - (selectedWidget.coordinate.width / 2)}px`); + selectedWidgets.forEach( + selectedWidget => + (selectedWidget.widget.style.left = `${newCoordinates.left + (newCoordinates.right - newCoordinates.left) / 2 - selectedWidget.coordinate.width / 2}px`), + ); } else if (type === 'vertical-center') { selectedWidgets.forEach(selectedWidget => { if (newCoordinates.top === 0 || selectedWidget.coordinate.top < newCoordinates.top) { @@ -789,12 +920,17 @@ class Editor extends Runtime { newCoordinates.bottom = selectedWidget.coordinate.bottom; } }); - selectedWidgets.forEach(selectedWidget => selectedWidget.widget.style.top = `${(newCoordinates.top + (newCoordinates.bottom - newCoordinates.top) / 2) - (selectedWidget.coordinate.height / 2)}px`); + selectedWidgets.forEach( + selectedWidget => + (selectedWidget.widget.style.top = `${newCoordinates.top + (newCoordinates.bottom - newCoordinates.top) / 2 - selectedWidget.coordinate.height / 2}px`), + ); } else if (type === 'horizontal-equal') { let widgetsWidth = 0; let spaceWidth = 0; let currentLeft = 0; - selectedWidgets.sort((selectedWidget1, selectedWidget2) => (selectedWidget1.coordinate.left > selectedWidget2.coordinate.left ? 1 : -1)); + selectedWidgets.sort((selectedWidget1, selectedWidget2) => + selectedWidget1.coordinate.left > selectedWidget2.coordinate.left ? 1 : -1, + ); selectedWidgets.forEach(selectedWidget => { if (newCoordinates.left === 0 || selectedWidget.coordinate.left < newCoordinates.left) { newCoordinates.left = selectedWidget.coordinate.left; @@ -819,7 +955,9 @@ class Editor extends Runtime { let widgetsHeight = 0; let spaceHeight = 0; let currentTop = 0; - selectedWidgets.sort((selectedWidget1, selectedWidget2) => (selectedWidget1.coordinate.top > selectedWidget2.coordinate.top ? 1 : -1)); + selectedWidgets.sort((selectedWidget1, selectedWidget2) => + selectedWidget1.coordinate.top > selectedWidget2.coordinate.top ? 1 : -1, + ); selectedWidgets.forEach(selectedWidget => { if (newCoordinates.top === 0 || selectedWidget.coordinate.top < newCoordinates.top) { newCoordinates.top = selectedWidget.coordinate.top; @@ -851,7 +989,9 @@ class Editor extends Runtime { this.state.selectedWidgets.forEach(selectedWidget => { const boundingRect = window.document.getElementById(selectedWidget).getBoundingClientRect(); const w = boundingRect.width; - if (alignValues.indexOf(w) === -1) { alignValues.push(w); } + if (alignValues.indexOf(w) === -1) { + alignValues.push(w); + } }); } @@ -875,7 +1015,9 @@ class Editor extends Runtime { this.state.selectedWidgets.forEach(selectedWidget => { const boundingRect = window.document.getElementById(selectedWidget).getBoundingClientRect(); const h = boundingRect.height; - if (alignValues.indexOf(h) === -1) { alignValues.push(h); } + if (alignValues.indexOf(h) === -1) { + alignValues.push(h); + } }); } @@ -953,9 +1095,7 @@ class Editor extends Runtime { data: { members: this.state.selectedWidgets, }, - style: { - - }, + style: {}, }; const groupId = getNewGroupId(project); let left = 0; @@ -1127,7 +1267,11 @@ class Editor extends Runtime { if ('TextEncoder' in window) { const encoder = new TextEncoder(); const data = encoder.encode(projectStr); - await this.socket.writeFile64(this.adapterId, `${this.state.projectName}/vis-views.json`, data as unknown as string); + await this.socket.writeFile64( + this.adapterId, + `${this.state.projectName}/vis-views.json`, + data as unknown as string, + ); } else { await this.socket.writeFile64(this.adapterId, `${this.state.projectName}/vis-views.json`, projectStr); } @@ -1190,7 +1334,11 @@ class Editor extends Runtime { } }; - setSelectedWidgets = async (selectedWidgets: AnyWidgetId[], selectedView?: string | (() => void), cb?: () => void) => { + setSelectedWidgets = async ( + selectedWidgets: AnyWidgetId[], + selectedView?: string | (() => void), + cb?: () => void, + ) => { if (typeof selectedView === 'function') { cb = selectedView; selectedView = null; @@ -1205,11 +1353,17 @@ class Editor extends Runtime { store.dispatch(recalculateFields(true)); if (selectedView) { - window.localStorage.setItem(`${this.state.projectName}.${selectedView}.widgets`, JSON.stringify(selectedWidgets)); + window.localStorage.setItem( + `${this.state.projectName}.${selectedView}.widgets`, + JSON.stringify(selectedWidgets), + ); // changeView reads selected widgets from localStorage - await this.changeView(selectedView as string/* , true, true, true */); + await this.changeView(selectedView as string /* , true, true, true */); } else { - window.localStorage.setItem(`${this.state.projectName}.${this.state.selectedView}.widgets`, JSON.stringify(selectedWidgets)); + window.localStorage.setItem( + `${this.state.projectName}.${this.state.selectedView}.widgets`, + JSON.stringify(selectedWidgets), + ); await this.setStateAsync({ selectedWidgets, @@ -1226,12 +1380,16 @@ class Editor extends Runtime { window.localStorage.setItem('showCode', JSON.stringify(!oldShowCode)); }; - onWidgetsChanged = (changedData: { - wid: AnyWidgetId; - view: string; - style: WidgetStyle; - data: WidgetData; - }[], view: string, viewSettings: ViewSettings) => { + onWidgetsChanged = ( + changedData: { + wid: AnyWidgetId; + view: string; + style: WidgetStyle; + data: WidgetData; + }[], + view: string, + viewSettings: ViewSettings, + ) => { this.tempProject = this.tempProject || deepClone(store.getState().visProject); changedData?.forEach(item => { if (item.style) { @@ -1295,14 +1453,21 @@ class Editor extends Runtime { } cssClone = (attr: string, cb: (value: string | number | boolean) => void) => { - if (this.visEngineHandlers[this.state.selectedView] && this.visEngineHandlers[this.state.selectedView].onStealStyle) { + if ( + this.visEngineHandlers[this.state.selectedView] && + this.visEngineHandlers[this.state.selectedView].onStealStyle + ) { this.visEngineHandlers[this.state.selectedView].onStealStyle(attr, cb); } else { cb && cb(attr); // cancel selection } }; - registerCallback = (name: keyof VisEngineHandlers, view: string, cb: VisEngineHandlers[keyof VisEngineHandlers]) => { + registerCallback = ( + name: keyof VisEngineHandlers, + view: string, + cb: VisEngineHandlers[keyof VisEngineHandlers], + ) => { if (cb) { this.visEngineHandlers[view] = this.visEngineHandlers[view] || {}; (this.visEngineHandlers[view][name] as VisEngineHandlers[keyof VisEngineHandlers]) = cb; @@ -1315,7 +1480,10 @@ class Editor extends Runtime { }; onPxToPercent = (wids: AnyWidgetId[], attr: string, cb: (results: string[]) => void) => { - if (this.visEngineHandlers[this.state.selectedView] && this.visEngineHandlers[this.state.selectedView].onPxToPercent) { + if ( + this.visEngineHandlers[this.state.selectedView] && + this.visEngineHandlers[this.state.selectedView].onPxToPercent + ) { return this.visEngineHandlers[this.state.selectedView].onPxToPercent(wids, attr, cb); } @@ -1323,7 +1491,10 @@ class Editor extends Runtime { }; pxToPercent = (oldStyle: WidgetStyle, newStyle: WidgetStyle) => { - if (this.visEngineHandlers[this.state.selectedView] && this.visEngineHandlers[this.state.selectedView].pxToPercent) { + if ( + this.visEngineHandlers[this.state.selectedView] && + this.visEngineHandlers[this.state.selectedView].pxToPercent + ) { return this.visEngineHandlers[this.state.selectedView].pxToPercent(oldStyle, newStyle); } // cb && cb(wids, attr, null); // cancel selection @@ -1331,7 +1502,10 @@ class Editor extends Runtime { }; onPercentToPx = (wids: AnyWidgetId[], attr: string, cb: (results: string[]) => void) => { - if (this.visEngineHandlers[this.state.selectedView] && this.visEngineHandlers[this.state.selectedView].onPercentToPx) { + if ( + this.visEngineHandlers[this.state.selectedView] && + this.visEngineHandlers[this.state.selectedView].onPercentToPx + ) { return this.visEngineHandlers[this.state.selectedView].onPercentToPx(wids, attr, cb); } return null; @@ -1358,11 +1532,7 @@ class Editor extends Runtime { this.setState({ confirmDialog }); } - showCodeDialog(codeDialog: { - code: string; - title: string; - mode: string; - }) { + showCodeDialog(codeDialog: { code: string; title: string; mode: string }) { this.setState({ showCodeDialog: codeDialog }); } @@ -1375,7 +1545,9 @@ class Editor extends Runtime { if (!project.___settings.marketplace) { project.___settings.marketplace = []; } - const widgetIndex = project.___settings.marketplace.findIndex(item => item.widget_id === marketplaceWidget.widget_id); + const widgetIndex = project.___settings.marketplace.findIndex( + item => item.widget_id === marketplaceWidget.widget_id, + ); if (widgetIndex === -1) { project.___settings.marketplace.push(marketplaceWidget); } else { @@ -1394,12 +1566,24 @@ class Editor extends Runtime { await this.changeProject(project); }; - static importMarketplaceWidget(project: Project, view: string, widgets: (GroupWidget | SingleWidget)[], id: string, x: number, y: number, widgetId: AnyWidgetId, oldData: WidgetData, oldStyle: WidgetStyle) { + static importMarketplaceWidget( + project: Project, + view: string, + widgets: (GroupWidget | SingleWidget)[], + id: string, + x: number, + y: number, + widgetId: AnyWidgetId, + oldData: WidgetData, + oldStyle: WidgetStyle, + ) { const newWidgets: Record = {}; widgets.forEach(_widget => { if (_widget.isRoot) { - _widget.marketplace = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.id === id)); + _widget.marketplace = deepClone( + store.getState().visProject.___settings.marketplace.find(item => item.id === id), + ); } if (isGroup(_widget)) { let newKey: AnyWidgetId = getNewGroupId(store.getState().visProject); @@ -1426,7 +1610,12 @@ class Editor extends Runtime { } else { const newKey = getNewWidgetId(store.getState().visProject); newWidgets[newKey as GroupWidgetId] = _widget; - if (_widget.grouped && newWidgets[_widget.groupid] && newWidgets[_widget.groupid].data && newWidgets[_widget.groupid].data.members) { + if ( + _widget.grouped && + newWidgets[_widget.groupid] && + newWidgets[_widget.groupid].data && + newWidgets[_widget.groupid].data.members + ) { // find group const pos = newWidgets[_widget.groupid].data.members.indexOf(_widget._id as AnyWidgetId); if (pos !== -1) { @@ -1442,10 +1631,29 @@ class Editor extends Runtime { return project; } - addMarketplaceWidget = async (id: string, x: number, y: number, widgetId?: AnyWidgetId, oldData?: WidgetData, oldStyle?: WidgetStyle) => { + addMarketplaceWidget = async ( + id: string, + x: number, + y: number, + widgetId?: AnyWidgetId, + oldData?: WidgetData, + oldStyle?: WidgetStyle, + ) => { const project = deepClone(store.getState().visProject); - const widgets = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.id === id).widget); - Editor.importMarketplaceWidget(project, this.state.selectedView, widgets, id, x, y, widgetId, oldData, oldStyle); + const widgets = deepClone( + store.getState().visProject.___settings.marketplace.find(item => item.id === id).widget, + ); + Editor.importMarketplaceWidget( + project, + this.state.selectedView, + widgets, + id, + x, + y, + widgetId, + oldData, + oldStyle, + ); await this.changeProject(project); }; @@ -1453,7 +1661,11 @@ class Editor extends Runtime { const project = deepClone(store.getState().visProject); const widget = project[this.state.selectedView].widgets[id]; if (widget && widget.marketplace) { - const marketplace = deepClone(store.getState().visProject.___settings.marketplace.find(item => item.widget_id === widget.marketplace.widget_id)); + const marketplace = deepClone( + store + .getState() + .visProject.___settings.marketplace.find(item => item.widget_id === widget.marketplace.widget_id), + ); await this.deleteWidgetsAction(); await this.addMarketplaceWidget(marketplace.id, null, null, id, widget.data, widget.style); } @@ -1461,51 +1673,58 @@ class Editor extends Runtime { renderAskAboutIncludeDialog() { if (this.state.askAboutInclude) { - return this.setState({ askAboutInclude: null })} - > - {I18n.t('Include widget?')} - - - {I18n.t('Do you want to include "%s" widget into "%s"?', this.state.askAboutInclude.wid, this.state.askAboutInclude.toWid)} - - - - - - - - ; + return ( + this.setState({ askAboutInclude: null })} + > + {I18n.t('Include widget?')} + + + {I18n.t( + 'Do you want to include "%s" widget into "%s"?', + this.state.askAboutInclude.wid, + this.state.askAboutInclude.toWid, + )} + + + + + + + + + ); } return null; @@ -1519,268 +1738,342 @@ class Editor extends Runtime { const { visProject } = store.getState(); const views = visProject.___settings.openedViews.filter(view => Object.keys(visProject).includes(view)); - return
    - {this.state.hidePalette ? -
    - { - window.localStorage.removeItem('Vis.hidePalette'); - this.setState({ hidePalette: false }); - }} + return ( +
    + {this.state.hidePalette ? ( + - - -
    - : null} - {!this.state.showCode ? -
    - this.setState({ editMode: !this.state.editMode })} - size="small" - disabled={!!this.state.selectedGroup} - style={this.state.selectedGroup ? { opacity: 0.5 } : null} +
    + { + window.localStorage.removeItem('Vis.hidePalette'); + this.setState({ hidePalette: false }); + }} + > + + +
    + + ) : null} + {!this.state.showCode ? ( + - {this.state.editMode ? : } -
    -
    -
    : null} - -
    - this.setViewsManager(true)} size="small" disabled={!!this.state.selectedGroup}> - - -
    -
    - - { - views.map(view => { +
    + this.setState({ editMode: !this.state.editMode })} + size="small" + disabled={!!this.state.selectedGroup} + style={this.state.selectedGroup ? { opacity: 0.5 } : null} + > + {this.state.editMode ? ( + + ) : ( + + )} + +
    + + ) : null} + +
    + this.setViewsManager(true)} + size="small" + disabled={!!this.state.selectedGroup} + > + + +
    +
    + + {views.map(view => { const isGroupEdited = !!this.state.selectedGroup && view === this.state.selectedView; - const viewSettings = isGroupEdited ? {} : (store.getState().visProject[view].settings || {}); + const viewSettings = isGroupEdited ? {} : store.getState().visProject[view].settings || {}; let icon = viewSettings.navigationIcon || viewSettings.navigationImage; if (icon && icon.startsWith('_PRJ_NAME/')) { - icon = `../${this.adapterName}.${this.instance}/${this.state.projectName}${icon.substring(9)}`; // "_PRJ_NAME".length = 9 + icon = `../${this.adapterName}.${this.instance}/${this.state.projectName}${icon.substring(9)}`; // "_PRJ_NAME".length = 9 } - return - {icon ? : null} - {isGroupEdited ? `${I18n.t('Group %s', this.state.selectedGroup)}` : (viewSettings.navigationTitle || view)} - - - { - e.stopPropagation(); - if (isGroupEdited) { - this.setState({ selectedGroup: null }); - } else { - this.toggleView(view, false); - } - }} + disabled={!!this.state.selectedGroup && view !== this.state.selectedView} + label={ + + {icon ? ( + + ) : null} + {isGroupEdited + ? `${I18n.t('Group %s', this.state.selectedGroup)}` + : viewSettings.navigationTitle || view} + - - - - - } - sx={styles.viewTab} - value={view} - onClick={() => this.changeView(view)} - key={view} - // wrapped - />; - }) - } - - this.toggleCode()} - size="small" - style={{ - ...styles.tabButton, - cursor: 'default', - opacity: this.state.showCode ? 1 : 0, - width: 34, - height: 34, - }} - > - {this.state.showCode ? : } - - {views.length > 1 ? -
    - { - const project = deepClone(store.getState().visProject); - project.___settings.openedViews = [this.state.selectedView]; - this.changeProject(project, true); - }} + + { + e.stopPropagation(); + if (isGroupEdited) { + this.setState({ selectedGroup: null }); + } else { + this.toggleView(view, false); + } + }} + > + + + + + + } + sx={styles.viewTab} + value={view} + onClick={() => this.changeView(view)} + key={view} + // wrapped + /> + ); + })} + + this.toggleCode()} + size="small" + style={{ + ...styles.tabButton, + cursor: 'default', + opacity: this.state.showCode ? 1 : 0, + width: 34, + height: 34, + }} + > + {this.state.showCode ? : } + + {views.length > 1 ? ( + - - -
    -
    : null} - {this.state.hideAttributes ? -
    - { - window.localStorage.removeItem('Vis.hideAttributes'); - this.setState({ hideAttributes: false }); - }} +
    + { + const project = deepClone(store.getState().visProject); + project.___settings.openedViews = [this.state.selectedView]; + this.changeProject(project, true); + }} + > + + +
    + + ) : null} + {this.state.hideAttributes ? ( + - -
    -
    -
    : null} -
    ; +
    + { + window.localStorage.removeItem('Vis.hideAttributes'); + this.setState({ hideAttributes: false }); + }} + > + + +
    +
    + ) : null} +
    + ); } renderPalette() { - return
    - {this.state.widgetsLoaded !== Runtime.WIDGETS_LOADING_STEP_ALL_LOADED ? : null} - { - window.localStorage.setItem('Vis.hidePalette', 'true'); - this.setState({ hidePalette: true }); + return ( +
    -
    ; + > + {this.state.widgetsLoaded !== Runtime.WIDGETS_LOADING_STEP_ALL_LOADED ? ( + + ) : null} + { + window.localStorage.setItem('Vis.hidePalette', 'true'); + this.setState({ hidePalette: true }); + }} + uninstallWidget={this.uninstallWidget} + setMarketplaceDialog={this.setMarketplaceDialog} + updateWidgets={this.updateWidgets} + selectedView={this.state.selectedView} + changeView={this.changeView} + changeProject={this.changeProject} + socket={this.socket as unknown as LegacyConnection} + editMode={this.state.editMode} + themeType={this.state.themeType} + /> +
    + ); } renderWorkspace() { const visEngine = this.getVisEngine(); - return
    - {this.renderTabs()} -
    - {this.state.showCode - ?
    -                        {JSON.stringify(store.getState().visProject, null, 2)}
    -                    
    : null} - -
    + {this.renderTabs()} +
    + {this.state.showCode ?
    {JSON.stringify(store.getState().visProject, null, 2)}
    : null} + - - {visEngine} - -
    - + + {visEngine} + +
    +
    +
    -
    ; + ); } renderAttributes() { - return
    - { - window.localStorage.setItem('Vis.hideAttributes', 'true'); - this.setState({ hideAttributes: true }); + return ( +
    -
    ; + > + { + window.localStorage.setItem('Vis.hideAttributes', 'true'); + this.setState({ hideAttributes: true }); + }} + adapterId={this.adapterId} + /> +
    + ); } renderConfirmDialog() { if (this.state.confirmDialog) { - return { - const callback = this.state.confirmDialog.callback; - this.setState({ confirmDialog: null }, () => - typeof callback === 'function' && callback(isYes)); - }} - />; + return ( + { + const callback = this.state.confirmDialog.callback; + this.setState({ confirmDialog: null }, () => typeof callback === 'function' && callback(isYes)); + }} + /> + ); } return null; @@ -1788,13 +2081,15 @@ class Editor extends Runtime { renderShowCodeDialog() { if (this.state.showCodeDialog !== null) { - return this.setState({ showCodeDialog: null })} - title={this.state.showCodeDialog.title} - code={this.state.showCodeDialog.code} - mode={this.state.showCodeDialog.mode} - />; + return ( + this.setState({ showCodeDialog: null })} + title={this.state.showCodeDialog.title} + code={this.state.showCodeDialog.code} + mode={this.state.showCodeDialog.mode} + /> + ); } return null; @@ -1804,25 +2099,30 @@ class Editor extends Runtime { if (!this.state.showProjectUpdateDialog) { return null; } - return - this.setState({ showProjectUpdateDialog: false }, () => { - if (result) { - this.loadProject(this.state.projectName); - } - })} - />; + return ( + + this.setState({ showProjectUpdateDialog: false }, () => { + if (result) { + this.loadProject(this.state.projectName); + } + }) + } + /> + ); } renderCreateFirstProjectDialog() { - return this.state.createFirstProjectDialog ? this.setState({ createFirstProjectDialog: false })} - addProject={this.addProject} - /> : null; + return this.state.createFirstProjectDialog ? ( + this.setState({ createFirstProjectDialog: false })} + addProject={this.addProject} + /> + ) : null; } renderUpdateDialog() { @@ -1843,9 +2143,13 @@ class Editor extends Runtime { widgets: [], }; Object.keys(store.getState().visProject[view].widgets).forEach((widget: AnyWidgetId) => { - if (this.state.updateWidgetsDialog && - store.getState().visProject[view].widgets[widget].marketplace?.widget_id === this.state.updateWidgetsDialog.widget_id && - store.getState().visProject[view].widgets[widget].marketplace?.version !== this.state.updateWidgetsDialog.version) { + if ( + this.state.updateWidgetsDialog && + store.getState().visProject[view].widgets[widget].marketplace?.widget_id === + this.state.updateWidgetsDialog.widget_id && + store.getState().visProject[view].widgets[widget].marketplace?.version !== + this.state.updateWidgetsDialog.version + ) { viewWidgets.widgets.push(widget); } }); @@ -1854,35 +2158,39 @@ class Editor extends Runtime { } } }); - return -
    - {I18n.t('Are you sure to update widgets:')} -
    -
    - {widgets.map(view =>
    - - {view.name} - {': '} - - {view.widgets.join(', ')} -
    )} -
    - } - ok={I18n.t('Update')} - dialogName="updateDialog" - suppressQuestionMinutes={5} - onClose={isYes => { - if (isYes) { - if (this.state.updateWidgetsDialog) { - this.updateWidgetsAction(this.state.updateWidgetsDialog, widgets); - } + return ( + +
    {I18n.t('Are you sure to update widgets:')}
    +
    + {widgets.map(view => ( +
    + + {view.name} + {': '} + + {view.widgets.join(', ')} +
    + ))} +
    + } - this.setState({ updateWidgetsDialog: false }); - }} - />; + ok={I18n.t('Update')} + dialogName="updateDialog" + suppressQuestionMinutes={5} + onClose={isYes => { + if (isYes) { + if (this.state.updateWidgetsDialog) { + this.updateWidgetsAction(this.state.updateWidgetsDialog, widgets); + } + } + this.setState({ updateWidgetsDialog: false }); + }} + /> + ); } setLoadingText = (text: string) => { @@ -1890,7 +2198,7 @@ class Editor extends Runtime { }; renderDeleteDialog() { - return this.state.deleteWidgetsDialog ? + return this.state.deleteWidgetsDialog ? ( { this.setState({ deleteWidgetsDialog: false }); }} /> - : null; + ) : null; } renderMessageDialog() { - return this.state.messageDialog ? { - if (!this.state.messageDialog.noClose) { - this.setState({ messageDialog: null }); - } - }} - /> : null; + return this.state.messageDialog ? ( + { + if (!this.state.messageDialog.noClose) { + this.setState({ messageDialog: null }); + } + }} + /> + ) : null; } renderImportProjectDialog() { if (!this.state.showImportDialog) { return null; } - return { - this.setState({ showImportDialog: false }); - if (newProjectName) { - window.location.href = `edit.html?${newProjectName}`; - } - }} - openNewProjectOnCreate - projectName={this.state.projectName} - socket={this.socket as unknown as LegacyConnection} - adapterName={this.adapterName} - instance={this.instance} - loadProject={this.loadProject} - refreshProjects={this.refreshProjects} - />; + return ( + { + this.setState({ showImportDialog: false }); + if (newProjectName) { + window.location.href = `edit.html?${newProjectName}`; + } + }} + openNewProjectOnCreate + projectName={this.state.projectName} + socket={this.socket as unknown as LegacyConnection} + adapterName={this.adapterName} + instance={this.instance} + loadProject={this.loadProject} + refreshProjects={this.refreshProjects} + /> + ); } showLegacyFileSelector = ( @@ -1949,49 +2261,61 @@ class Editor extends Runtime { path?: string; userArg?: any; }, - ) => - this.setState({ legacyFileSelector: { callback, options } }); + ) => this.setState({ legacyFileSelector: { callback, options } }); renderLegacyFileSelectorDialog() { - return this.state.legacyFileSelector ? this.setState({ legacyFileSelector: false })} - restrictToFolder={`${this.adapterName}.${this.instance}/${this.state.projectName}`} - allowNonRestricted - allowUpload - allowDownload - allowCreateFolder - allowDelete - allowView - showToolbar - imagePrefix="../" - theme={this.state.theme} - selected={this.state.legacyFileSelector.options?.path || ''} - filterByType="images" - onOk={(selected: string) => { - const projectPrefix = `${this.adapterName}.${this.instance}/${this.state.projectName}/`; - if (selected.startsWith(projectPrefix)) { - selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; - } else if (selected.startsWith('/')) { - selected = `..${selected}`; - } else if (!selected.startsWith('.')) { - selected = `../${selected}`; - } - const parts = selected.split('/'); - const file = parts.pop(); - const path = `${parts.join('/')}/`; - this.state.legacyFileSelector && this.state.legacyFileSelector.callback({ path, file }, this.state.legacyFileSelector.options?.userArg); - this.setState({ legacyFileSelector: null }); - }} - socket={this.socket} - /> : null; + return this.state.legacyFileSelector ? ( + this.setState({ legacyFileSelector: false })} + restrictToFolder={`${this.adapterName}.${this.instance}/${this.state.projectName}`} + allowNonRestricted + allowUpload + allowDownload + allowCreateFolder + allowDelete + allowView + showToolbar + imagePrefix="../" + theme={this.state.theme} + selected={this.state.legacyFileSelector.options?.path || ''} + filterByType="images" + onOk={(selected: string) => { + const projectPrefix = `${this.adapterName}.${this.instance}/${this.state.projectName}/`; + if (selected.startsWith(projectPrefix)) { + selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; + } else if (selected.startsWith('/')) { + selected = `..${selected}`; + } else if (!selected.startsWith('.')) { + selected = `../${selected}`; + } + const parts = selected.split('/'); + const file = parts.pop(); + const path = `${parts.join('/')}/`; + this.state.legacyFileSelector && + this.state.legacyFileSelector.callback( + { path, file }, + this.state.legacyFileSelector.options?.userArg, + ); + this.setState({ legacyFileSelector: null }); + }} + socket={this.socket} + /> + ) : null; } renderLoadingText() { if (!this.state.loadingText) { return null; } - return {this.state.loadingText}; + return ( + + {this.state.loadingText} + + ); } toggleTheme(newThemeName?: ThemeName) { @@ -2003,28 +2327,30 @@ class Editor extends Runtime { render() { if (this.state.projectDoesNotExist) { - return - - {this.renderProjectDoesNotExist()} - - ; + return ( + + {this.renderProjectDoesNotExist()} + + ); } if (this.state.showProjectsDialog) { - return - - {this.showSmallProjectsDialog()} - - ; + return ( + + {this.showSmallProjectsDialog()} + + ); } if (!this.state.loaded || !store.getState().visProject.___settings || !this.state.userGroups) { - return - - {this.renderLoadingText()} - {this.renderLoader()} - - ; + return ( + + + {this.renderLoadingText()} + {this.renderLoader()} + + + ); } if (this.state.runtime) { @@ -2038,10 +2364,11 @@ class Editor extends Runtime { } } - return - - - - this.setState({ widgetsClipboard: { widgets: {}, type: '' } })} + + - {Object.keys(this.state.widgetsClipboard.widgets).join(', ')} - - - - this.toggleTheme()} - refreshProjects={this.refreshProjects} - viewsManager={this.state.viewsManager} - setViewsManager={this.setViewsManager} - projectsDialog={this.state.projects && this.state.projects.length ? this.state.projectsDialog : !this.state.createFirstProjectDialog} - setProjectsDialog={this.setProjectsDialog} - selectedWidgets={this.state.editMode ? this.state.selectedWidgets : []} - setSelectedWidgets={this.setSelectedWidgets} - history={this.state.history} - historyCursor={this.state.historyCursor} - undo={this.undo} - redo={this.redo} - deleteWidgets={this.deleteWidgets} - widgetsLoaded={this.state.widgetsLoaded === Runtime.WIDGETS_LOADING_STEP_ALL_LOADED} - widgetsClipboard={this.state.widgetsClipboard} - cutWidgets={this.cutWidgets} - copyWidgets={this.copyWidgets} - pasteWidgets={this.pasteWidgets} - alignWidgets={this.alignWidgets} - cloneWidgets={this.cloneWidgets} - orderWidgets={this.orderWidgets} - lockDragging={this.state.lockDragging} - // disableInteraction={this.state.disableInteraction} - toggleLockDragging={this.toggleLockDragging} - // toggleDisableInteraction={this.toggleDisableInteraction} - adapterName={this.adapterName} - selectedGroup={this.state.selectedGroup} - // setSelectedGroup={this.setSelectedGroup} - widgetHint={this.state.widgetHint} - toggleWidgetHint={this.toggleWidgetHint} - instance={this.instance} - editMode={this.state.editMode} - toolbarHeight={this.state.toolbarHeight} - setToolbarHeight={(value: 'narrow' | 'veryNarrow') => { - window.localStorage.setItem('Vis.toolbarForm', value); - this.setState({ toolbarHeight: value }); - }} - version={this.props.version} - /> -
    - - - {this.state.hidePalette && this.state.hideAttributes ? this.renderWorkspace() : null} - { - let splitSizes: [number, number, number] = [0, 0, 0]; - if (this.state.hidePalette && !this.state.hideAttributes) { - splitSizes[0] = this.state.splitSizes[0]; - splitSizes[1] = newSizes[0] - this.state.splitSizes[0]; - splitSizes[2] = newSizes[1]; - } else if (!this.state.hidePalette && this.state.hideAttributes) { - splitSizes[0] = newSizes[0]; - splitSizes[1] = newSizes[1] - this.state.splitSizes[2]; - splitSizes[2] = this.state.splitSizes[2]; - } else { - splitSizes = newSizes; + this.setState({ widgetsClipboard: { widgets: {}, type: '' } })} + > + {Object.keys(this.state.widgetsClipboard.widgets).join(', ')} + + + + this.toggleTheme()} + refreshProjects={this.refreshProjects} + viewsManager={this.state.viewsManager} + setViewsManager={this.setViewsManager} + projectsDialog={ + this.state.projects && this.state.projects.length + ? this.state.projectsDialog + : !this.state.createFirstProjectDialog + } + setProjectsDialog={this.setProjectsDialog} + selectedWidgets={this.state.editMode ? this.state.selectedWidgets : []} + setSelectedWidgets={this.setSelectedWidgets} + history={this.state.history} + historyCursor={this.state.historyCursor} + undo={this.undo} + redo={this.redo} + deleteWidgets={this.deleteWidgets} + widgetsLoaded={this.state.widgetsLoaded === Runtime.WIDGETS_LOADING_STEP_ALL_LOADED} + widgetsClipboard={this.state.widgetsClipboard} + cutWidgets={this.cutWidgets} + copyWidgets={this.copyWidgets} + pasteWidgets={this.pasteWidgets} + alignWidgets={this.alignWidgets} + cloneWidgets={this.cloneWidgets} + orderWidgets={this.orderWidgets} + lockDragging={this.state.lockDragging} + // disableInteraction={this.state.disableInteraction} + toggleLockDragging={this.toggleLockDragging} + // toggleDisableInteraction={this.toggleDisableInteraction} + adapterName={this.adapterName} + selectedGroup={this.state.selectedGroup} + // setSelectedGroup={this.setSelectedGroup} + widgetHint={this.state.widgetHint} + toggleWidgetHint={this.toggleWidgetHint} + instance={this.instance} + editMode={this.state.editMode} + toolbarHeight={this.state.toolbarHeight} + setToolbarHeight={(value: 'narrow' | 'veryNarrow') => { + window.localStorage.setItem('Vis.toolbarForm', value); + this.setState({ toolbarHeight: value }); + }} + version={this.props.version} + /> +
    + + + {this.state.hidePalette && this.state.hideAttributes ? this.renderWorkspace() : null} + { + let splitSizes: [number, number, number] = [0, 0, 0]; + if (this.state.hidePalette && !this.state.hideAttributes) { + splitSizes[0] = this.state.splitSizes[0]; + splitSizes[1] = newSizes[0] - this.state.splitSizes[0]; + splitSizes[2] = newSizes[1]; + } else if (!this.state.hidePalette && this.state.hideAttributes) { + splitSizes[0] = newSizes[0]; + splitSizes[1] = newSizes[1] - this.state.splitSizes[2]; + splitSizes[2] = this.state.splitSizes[2]; + } else { + splitSizes = newSizes; + } - const sum = splitSizes.reduce((prev, curr) => prev + curr); - if (Math.ceil(sum) !== 100) { - if (Math.ceil(sum) === 101 || Math.ceil(sum) === 99) { - // Round the first 2 sizes to 0.01 and calculate the last - splitSizes[0] = Math.round(splitSizes[0] * 100) / 100; - splitSizes[1] = Math.round(splitSizes[1] * 100) / 100; - splitSizes[2] = 100 - splitSizes[0] - splitSizes[1]; + const sum = splitSizes.reduce((prev, curr) => prev + curr); + if (Math.ceil(sum) !== 100) { + if (Math.ceil(sum) === 101 || Math.ceil(sum) === 99) { + // Round the first 2 sizes to 0.01 and calculate the last + splitSizes[0] = Math.round(splitSizes[0] * 100) / 100; + splitSizes[1] = Math.round(splitSizes[1] * 100) / 100; + splitSizes[2] = 100 - splitSizes[0] - splitSizes[1]; + this.setState({ splitSizes }); + window.localStorage.setItem( + 'Vis.splitSizes', + JSON.stringify(splitSizes), + ); + } else { + // https://github.com/devbookhq/splitter/issues/15 + console.log( + 'Decline resize, to work around bug in @devbookhq/splitter', + ); + this.setState({ splitSizes: this.state.splitSizes }); + } + } else { this.setState({ splitSizes }); window.localStorage.setItem('Vis.splitSizes', JSON.stringify(splitSizes)); - } else { - // https://github.com/devbookhq/splitter/issues/15 - console.log('Decline resize, to work around bug in @devbookhq/splitter'); - this.setState({ splitSizes: this.state.splitSizes }); } - } else { - this.setState({ splitSizes }); - window.localStorage.setItem('Vis.splitSizes', JSON.stringify(splitSizes)); + }} + // theme={this.state.themeType === 'dark' ? GutterTheme.Dark : GutterTheme.Light} + gutterClassName={ + this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter' } - }} - // theme={this.state.themeType === 'dark' ? GutterTheme.Dark : GutterTheme.Light} - gutterClassName={this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} - > - {!this.state.hidePalette ? this.renderPalette() : null} - {this.renderWorkspace()} - {!this.state.hideAttributes ? this.renderAttributes() : null} - - -
    -
    - {this.renderLoadingText()} - {this.renderCreateFirstProjectDialog()} - {this.renderDeleteDialog()} - {this.renderUpdateDialog()} - {this.renderAlertDialog()} - {this.renderConfirmDialog()} - {this.renderShowCodeDialog()} - {this.renderShowProjectUpdateDialog()} - {this.renderMessageDialog()} - {this.renderLegacyFileSelectorDialog()} - {this.renderAskAboutIncludeDialog()} - {this.state.marketplaceDialog ? this.setState({ marketplaceDialog: false })} - installWidget={this.installWidget} - updateWidgets={this.updateWidgets} - installedWidgets={store.getState().visProject?.___settings.marketplace} - {...this.state.marketplaceDialog} - themeName={this.state.themeName} - /> : null} - - ; + > + {!this.state.hidePalette ? this.renderPalette() : null} + {this.renderWorkspace()} + {!this.state.hideAttributes ? this.renderAttributes() : null} +
    +
    +
    +
    + {this.renderLoadingText()} + {this.renderCreateFirstProjectDialog()} + {this.renderDeleteDialog()} + {this.renderUpdateDialog()} + {this.renderAlertDialog()} + {this.renderConfirmDialog()} + {this.renderShowCodeDialog()} + {this.renderShowProjectUpdateDialog()} + {this.renderMessageDialog()} + {this.renderLegacyFileSelectorDialog()} + {this.renderAskAboutIncludeDialog()} + {this.state.marketplaceDialog ? ( + this.setState({ marketplaceDialog: false })} + installWidget={this.installWidget} + updateWidgets={this.updateWidgets} + installedWidgets={store.getState().visProject?.___settings.marketplace} + {...this.state.marketplaceDialog} + themeName={this.state.themeName} + /> + ) : null} +
    +
    + ); } } diff --git a/packages/iobroker.vis-2/src/src/Marketplace/MarketplaceDialog.tsx b/packages/iobroker.vis-2/src/src/Marketplace/MarketplaceDialog.tsx index 14b679b2c..1b4b534e9 100644 --- a/packages/iobroker.vis-2/src/src/Marketplace/MarketplaceDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Marketplace/MarketplaceDialog.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import { Close } from '@mui/icons-material'; import { I18n, type ThemeName } from '@iobroker/adapter-react-v5'; @@ -32,7 +30,7 @@ export interface MarketplaceDialogProps { themeName: ThemeName; } -const MarketplaceDialog = (props: MarketplaceDialogProps) => { +const MarketplaceDialog = (props: MarketplaceDialogProps): React.JSX.Element => { const VisMarketplace = window.VisMarketplace?.default; let installWidget: (marketplace: MarketplaceWidgetRevision) => void; @@ -50,7 +48,8 @@ const MarketplaceDialog = (props: MarketplaceDialogProps) => { }; Object.keys(project[view].widgets).forEach((wid: GroupWidgetId | SingleWidgetId) => { const widget: Widget = project[view].widgets[wid]; - if (widget.marketplace?.widget_id === marketplace.widget_id && + if ( + widget.marketplace?.widget_id === marketplace.widget_id && widget.marketplace?.version !== marketplace.version ) { viewWidgets.widgets.push(wid); @@ -70,37 +69,42 @@ const MarketplaceDialog = (props: MarketplaceDialogProps) => { }; } - return - {props.addPage ? I18n.t('Add new or update existing widget') : I18n.t('Browse the widgeteria')} - - {VisMarketplace && - // @ts-expect-error how to fix it? - props.onClose()} - />} - - - - - ; + return ( + + + {props.addPage ? I18n.t('Add new or update existing widget') : I18n.t('Browse the widgeteria')} + + + {VisMarketplace && ( + // @ts-expect-error how to fix it? + props.onClose()} + /> + )} + + + + + + ); }; export default MarketplaceDialog; diff --git a/packages/iobroker.vis-2/src/src/Marketplace/MarketplacePalette.tsx b/packages/iobroker.vis-2/src/src/Marketplace/MarketplacePalette.tsx index defb16f1c..5cf985ca0 100644 --- a/packages/iobroker.vis-2/src/src/Marketplace/MarketplacePalette.tsx +++ b/packages/iobroker.vis-2/src/src/Marketplace/MarketplacePalette.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { - Button, -} from '@mui/material'; +import { Button } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; import { type MarketplaceDialogProps } from './MarketplaceDialog'; @@ -10,10 +8,16 @@ interface MarketplacePaletteProps { setMarketplaceDialog: (props: Partial) => void; } -const MarketplacePalette = (props: MarketplacePaletteProps) =>
    - -
    ; +const MarketplacePalette = (props: MarketplacePaletteProps): React.JSX.Element => ( +
    + +
    +); export default MarketplacePalette; diff --git a/packages/iobroker.vis-2/src/src/Palette/Widget.tsx b/packages/iobroker.vis-2/src/src/Palette/Widget.tsx index 5d58602ef..ddfaa48a3 100644 --- a/packages/iobroker.vis-2/src/src/Palette/Widget.tsx +++ b/packages/iobroker.vis-2/src/src/Palette/Widget.tsx @@ -3,25 +3,13 @@ import { useDrag } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; import { Box, IconButton, Tooltip } from '@mui/material'; -import { - Delete as DeleteIcon, - Update as UpdateIcon, - Block as DeletedIcon, -} from '@mui/icons-material'; +import { Delete as DeleteIcon, Update as UpdateIcon, Block as DeletedIcon } from '@mui/icons-material'; -import { - I18n, - Utils, -} from '@iobroker/adapter-react-v5'; -import type { - LegacyConnection, - ThemeType, -} from '@iobroker/adapter-react-v5'; +import { I18n, Utils, type LegacyConnection, type ThemeType } from '@iobroker/adapter-react-v5'; import type { MarketplaceWidgetRevision, Project } from '@iobroker/types-vis-2'; import { store } from '@/Store'; -import commonStyles from '@/Utils/styles'; import type { WidgetType } from '@/Vis/visWidgetsCatalog'; import helpers from '../Components/WizardHelpers'; @@ -75,7 +63,7 @@ const styles: Record = { alignItems: 'center', overflow: 'hidden', }, - widgetMarketplace:{ + widgetMarketplace: { fontSize: '80%', fontStyle: 'italic', }, @@ -108,7 +96,7 @@ interface WidgetProps { marketplaceDeleted?: string[]; } -const Widget = (props: WidgetProps) => { +const Widget = (props: WidgetProps): React.JSX.Element => { const imageRef = useRef(); const style: React.CSSProperties = {}; @@ -142,35 +130,44 @@ const Widget = (props: WidgetProps) => { if (props.widgetType.preview?.startsWith('; + img = ( + {props.widgetType.name} + ); } - } else if (props.widgetType.preview && - ( - IMAGE_TYPES.find(ext => props.widgetType.preview.toLowerCase().endsWith(ext)) || - props.widgetSet === '__marketplace' - ) + } else if ( + props.widgetType.preview && + (IMAGE_TYPES.find(ext => props.widgetType.preview.toLowerCase().endsWith(ext)) || + props.widgetSet === '__marketplace') ) { - img = {props.widgetType.name} { - if (e.target) { - (e.target as HTMLImageElement).onerror = null; - (e.target as HTMLImageElement).src = './img/no-image.svg'; - (e.target as HTMLImageElement).style.height = '24px'; - } - }} - />; + img = ( + {props.widgetType.name} { + if (e.target) { + (e.target as HTMLImageElement).onerror = null; + (e.target as HTMLImageElement).src = './img/no-image.svg'; + (e.target as HTMLImageElement).style.height = '24px'; + } + }} + /> + ); } if (!img) { - img = ; + img = ( + + ); } let label = props.widgetType.label ? I18n.t(props.widgetType.label) : window.vis._(props.widgetType.title); @@ -186,61 +183,81 @@ const Widget = (props: WidgetProps) => { marketplaceDeleted = props.marketplaceDeleted?.includes(props.widgetMarketplaceId); } - const result = -
    {img}
    - {props.widgetType.help ?
    {props.widgetType.help}
    : null} - } - componentsProps={{ popper: { sx: commonStyles.tooltip } }} - placement="right-end" - > -
    - {props.widgetTypeName} -
    -
    {label}
    - {props.widgetSet === '__marketplace' && props.marketplace &&
    - {`${I18n.t('version')} ${props.marketplace.version}`} -
    } + const result = ( + +
    {img}
    + {props.widgetType.help ?
    {props.widgetType.help}
    : null} + + } + slotProps={{ popper: { sx: { pointerEvents: 'none' } } }} + placement="right-end" + > +
    + {props.widgetTypeName} +
    +
    {label}
    + {props.widgetSet === '__marketplace' && props.marketplace && ( +
    + {`${I18n.t('version')} ${props.marketplace.version}`} +
    + )} +
    + {props.widgetSet === '__marketplace' && ( + <> + + props.uninstallWidget(props.widgetType.name)}> + + + + {marketplaceUpdate && ( + + props.updateWidgets(marketplaceUpdate)}> + + + + )} + {marketplaceDeleted && ( + + + + )} + + )} + {img}
    - {props.widgetSet === '__marketplace' && <> - - props.uninstallWidget(props.widgetType.name)}> - - - - {marketplaceUpdate && - { - await props.updateWidgets(marketplaceUpdate); - }} - > - - - } - {marketplaceDeleted && - - } - } - - {img} - -
    - ; + + ); const widthRef = useRef(); - const [, dragRef, preview] = useDrag({ - type: 'widget', - item: () => ({ - widgetType: props.widgetType, - widgetSet: props.widgetSet, - preview:
    - {result} -
    , - }), - collect: monitor => ({ - isDragging: monitor.isDragging(), - handlerId: monitor.getHandlerId(), - }), - }, [props.widgetType]); + const [, dragRef, preview] = useDrag( + { + type: 'widget', + item: () => ({ + widgetType: props.widgetType, + widgetSet: props.widgetSet, + preview:
    {result}
    , + }), + collect: monitor => ({ + isDragging: monitor.isDragging(), + handlerId: monitor.getHandlerId(), + }), + }, + [props.widgetType], + ); useEffect(() => { preview(getEmptyImage(), { captureDraggingState: true }); @@ -261,11 +278,15 @@ const Widget = (props: WidgetProps) => { }); } - return - - {result} + return ( + + {result} - ; + ); }; export default Widget; diff --git a/packages/iobroker.vis-2/src/src/Palette/index.tsx b/packages/iobroker.vis-2/src/src/Palette/index.tsx index 75ea1f999..3dc072022 100644 --- a/packages/iobroker.vis-2/src/src/Palette/index.tsx +++ b/packages/iobroker.vis-2/src/src/Palette/index.tsx @@ -4,7 +4,9 @@ import { Accordion, AccordionDetails, AccordionSummary, - IconButton, InputAdornment, LinearProgress, + IconButton, + InputAdornment, + LinearProgress, TextField, Tooltip, Typography, @@ -19,18 +21,12 @@ import { Palette as IconPalette, } from '@mui/icons-material'; -import { - I18n, Utils, - Icon, - type LegacyConnection, - type ThemeType, -} from '@iobroker/adapter-react-v5'; +import { I18n, Utils, Icon, type LegacyConnection, type ThemeType } from '@iobroker/adapter-react-v5'; import type { Marketplace, MarketplaceWidgetRevision, VisTheme } from '@iobroker/types-vis-2'; import { store } from '@/Store'; -import type { WidgetType } from '@/Vis/visWidgetsCatalog'; -import { getWidgetTypes } from '@/Vis/visWidgetsCatalog'; +import { getWidgetTypes, type WidgetType } from '@/Vis/visWidgetsCatalog'; import { loadComponent } from '@/Vis/visLoadWidgets'; import commonStyles from '@/Utils/styles'; import type Editor from '../Editor'; @@ -172,13 +168,15 @@ class Palette extends Component { private marketplaceLoadingStarted = false; + private readonly lang: ioBroker.Languages = I18n.getLanguage(); + constructor(props: PaletteProps) { super(props); const accordionOpenStr = window.localStorage.getItem('widgets'); let accordionOpen; try { accordionOpen = accordionOpenStr ? JSON.parse(accordionOpenStr) : {}; - } catch (e) { + } catch { accordionOpen = {}; } @@ -193,13 +191,13 @@ class Palette extends Component { }; } - componentDidMount() { + componentDidMount(): void { if (this.state.accordionOpen.__marketplace) { this.loadMarketplace(); } } - loadMarketplace() { + loadMarketplace(): void { if ( this.state.accordionOpen.__marketplace && window.marketplaceClient && @@ -210,13 +208,21 @@ class Palette extends Component { this.marketplaceLoadingStarted = true; // load marketplace this.setState({ marketplaceLoading: true }, () => { - // @ts-expect-error solve later - const tPromise = loadComponent('__marketplace', 'default', './translations', `${window.marketplaceClient}/customWidgets.js`)() - .then((translations: any) => I18n.extendTranslations(translations.default)); - - // @ts-expect-error solve later - const mPromise = loadComponent('__marketplace', 'default', './VisMarketplace', `${window.marketplaceClient}/customWidgets.js`)() - .then(marketplace => window.VisMarketplace = marketplace as any as Marketplace); + const tPromise = loadComponent( + // @ts-expect-error solve later + '__marketplace', + 'default', + './translations', + `${window.marketplaceClient}/customWidgets.js`, + )().then((translations: any) => I18n.extendTranslations(translations.default)); + + const mPromise = loadComponent( + // @ts-expect-error solve later + '__marketplace', + 'default', + './VisMarketplace', + `${window.marketplaceClient}/customWidgets.js`, + )().then(marketplace => (window.VisMarketplace = marketplace as any as Marketplace)); Promise.all([tPromise, mPromise]) .then(async () => { @@ -252,7 +258,7 @@ class Palette extends Component { } } - buildWidgetList() { + buildWidgetList(): void { if (!this.props.widgetsLoaded) { if (this.state.widgetsList !== null || this.state.widgetSetProps !== null) { this.setState({ widgetsList: null, widgetSetProps: null }); @@ -300,7 +306,10 @@ class Palette extends Component { } const title = widgetType.label ? I18n.t(widgetType.label) : window.vis._(widgetType.title) || ''; - if (widgetType.hidden || (this.state.filter && !title.toLowerCase().includes(this.state.filter.toLowerCase()))) { + if ( + widgetType.hidden || + (this.state.filter && !title.toLowerCase().includes(this.state.filter.toLowerCase())) + ) { return; } @@ -330,7 +339,7 @@ class Palette extends Component { resultSet.unshift('basic'); } const sorted: Record> = {}; - resultSet.forEach(key => sorted[key] = _widgetsList[key]); + resultSet.forEach(key => (sorted[key] = _widgetsList[key])); _widgetsList = sorted; if (this.state.filter) { @@ -355,76 +364,87 @@ class Palette extends Component { }); } - renderMarketplace() { + renderMarketplace(): React.JSX.Element { const opened = this.state.accordionOpen.__marketplace; - return { - const accordionOpen = JSON.parse(JSON.stringify(this.state.accordionOpen)); - accordionOpen.__marketplace = expanded; - window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); - this.setState({ accordionOpen }, () => - expanded && this.loadMarketplace()); - }} - > - } - className={Utils.clsx('vis-palette-widget-set', opened && 'vis-palette-summary-expanded')} + return ( + { + const accordionOpen = JSON.parse(JSON.stringify(this.state.accordionOpen)); + accordionOpen.__marketplace = expanded; + window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); + this.setState({ accordionOpen }, () => expanded && this.loadMarketplace()); }} > - - {I18n.t('__marketplace')} - - - {opened && this.state.marketplaceLoading ? : null} - {opened && this.state.marketplaceUpdates &&
    - - {store.getState().visProject.___settings.marketplace?.map(item =>
    - -
    )} -
    } -
    -
    ; + } + className={Utils.clsx('vis-palette-widget-set', opened && 'vis-palette-summary-expanded')} + sx={{ + ...Utils.getStyle( + this.props.theme, + commonStyles.clearPadding, + opened ? styles.groupSummaryExpanded : styles.groupSummary, + styles.lightedPanel, + { minHeight: 0 }, + ), + '&.Mui-expanded': { minHeight: 0 }, + '& .MuiAccordionSummary-content': { + ...commonStyles.clearPadding, + ...(opened ? styles.accordionOpenedSummary : undefined), + }, + // '& .MuiAccordionSummary-expandIcon': commonStyles.clearPadding, + }} + > + + {I18n.t('__marketplace')} + + + {opened && this.state.marketplaceLoading ? : null} + {opened && this.state.marketplaceUpdates && ( +
    + + {store.getState().visProject.___settings.marketplace?.map(item => ( +
    + +
    + ))} +
    + )} +
    + + ); } - async toggleDebugVersion(category: string) { + async toggleDebugVersion(category: string): Promise { const objects = await this.props.socket.getObjectViewSystem( 'instance', 'system.adapter.', @@ -438,8 +458,19 @@ class Palette extends Component { if (this.state.widgetSetProps[category].developerMode) { if (wSetObj) { // find any set with http://localhost:4173/customWidgets.js - if (Object.keys(wSetObj.common.visWidgets).find(key => wSetObj.common.visWidgets[key].url?.startsWith('http'))) { - Object.keys(wSetObj.common.visWidgets).forEach(key => wSetObj.common.visWidgets[key].url = `${wSetObj.common.name}/customWidgets.js`); + if ( + Object.keys(wSetObj.common.visWidgets).find(key => + wSetObj.common.visWidgets[key].url?.startsWith('http'), + ) + ) { + Object.keys(wSetObj.common.visWidgets).forEach(key => { + if (typeof wSetObj.common.name === 'object') { + wSetObj.common.visWidgets[key].url = + `${wSetObj.common.name[this.lang] || wSetObj.common.name.en}/customWidgets.js`; + } else { + wSetObj.common.visWidgets[key].url = `${wSetObj.common.name}/customWidgets.js`; + } + }); await this.props.socket.setObject(wSetObj._id, wSetObj); reload = true; } @@ -453,7 +484,14 @@ class Palette extends Component { // find any set with http://localhost:4173/customWidgets.js if (Object.keys(visWidgets).find(key => visWidgets[key].url?.startsWith('http'))) { // disable the load over http - Object.keys(visWidgets).forEach(key => visWidgets[key].url = `${dynamicWidgetInstances[i].common.name}/customWidgets.js`); + Object.keys(visWidgets).forEach(key => { + const name: ioBroker.StringOrTranslated = dynamicWidgetInstances[i].common.name; + if (typeof name === 'object') { + visWidgets[key].url = `${name[this.lang] || name.en}/customWidgets.js`; + } else { + visWidgets[key].url = `${name}/customWidgets.js`; + } + }); await this.props.socket.setObject(dynamicWidgetInstances[i]._id, dynamicWidgetInstances[i]); reload = true; } @@ -461,10 +499,12 @@ class Palette extends Component { // check if http://localhost:4173/customWidgets.js available try { await fetch('http://localhost:4173/customWidgets.js'); - Object.keys(visWidgets).forEach(key => visWidgets[key].url = 'http://localhost:4173/customWidgets.js'); + Object.keys(visWidgets).forEach( + key => (visWidgets[key].url = 'http://localhost:4173/customWidgets.js'), + ); await this.props.socket.setObject(wSetObj._id, wSetObj); reload = true; - } catch (e) { + } catch { window.alert(`Please start the widget development of ${wSetObj._id.split('.')[2]} first`); } } @@ -473,18 +513,23 @@ class Palette extends Component { reload && setTimeout(() => window.location.reload(), 1000); } - buildListTrigger(immediate?: boolean) { + buildListTrigger(immediate?: boolean): void { if (this.buildWidgetListTimeout) { clearTimeout(this.buildWidgetListTimeout); this.buildWidgetListTimeout = null; } - this.buildWidgetListTimeout = this.buildWidgetListTimeout || setTimeout(() => { - this.buildWidgetListTimeout = null; - this.buildWidgetList(); - }, immediate ? 0 : 100); + this.buildWidgetListTimeout = + this.buildWidgetListTimeout || + setTimeout( + () => { + this.buildWidgetListTimeout = null; + this.buildWidgetList(); + }, + immediate ? 0 : 100, + ); } - render() { + render(): React.JSX.Element | null { if (!this.props.widgetsLoaded) { return null; } @@ -496,155 +541,221 @@ class Palette extends Component { const allOpened = !Object.keys(this.state.widgetsList).find(group => !this.state.accordionOpen[group]); const allClosed = !Object.keys(this.state.widgetsList).find(group => this.state.accordionOpen[group]); - return <> - - - {I18n.t('Palette')} -
    - {!allOpened ? - { - // save the state of marketplace and do not open it if it is not opened - const __marketplace = this.state.accordionOpen.__marketplace; - const accordionOpen: Record = {}; - Object.keys(this.state.widgetsList).forEach(group => accordionOpen[group] = true); - this.state.accordionOpen.__marketplace = __marketplace; - window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); - this.setState({ accordionOpen }); - }} - > - - - : } - {!allClosed ? - { - const accordionOpen: Record = {}; - Object.keys(this.state.widgetsList).forEach(group => accordionOpen[group] = false); - this.state.accordionOpen.__marketplace = false; - window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); - this.setState({ accordionOpen }); - }} - > - - - : } - - this.props.onHide(true)} - > - - - - - this.setState({ filter: e.target.value }, () => this.buildListTrigger())} - placeholder={I18n.t('Search')} - sx={styles.searchLabel} - InputProps={{ - sx: styles.clearPadding, - endAdornment: this.state.filter ? this.setState({ filter: '' }, () => this.buildListTrigger())}> - - : null, - startAdornment: - - , - }} - /> -
    - {this.renderMarketplace()} - {Object.keys(this.state.widgetsList).map((category, categoryKey) => { - let version = null; - if (this.state.widgetSetProps[category]?.version) { - if (DEVELOPER_MODE) { - version =
    + + + {I18n.t('Palette')} +
    + {!allOpened ? ( + + { + // save the state of marketplace and do not open it if it is not opened + const __marketplace = this.state.accordionOpen.__marketplace; + const accordionOpen: Record = {}; + Object.keys(this.state.widgetsList).forEach(group => (accordionOpen[group] = true)); + Object.assign(this.state.accordionOpen, { __marketplace }); + window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); + this.setState({ accordionOpen }); }} - onClick={() => this.toggleDebugVersion(category)} > - {this.state.widgetSetProps[category]?.version} -
    ; - } else { - version =
    {this.state.widgetSetProps[category]?.version}
    ; - } - } - - return { - const accordionOpen = JSON.parse(JSON.stringify(this.state.accordionOpen)); - accordionOpen[category] = expanded; - window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); - this.setState({ accordionOpen }); - }} + + + + ) : ( + + + + )} + {!allClosed ? ( + + { + const accordionOpen: Record = {}; + Object.keys(this.state.widgetsList).forEach( + group => (accordionOpen[group] = false), + ); + Object.assign(this.state.accordionOpen, { __marketplace: false }); + window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); + this.setState({ accordionOpen }); + }} + > + + + + ) : ( + + + + )} + - } - className={Utils.clsx('vis-palette-widget-set', this.state.accordionOpen[category] && 'vis-palette-summary-expanded')} - sx={{ - ...Utils.getStyle( - this.props.theme, - commonStyles.clearPadding, - this.state.accordionOpen[category] ? styles.groupSummaryExpanded : styles.groupSummary, - styles.lightedPanel, - ), - '&.Mui-expanded': { minHeight: 0 }, - '& .MuiAccordionSummary-content': { ...commonStyles.clearPadding, ...(this.state.accordionOpen[category] ? styles.accordionOpenedSummary : undefined) }, - }} + this.props.onHide(true)} > - {this.state.widgetSetProps[category]?.icon ? - - : - null} - {this.state.widgetSetProps[category]?.label ? - (this.state.widgetSetProps[category].label.startsWith('Vis 2 - ') ? - this.state.widgetSetProps[category].label.substring(8) : this.state.widgetSetProps[category].label) - : - I18n.t(category)} - - - {version} -
    - {this.state.accordionOpen[category] ? this.state.widgetsList[category].map(widgetItem => - (widgetItem.name === '_tplGroup' ? null : )) : null} -
    -
    -
    ; - })} -
    - ; + + + + + this.setState({ filter: e.target.value }, () => this.buildListTrigger())} + placeholder={I18n.t('Search')} + sx={styles.searchLabel} + InputProps={{ + sx: styles.clearPadding, + endAdornment: this.state.filter ? ( + this.setState({ filter: '' }, () => this.buildListTrigger())} + > + + + ) : null, + startAdornment: ( + + + + ), + }} + /> +
    + {this.renderMarketplace()} + {Object.keys(this.state.widgetsList).map((category, categoryKey) => { + let version = null; + if (this.state.widgetSetProps[category]?.version) { + if (DEVELOPER_MODE) { + version = ( +
    this.toggleDebugVersion(category)} + > + {this.state.widgetSetProps[category]?.version} +
    + ); + } else { + version = ( +
    {this.state.widgetSetProps[category]?.version}
    + ); + } + } + + return ( + { + const accordionOpen = JSON.parse(JSON.stringify(this.state.accordionOpen)); + accordionOpen[category] = expanded; + window.localStorage.setItem('widgets', JSON.stringify(accordionOpen)); + this.setState({ accordionOpen }); + }} + > + } + className={Utils.clsx( + 'vis-palette-widget-set', + this.state.accordionOpen[category] && 'vis-palette-summary-expanded', + )} + sx={{ + ...Utils.getStyle( + this.props.theme, + commonStyles.clearPadding, + this.state.accordionOpen[category] + ? styles.groupSummaryExpanded + : styles.groupSummary, + styles.lightedPanel, + ), + '&.Mui-expanded': { minHeight: 0 }, + '& .MuiAccordionSummary-content': { + ...commonStyles.clearPadding, + ...(this.state.accordionOpen[category] + ? styles.accordionOpenedSummary + : undefined), + }, + }} + > + {this.state.widgetSetProps[category]?.icon ? ( + + ) : null} + {this.state.widgetSetProps[category]?.label + ? this.state.widgetSetProps[category].label.startsWith('Vis 2 - ') + ? this.state.widgetSetProps[category].label.substring(8) + : this.state.widgetSetProps[category].label + : I18n.t(category)} + + + {version} +
    + {this.state.accordionOpen[category] + ? this.state.widgetsList[category].map(widgetItem => + widgetItem.name === '_tplGroup' ? null : ( + + ), + ) + : null} +
    +
    +
    + ); + })} +
    + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Runtime.tsx b/packages/iobroker.vis-2/src/src/Runtime.tsx index 8e24ab471..5d8398ff1 100644 --- a/packages/iobroker.vis-2/src/src/Runtime.tsx +++ b/packages/iobroker.vis-2/src/src/Runtime.tsx @@ -17,16 +17,16 @@ import { ListItemIcon, } from '@mui/material'; -import { - Add as IconAdd, - Close as IconClose, - FileCopy as IconDocument, -} from '@mui/icons-material'; +import { Add as IconAdd, Close as IconClose, FileCopy as IconDocument } from '@mui/icons-material'; import { BiImport } from 'react-icons/bi'; import { - I18n, Loader, LegacyConnection, - LoaderMV, LoaderPT, LoaderVendor, + I18n, + Loader, + LegacyConnection, + LoaderMV, + LoaderPT, + LoaderVendor, GenericApp, type GenericAppProps, type GenericAppState, @@ -35,17 +35,17 @@ import { } from '@iobroker/adapter-react-v5'; import type { - AnyWidgetId, GroupWidgetId, - Project, SingleWidgetId, - ViewSettings, VisTheme, - WidgetData, WidgetStyle, + AnyWidgetId, + GroupWidgetId, + Project, + SingleWidgetId, + ViewSettings, + VisTheme, + WidgetData, + WidgetStyle, } from '@iobroker/types-vis-2'; import VisEngine from './Vis/visEngine'; -import { - extractBinding, - findWidgetUsages, - readFile, -} from './Vis/visUtils'; +import { extractBinding, findWidgetUsages, readFile } from './Vis/visUtils'; import { registerWidgetsLoadIndicator } from './Vis/visLoadWidgets'; import VisWidgetsCatalog from './Vis/visWidgetsCatalog'; @@ -163,16 +163,24 @@ class Runtime

    void), cb?: () => void) => Promise; + setSelectedWidgets: ( + selectedWidgets: AnyWidgetId[], + selectedView?: string | (() => void), + cb?: () => void, + ) => Promise; setSelectedGroup: (selectedGroup: GroupWidgetId) => void; - onWidgetsChanged: (changedData: { - wid: AnyWidgetId; - view: string; - style: WidgetStyle; - data: WidgetData; - }[], view: string, viewSettings: ViewSettings) => void; + onWidgetsChanged: ( + changedData: { + wid: AnyWidgetId; + view: string; + style: WidgetStyle; + data: WidgetData; + }[], + view: string, + viewSettings: ViewSettings, + ) => void; onFontsUpdate?(fonts: string[]): void; @@ -189,21 +197,17 @@ class Runtime

    void; - }): void; + }): void; - showCodeDialog?(dialog: { - code: string; - title: string; - mode: string; - }): void; + showCodeDialog?(dialog: { code: string; title: string; mode: string }): void; showLegacyFileSelector: ( - callback: (data: { path: string; file: string}, userArg: any) => void, + callback: (data: { path: string; file: string }, userArg: any) => void, options: { path?: string; userArg?: any; - } - ) => void; + }, + ) => void; initState: (newState: Partial) => void; @@ -276,8 +280,7 @@ class Runtime

    ): Promise { return new Promise(resolve => { - this.setState(newState as RuntimeState, () => - resolve()); + this.setState(newState as RuntimeState, () => resolve()); }); } @@ -339,8 +342,7 @@ class Runtime

    { const currentPath = VisEngine.getCurrentPath(); - this.changeView(currentPath.view) - .catch(e => console.error(`Cannot change view: ${e}`)); + this.changeView(currentPath.view).catch(e => console.error(`Cannot change view: ${e}`)); }; onProjectChange = (id: string, fileName: string) => { @@ -357,24 +359,23 @@ class Runtime

    { this.checkTimeout = null; // compare last executed file with new one - readFile(this.socket as unknown as LegacyConnection, this.adapterId, fileName) - .then(file => { - try { - const ts = (JSON.parse((file as any).file || file) as Project).___settings.ts; - if (ts === store.getState().visProject.___settings.ts) { - return; - } - const tsInt = parseInt(ts.split('.')[0], 10); - if (tsInt < parseInt(store.getState().visProject.___settings.ts.split('.')[0], 10)) { - // ignore older files - return; - } - } catch (e) { - console.warn(`Cannot parse project file "${fileName}": ${e}`); + readFile(this.socket as unknown as LegacyConnection, this.adapterId, fileName).then(file => { + try { + const ts = (JSON.parse((file as any).file || file) as Project).___settings.ts; + if (ts === store.getState().visProject.___settings.ts) { + return; } + const tsInt = parseInt(ts.split('.')[0], 10); + if (tsInt < parseInt(store.getState().visProject.___settings.ts.split('.')[0], 10)) { + // ignore older files + return; + } + } catch (e) { + console.warn(`Cannot parse project file "${fileName}": ${e}`); + } - this.setState({ showProjectUpdateDialog: true }); - }); + this.setState({ showProjectUpdateDialog: true }); + }); }, 500); } } @@ -428,7 +429,8 @@ class Runtime

    { - if (attr === 'bindings' || + if ( + attr === 'bindings' || !widget.data[attr] || attr.startsWith('g_') || typeof widget.data[attr] !== 'string' @@ -446,7 +448,8 @@ class Runtime

    { - if (attr === 'bindings' || + if ( + attr === 'bindings' || !widget.style[attr] || attr.startsWith('g_') || typeof widget.data[attr] !== 'string' @@ -463,20 +466,24 @@ class Runtime

    - widget.data.members[i] = (_wid.replace(/\s/g, '_')) as AnyWidgetId); + widget.data.members.forEach( + (_wid, i) => (widget.data.members[i] = _wid.replace(/\s/g, '_') as AnyWidgetId), + ); } if (wid.includes(' ')) { - const newWid: SingleWidgetId = (wid.replace(/\s/g, '_') as SingleWidgetId); + const newWid: SingleWidgetId = wid.replace(/\s/g, '_') as SingleWidgetId; delete project[view].widgets[wid]; project[view].widgets[newWid] = widget; } if (!this.state.runtime && this.getNewWidgetId) { // If the widget is not unique, change its name (only in editor mode) - if (Object.keys(project).find(v => v !== view && project[v].widgets && project[v].widgets[wid])) { - const _newWid: AnyWidgetId = wid[0] === 'g' ? this.getNewGroupId(project) : this.getNewWidgetId(project); + if ( + Object.keys(project).find(v => v !== view && project[v].widgets && project[v].widgets[wid]) + ) { + const _newWid: AnyWidgetId = + wid[0] === 'g' ? this.getNewGroupId(project) : this.getNewWidgetId(project); console.log(`Rename widget ${wid} to ${_newWid}`); delete project[view].widgets[wid]; project[view].widgets[_newWid as SingleWidgetId] = widget; @@ -519,14 +526,18 @@ class Runtime

    { if (viewId !== view && project[viewId]) { // copy all widgets, that must be shown in this view too - project[viewId].widgets[`${view}_${widgetId}` as AnyWidgetId] = JSON.parse(JSON.stringify(oWidget)); + project[viewId].widgets[`${view}_${widgetId}` as AnyWidgetId] = JSON.parse( + JSON.stringify(oWidget), + ); delete project[viewId].widgets[`${view}_${widgetId}` as AnyWidgetId].data['multi-views']; if (oWidget.tpl === '_tplGroup' && oWidget.data.members?.length) { // copy all group widgets too const newWidget = project[viewId].widgets[`${view}_${widgetId}` as AnyWidgetId]; newWidget.data.members.forEach((memberId, i) => { const newId: AnyWidgetId = `${view}_${memberId}` as AnyWidgetId; - project[viewId].widgets[newId] = JSON.parse(JSON.stringify(oView.widgets[memberId])); + project[viewId].widgets[newId] = JSON.parse( + JSON.stringify(oView.widgets[memberId]), + ); delete project[viewId].widgets[newId].data['multi-views']; // do not allow multi-multi-views newWidget.data.members[i] = newId; // do not copy members of multi-group @@ -558,17 +569,19 @@ class Runtime

    { - if (view === '___settings') { - return; - } - if (project[view].settings?.useAsDefault - // If difference less than 20% - && Math.abs(project[view].settings.sizex - w) / project[view].settings.sizex < 0.2 - ) { - views.push(view); - } - }); + project && + Object.keys(project).forEach(view => { + if (view === '___settings') { + return; + } + if ( + project[view].settings?.useAsDefault && + // If difference less than 20% + Math.abs(project[view].settings.sizex - w) / project[view].settings.sizex < 0.2 + ) { + views.push(view); + } + }); for (let i = 0; i < views.length; i++) { if (Math.abs(project[views[i]].settings.sizey - h) < difference) { @@ -582,22 +595,34 @@ class Runtime

    { - if (view === '___settings') { - return; - } - if (project[view].settings?.useAsDefault && - // If difference less than 20% - parseInt(project[view].settings.sizey.toString(), 10) && - Math.abs(ratio - (parseInt(project[view].settings.sizex.toString(), 10) / parseInt(project[view].settings.sizey.toString(), 10))) < difference - ) { - result = view; - difference = Math.abs(ratio - (parseInt(project[view].settings.sizex.toString(), 10) / parseInt(project[view].settings.sizey.toString(), 10))); - } - }); + project && + Object.keys(project).forEach(view => { + if (view === '___settings') { + return; + } + if ( + project[view].settings?.useAsDefault && + // If difference less than 20% + parseInt(project[view].settings.sizey.toString(), 10) && + Math.abs( + ratio - + parseInt(project[view].settings.sizex.toString(), 10) / + parseInt(project[view].settings.sizey.toString(), 10), + ) < difference + ) { + result = view; + difference = Math.abs( + ratio - + parseInt(project[view].settings.sizex.toString(), 10) / + parseInt(project[view].settings.sizey.toString(), 10), + ); + } + }); } if (!result && resultRequired) { - result = project && Object.keys(project).find(view => !view.startsWith('__') && project[view].settings?.useAsDefault); + result = + project && + Object.keys(project).find(view => !view.startsWith('__') && project[view].settings?.useAsDefault); } if (!result && resultRequired) { return (project && Object.keys(project).find(view => !view.startsWith('__'))) || ''; @@ -610,7 +635,11 @@ class Runtime

    - window.location.reload(), 2000); + setTimeout(() => window.location.reload(), 2000); } }, }, @@ -787,8 +822,7 @@ class Runtime

    - window.location.reload(), 2000); + setTimeout(() => window.location.reload(), 2000); } } @@ -802,7 +836,11 @@ class Runtime

    = {}; - groups.forEach(group => userGroups[group._id] = group); + groups.forEach(group => (userGroups[group._id] = group)); await this.setStateAsync({ currentUser, @@ -856,7 +894,9 @@ class Runtime

    = 0; i--) { - if (!store.getState().visProject[selectedView] || !store.getState().visProject[selectedView].widgets || !store.getState().visProject[selectedView].widgets[selectedWidgets[i] as AnyWidgetId]) { + if ( + !store.getState().visProject[selectedView] || + !store.getState().visProject[selectedView].widgets || + !store.getState().visProject[selectedView].widgets[selectedWidgets[i] as AnyWidgetId] + ) { selectedWidgets = selectedWidgets.splice(i, 1); } } @@ -917,9 +960,9 @@ class Runtime

    this.setState({ alert: false })} - onClose={(e, reason) => { - if (reason === 'clickaway') { - return; + renderAlertDialog = () => ( + ; + open={this.state.alert} + autoHideDuration={6000} + onClick={() => this.setState({ alert: false })} + onClose={(e, reason) => { + if (reason === 'clickaway') { + return; + } + this.setState({ alert: false }); + }} + message={this.state.alertMessage} + /> + ); async onWidgetsLoaded() { let widgetsLoaded = Runtime.WIDGETS_LOADING_STEP_HTML_LOADED; if (this.socket.isConnected()) { - await VisWidgetsCatalog.collectRxInformation(this.socket as unknown as LegacyConnection, store.getState().visProject, this.changeProject); + await VisWidgetsCatalog.collectRxInformation( + this.socket as unknown as LegacyConnection, + store.getState().visProject, + this.changeProject, + ); widgetsLoaded = Runtime.WIDGETS_LOADING_STEP_ALL_LOADED; } this.setState({ widgetsLoaded }); @@ -984,7 +1036,7 @@ class Runtime

    { try { - const project: Project = ({ + const project: Project = { ___settings: { folders: [], openedViews: [], @@ -997,7 +1049,7 @@ class Runtime

    this.setState({ showNewProjectDialog: false })} - > - {I18n.t('Create new project')} - - { - if (e.key === 'Enter' && this.state.newProjectName && !this.state.projects.includes(this.state.newProjectName)) { - await this.addProject(this.state.newProjectName); + return ( +

    this.setState({ showNewProjectDialog: false })} + > + {I18n.t('Create new project')} + + { + if ( + e.key === 'Enter' && + this.state.newProjectName && + !this.state.projects.includes(this.state.newProjectName) + ) { + await this.addProject(this.state.newProjectName); + window.location.href = `edit.html?${this.state.newProjectName}`; + } + }} + value={this.state.newProjectName} + onChange={e => this.setState({ newProjectName: e.target.value })} + margin="dense" + /> + + + - - - ; + }} + startIcon={} + > + {I18n.t('Create')} + + + + + ); } showSmallProjectsDialog() { - return { - /* do nothing */ - }} - > - - vis-2 - {!this.state.projects.length ? I18n.t('Create or import new "vis-2" project') : I18n.t('Select vis-2 project')} - - - {!this.state.projects ? : - {!this.state.projects.length ?
    - {I18n.t('welcome_message')} -
    : null} - - {this.state.projects.map(project => - window.location.href = `?${project}`} - > - - - - {project} - )} - this.setState({ showNewProjectDialog: true, newProjectName: this.state.projects.length ? '' : 'main' })} - style={{ backgroundColor: '#112233', color: '#ffffff' }} - > - - - - {I18n.t('Create new project')} - - {this.renderImportProjectDialog ? this.setState({ showImportDialog: true })} - style={{ backgroundColor: '#112233', color: '#4b9ed3' }} - > - - {I18n.t('Import project')} - : null} - -
    } -
    - {this.showCreateNewProjectDialog()} - {this.renderImportProjectDialog ? this.renderImportProjectDialog() : null} - {this.renderAlertDialog()} -
    ; + return ( + { + /* do nothing */ + }} + > + + vis-2 + {!this.state.projects.length + ? I18n.t('Create or import new "vis-2" project') + : I18n.t('Select vis-2 project')} + + + {!this.state.projects ? ( + + ) : ( + + {!this.state.projects.length ? ( +
    + {I18n.t('welcome_message')} +
    + ) : null} + + {this.state.projects.map(project => ( + (window.location.href = `?${project}`)} + > + + + + {project} + + ))} + + this.setState({ + showNewProjectDialog: true, + newProjectName: this.state.projects.length ? '' : 'main', + }) + } + style={{ backgroundColor: '#112233', color: '#ffffff' }} + > + + + + {I18n.t('Create new project')} + + {this.renderImportProjectDialog ? ( + this.setState({ showImportDialog: true })} + style={{ backgroundColor: '#112233', color: '#4b9ed3' }} + > + + + + {I18n.t('Import project')} + + ) : null} + +
    + )} +
    + {this.showCreateNewProjectDialog()} + {this.renderImportProjectDialog ? this.renderImportProjectDialog() : null} + {this.renderAlertDialog()} +
    + ); } renderProjectDoesNotExist() { - return
    - {I18n.t('Project "%s" does not exist', this.state.projectName)} -
    ; + return ( +
    + {I18n.t('Project "%s" does not exist', this.state.projectName)} +
    + ); } getVisEngine() { @@ -1166,7 +1246,9 @@ class Runtime

    this.onWidgetsLoaded()} - selectedGroup={this.state.selectedGroup} - setSelectedGroup={this.setSelectedGroup} - onWidgetsChanged={this.onWidgetsChanged} - projectName={this.state.projectName} - lockDragging={this.state.lockDragging} - disableInteraction={this.state.disableInteraction} - widgetHint={this.state.widgetHint} - onFontsUpdate={this.state.runtime ? null : (fonts: string[]) => this.onFontsUpdate(fonts)} - registerEditorCallback={this.state.runtime ? null : this.registerCallback} - themeType={this.state.themeType} - themeName={this.state.themeName} - theme={this.state.theme as VisTheme} - adapterId={this.adapterId} - editModeComponentStyle={styles.editModeComponentStyle} - onIgnoreMouseEvents={this.onIgnoreMouseEvents} - setLoadingText={this.setLoadingText} - onConfirmDialog={(message: string, title: string, icon: string, width: number, callback: (isYes: boolean) => void) => this.showConfirmDialog && this.showConfirmDialog({ - message, - title, - icon, - width, - callback, - })} - onShowCode={(code: string, title: string, mode: string) => this.showCodeDialog && this.showCodeDialog({ code, title, mode })} - currentUser={this.state.currentUser} - userGroups={this.state.userGroups} - renderAlertDialog={this.renderAlertDialog} - showLegacyFileSelector={this.showLegacyFileSelector} - toggleTheme={(newThemeName: ThemeName) => this.toggleTheme(newThemeName)} - askAboutInclude={this.askAboutInclude} - changeProject={this.changeProject} - />; + return ( + this.onWidgetsLoaded()} + selectedGroup={this.state.selectedGroup} + setSelectedGroup={this.setSelectedGroup} + onWidgetsChanged={this.onWidgetsChanged} + projectName={this.state.projectName} + lockDragging={this.state.lockDragging} + disableInteraction={this.state.disableInteraction} + widgetHint={this.state.widgetHint} + onFontsUpdate={this.state.runtime ? null : (fonts: string[]) => this.onFontsUpdate(fonts)} + registerEditorCallback={this.state.runtime ? null : this.registerCallback} + themeType={this.state.themeType} + themeName={this.state.themeName} + theme={this.state.theme as VisTheme} + adapterId={this.adapterId} + editModeComponentStyle={styles.editModeComponentStyle} + onIgnoreMouseEvents={this.onIgnoreMouseEvents} + setLoadingText={this.setLoadingText} + onConfirmDialog={( + message: string, + title: string, + icon: string, + width: number, + callback: (isYes: boolean) => void, + ) => + this.showConfirmDialog && + this.showConfirmDialog({ + message, + title, + icon, + width, + callback, + }) + } + onShowCode={(code: string, title: string, mode: string) => + this.showCodeDialog && this.showCodeDialog({ code, title, mode }) + } + currentUser={this.state.currentUser} + userGroups={this.state.userGroups} + renderAlertDialog={this.renderAlertDialog} + showLegacyFileSelector={this.showLegacyFileSelector} + toggleTheme={(newThemeName: ThemeName) => this.toggleTheme(newThemeName)} + askAboutInclude={this.askAboutInclude} + changeProject={this.changeProject} + /> + ); } renderLoader() { @@ -1231,26 +1326,53 @@ class Runtime

    ; + return ( + + ); } if (window.vendorPrefix === 'PT') { - return ; + return ( + + ); } if (window.vendorPrefix && window.vendorPrefix !== '@@vendorPrefix@@') { - return ; + return ( + + ); } - return ; + return ( + + ); } render() { - return - - {!this.state.loaded || !store.getState().visProject.___settings ? - this.renderLoader() : this.getVisEngine()} - {this.state.projectDoesNotExist ? this.renderProjectDoesNotExist() : null} - {this.state.showProjectsDialog ? this.showSmallProjectsDialog() : null} - - ; + return ( + + + {!this.state.loaded || !store.getState().visProject.___settings + ? this.renderLoader() + : this.getVisEngine()} + {this.state.projectDoesNotExist ? this.renderProjectDoesNotExist() : null} + {this.state.showProjectsDialog ? this.showSmallProjectsDialog() : null} + + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Store.tsx b/packages/iobroker.vis-2/src/src/Store.tsx index b7bae1d5f..8aa672504 100644 --- a/packages/iobroker.vis-2/src/src/Store.tsx +++ b/packages/iobroker.vis-2/src/src/Store.tsx @@ -1,9 +1,12 @@ -import { - createReducer, configureStore, createAction, createSelector, -} from '@reduxjs/toolkit'; +import { createReducer, configureStore, createAction, createSelector } from '@reduxjs/toolkit'; import type { - View, Project, AnyWidgetId, SingleWidgetId, - SingleWidget, GroupWidget, GroupWidgetId, + View, + Project, + AnyWidgetId, + SingleWidgetId, + SingleWidget, + GroupWidget, + GroupWidgetId, } from '@iobroker/types-vis-2'; /** This id is used by some special widgets to work with non-existing widgets */ @@ -11,8 +14,16 @@ const FAKE_ID = 'fakeId'; export const updateProject = createAction('project/update'); export const updateView = createAction<{ viewId: string; data: View }>('view/update'); -export const updateWidget = createAction<{ viewId: string; widgetId: GroupWidgetId | SingleWidgetId | typeof FAKE_ID; data: SingleWidget }>('widget/update'); -export const updateGroupWidget = createAction<{ viewId: string; widgetId: GroupWidgetId | typeof FAKE_ID; data: GroupWidget }>('group/update'); +export const updateWidget = createAction<{ + viewId: string; + widgetId: GroupWidgetId | SingleWidgetId | typeof FAKE_ID; + data: SingleWidget; +}>('widget/update'); +export const updateGroupWidget = createAction<{ + viewId: string; + widgetId: GroupWidgetId | typeof FAKE_ID; + data: GroupWidget; +}>('group/update'); export const updateActiveUser = createAction('activeUser/update'); export const recalculateFields = createAction('attributes/recalculate'); @@ -24,68 +35,69 @@ const initialState = { activeUser: '', }; -const reducer = createReducer( - initialState, - builder => { - builder - .addCase(updateProject, (state, action) => { - state.visProject = action.payload as Project; - }) - .addCase(updateView, (state, action) => { - const { viewId, data } = action.payload; - state.visProject[viewId] = data; - }) - .addCase(updateWidget, (state, action) => { - const { viewId, widgetId, data } = action.payload; - if (widgetId === FAKE_ID) { - // Ignore it - return; - } +const reducer = createReducer(initialState, builder => { + builder + .addCase(updateProject, (state, action) => { + state.visProject = action.payload; + }) + .addCase(updateView, (state, action) => { + const { viewId, data } = action.payload; + state.visProject[viewId] = data; + }) + .addCase(updateWidget, (state, action) => { + const { viewId, widgetId, data } = action.payload; + if (widgetId === FAKE_ID) { + // Ignore it + return; + } - if (!(viewId in state.visProject)) { - console.error(`Cannot update widget "${widgetId}". The view "${viewId}" does not exist in the project.`); - return; - } + if (!(viewId in state.visProject)) { + console.error( + `Cannot update widget "${widgetId}". The view "${viewId}" does not exist in the project.`, + ); + return; + } - state.visProject[viewId].widgets[widgetId as SingleWidgetId] = data; - }) - .addCase(updateGroupWidget, (state, action) => { - const { viewId, widgetId, data } = action.payload; + state.visProject[viewId].widgets[widgetId as SingleWidgetId] = data; + }) + .addCase(updateGroupWidget, (state, action) => { + const { viewId, widgetId, data } = action.payload; - if (widgetId === FAKE_ID) { - // Ignore it - return; - } + if (widgetId === FAKE_ID) { + // Ignore it + return; + } - if (!(viewId in state.visProject)) { - console.error(`Cannot update group widget "${widgetId}". The view "${viewId}" does not exist in the project.`); - return; - } + if (!(viewId in state.visProject)) { + console.error( + `Cannot update group widget "${widgetId}". The view "${viewId}" does not exist in the project.`, + ); + return; + } - state.visProject[viewId].widgets[widgetId] = data; - }) - .addCase(updateActiveUser, (state, action) => { - state.activeUser = action.payload; - }) - .addCase(recalculateFields, (state, action) => { - state.recalculateFields = action.payload; - }); - }, -); + state.visProject[viewId].widgets[widgetId] = data; + }) + .addCase(updateActiveUser, (state, action) => { + state.activeUser = action.payload; + }) + .addCase(recalculateFields, (state, action) => { + state.recalculateFields = action.payload; + }); +}); -type StoreState = typeof initialState +type StoreState = typeof initialState; -export const selectProject = (state: StoreState) => state.visProject; +export const selectProject = (state: StoreState): Project => state.visProject; -export const selectView = createSelector([ - selectProject, - (_state: StoreState, viewName: string) => viewName, -], (project, view) => project[view]); +export const selectView = createSelector( + [selectProject, (_state: StoreState, viewName: string) => viewName], + (project, view) => project[view], +); -export const selectWidget = createSelector([ - selectView, - (_state: StoreState, _viewName: string, wid: AnyWidgetId) => wid, -], (view, wid) => view.widgets[wid]); +export const selectWidget = createSelector( + [selectView, (_state: StoreState, _viewName: string, wid: AnyWidgetId) => wid], + (view, wid) => view.widgets[wid], +); export const store = configureStore({ reducer, diff --git a/packages/iobroker.vis-2/src/src/Toolbar/MultiSelect.tsx b/packages/iobroker.vis-2/src/src/Toolbar/MultiSelect.tsx index 83ce74c0d..1c8a88c53 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/MultiSelect.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/MultiSelect.tsx @@ -1,15 +1,19 @@ import React, { Component } from 'react'; import { - Checkbox, FormControl, MenuItem, - Menu, List, - ListItemButton, ListItemText, - ListItemIcon, InputLabel, Button, Box, + Checkbox, + FormControl, + MenuItem, + Menu, + List, + ListItemButton, + ListItemText, + ListItemIcon, + InputLabel, + Button, + Box, } from '@mui/material'; -import { - ArrowDropDown as IconArrowDown, - ArrowDropUp as IconArrowUp, -} from '@mui/icons-material'; +import { ArrowDropDown as IconArrowDown, ArrowDropUp as IconArrowUp } from '@mui/icons-material'; import { Utils, I18n, type ThemeType } from '@iobroker/adapter-react-v5'; import type { VisTheme } from '@iobroker/types-vis-2'; @@ -97,7 +101,7 @@ class MultiSelect extends Component { }; } - render() { + render(): React.JSX.Element { const props = this.props; const value = props.value || []; @@ -111,11 +115,13 @@ class MultiSelect extends Component { text = item.name; subText = item.subName; color = item.color; - icon = item.icon ? {item.name} : null; + icon = item.icon ? ( + {item.name} + ) : null; } else { text = value[0]; } @@ -135,112 +141,132 @@ class MultiSelect extends Component { const isNarrow = !props.label; - return - {props.label ? {props.label} : null} - - this.setState({ elAnchor: e.currentTarget })} - style={styles.listItemButton} + {props.label ? {props.label} : null} + - {icon ? - {icon} - : null} - - {subText} - } - /> - {this.state.elAnchor ? : } - - -

    this.setState({ elAnchor: null })} - > - - {I18n.t('All')} - - - - {props.options.map(item => - this.setState({ elAnchor: null }, () => - props.onChange([item.value]))} + {icon ? {icon} : null} + + {subText} + + } + /> + + {this.state.elAnchor ? : } + + + + this.setState({ elAnchor: null })} > -
    -
    - { - e.stopPropagation(); - e.preventDefault(); - const _value = [...value]; - if (_value.includes(item.value)) { - _value.splice(_value.indexOf(item.value), 1); - } else { - _value.push(item.value); - } - props.onChange(_value); - }} - /> -
    -
    - {item.icon ?
    - {item.name} -
    : null} -
    {item.name}
    -
    - {item.subName} + + {I18n.t('All')} + + + + {props.options.map(item => ( + this.setState({ elAnchor: null }, () => props.onChange([item.value]))} + > +
    +
    + { + e.stopPropagation(); + e.preventDefault(); + const _value = [...value]; + if (_value.includes(item.value)) { + _value.splice(_value.indexOf(item.value), 1); + } else { + _value.push(item.value); + } + props.onChange(_value); + }} + /> +
    +
    + {item.icon ? ( +
    + {item.name} +
    + ) : null} +
    {item.name}
    +
    + {item.subName} +
    +
    -
    -
    - )} -
    - ; +
    + ))} +
    + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Toolbar/Projects.tsx b/packages/iobroker.vis-2/src/src/Toolbar/Projects.tsx index d650067a2..11a6c8ca4 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/Projects.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/Projects.tsx @@ -12,13 +12,14 @@ import { I18n, SelectFile as SelectFileDialog, Utils, - type Connection, type ThemeType, type LegacyConnection, + type Connection, + type ThemeType, + type LegacyConnection, } from '@iobroker/adapter-react-v5'; import type { VisTheme } from '@iobroker/types-vis-2'; import type Editor from '@/Editor'; -import {ToolbarGroup, ToolbarItem} from './ToolbarItems'; -import ToolbarItems from './ToolbarItems'; +import ToolbarItems, { type ToolbarGroup } from './ToolbarItems'; import Settings from './Settings'; import ProjectsManager from './ProjectsManager'; @@ -43,7 +44,7 @@ interface ToolsProps { toolbarHeight: 'full' | 'narrow' | 'veryNarrow'; } -const Tools = (props: ToolsProps) => { +const Tools = (props: ToolsProps): React.JSX.Element => { const [settingsDialog, setSettingsDialog] = useState(false); const [objectsDialog, setObjectsDialog] = useState(false); const [filesDialog, setFilesDialog] = useState(false); @@ -53,113 +54,131 @@ const Tools = (props: ToolsProps) => { compact: window.innerWidth < 1230 ? { tooltip: 'Projects', icon: } : undefined, items: [ { - type: 'icon-button', Icon: SettingsIcon, name: 'Settings', onAction: () => setSettingsDialog(true), + type: 'icon-button', + Icon: SettingsIcon, + name: 'Settings', + onAction: () => setSettingsDialog(true), }, { - type: 'icon-button', Icon: MenuIcon, name: 'Manage projects', onAction: () => props.setProjectsDialog(true), + type: 'icon-button', + Icon: MenuIcon, + name: 'Manage projects', + onAction: () => props.setProjectsDialog(true), }, { - type: 'icon-button', Icon: ListIcon, name: 'Objects', onAction: () => setObjectsDialog(true), + type: 'icon-button', + Icon: ListIcon, + name: 'Objects', + onAction: () => setObjectsDialog(true), }, { - type: 'icon-button', Icon: FilesIcon, name: 'Files', onAction: () => setFilesDialog(true), + type: 'icon-button', + Icon: FilesIcon, + name: 'Files', + onAction: () => setFilesDialog(true), }, ], }; - return <> - - {settingsDialog ? setSettingsDialog(false)} - adapterName={props.adapterName} - instance={props.instance} - projectName={props.projectName} - changeProject={props.changeProject} - socket={props.socket} - /> : null} - {props.projectsDialog ? props.setProjectsDialog(false)} - adapterName={props.adapterName} - instance={props.instance} - addProject={props.addProject} - changeProject={props.changeProject} - deleteProject={props.deleteProject} - loadProject={props.loadProject} - projectName={props.projectName} - projects={props.projects} - refreshProjects={props.refreshProjects} - renameProject={props.renameProject} - selectedView={props.selectedView} - socket={props.socket} - themeType={props.themeType} - theme={props.theme} - /> : null} - { - objectsDialog ? setObjectsDialog(false)} - socket={props.socket as any as Connection} - title={I18n.t('Browse objects')} - columns={['role', 'func', 'val', 'name']} - notEditable={false} + return ( + <> + { - const selected: string = Array.isArray(_selected) ? _selected[0] : _selected as string; - Utils.copyToClipboard(selected); - setObjectsDialog(false); - window.alert(I18n.t('Copied')); - }} - ok={I18n.t('Copy to clipboard')} - cancel={I18n.t('ra_Close')} - /> : null - } - { - filesDialog ? setFilesDialog(false)} - restrictToFolder={`${props.adapterName}.${props.instance}/${props.projectName}`} - allowNonRestricted - allowUpload - allowDownload - allowCreateFolder - allowDelete - allowView - showToolbar - theme={props.theme} - imagePrefix="../" - selected="" - showTypeSelector - onOk={_selected => { - let selected: string = Array.isArray(_selected) ? _selected[0] : _selected as string; + selectedView={props.selectedView} + setSelectedWidgets={props.setSelectedWidgets} + themeType={props.themeType} + toolbarHeight={props.toolbarHeight} + /> + {settingsDialog ? ( + setSettingsDialog(false)} + adapterName={props.adapterName} + instance={props.instance} + projectName={props.projectName} + changeProject={props.changeProject} + socket={props.socket} + /> + ) : null} + {props.projectsDialog ? ( + props.setProjectsDialog(false)} + adapterName={props.adapterName} + instance={props.instance} + addProject={props.addProject} + changeProject={props.changeProject} + deleteProject={props.deleteProject} + loadProject={props.loadProject} + projectName={props.projectName} + projects={props.projects} + refreshProjects={props.refreshProjects} + renameProject={props.renameProject} + selectedView={props.selectedView} + socket={props.socket} + themeType={props.themeType} + theme={props.theme} + /> + ) : null} + {objectsDialog ? ( + setObjectsDialog(false)} + socket={props.socket as any as Connection} + title={I18n.t('Browse objects')} + columns={['role', 'func', 'val', 'name']} + notEditable={false} + theme={props.theme} + onOk={_selected => { + const selected: string = Array.isArray(_selected) ? _selected[0] : _selected; + Utils.copyToClipboard(selected); + setObjectsDialog(false); + window.alert(I18n.t('Copied')); + }} + ok={I18n.t('Copy to clipboard')} + cancel={I18n.t('ra_Close')} + /> + ) : null} + {filesDialog ? ( + setFilesDialog(false)} + restrictToFolder={`${props.adapterName}.${props.instance}/${props.projectName}`} + allowNonRestricted + allowUpload + allowDownload + allowCreateFolder + allowDelete + allowView + showToolbar + theme={props.theme} + imagePrefix="../" + selected="" + showTypeSelector + onOk={_selected => { + let selected: string = Array.isArray(_selected) ? _selected[0] : _selected; - const projectPrefix = `${props.adapterName}.${props.instance}/${props.projectName}/`; - if (selected.startsWith(projectPrefix)) { - selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; - } else if (selected.startsWith('/')) { - selected = `..${selected}`; - } else if (!selected.startsWith('.')) { - selected = `../${selected}`; - } - Utils.copyToClipboard(selected); - setFilesDialog(false); - window.alert(I18n.t('ra_Copied %s', selected)); - }} - socket={props.socket as any as Connection} - ok={I18n.t('Copy to clipboard')} - cancel={I18n.t('ra_Close')} - /> : null - } - ; + const projectPrefix = `${props.adapterName}.${props.instance}/${props.projectName}/`; + if (selected.startsWith(projectPrefix)) { + selected = `_PRJ_NAME/${selected.substring(projectPrefix.length)}`; + } else if (selected.startsWith('/')) { + selected = `..${selected}`; + } else if (!selected.startsWith('.')) { + selected = `../${selected}`; + } + Utils.copyToClipboard(selected); + setFilesDialog(false); + window.alert(I18n.t('ra_Copied %s', selected)); + }} + socket={props.socket as any as Connection} + ok={I18n.t('Copy to clipboard')} + cancel={I18n.t('ra_Close')} + /> + ) : null} + + ); }; export default Tools; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ImportProjectDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ImportProjectDialog.tsx index 1732e0a9c..4a476fe34 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ImportProjectDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ImportProjectDialog.tsx @@ -1,20 +1,15 @@ import React, { useState } from 'react'; -import { - TextField, -} from '@mui/material'; +import { TextField } from '@mui/material'; import { BiImport } from 'react-icons/bi'; -import { - I18n, Confirm as ConfirmDialog, - type ThemeType, type LegacyConnection, -} from '@iobroker/adapter-react-v5'; +import { I18n, Confirm as ConfirmDialog, type ThemeType, type LegacyConnection } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; import UploadFile from '../../Components/UploadFile'; import IODialog from '../../Components/IODialog'; -export const getLiveHost = async (socket: LegacyConnection) => { +export const getLiveHost = async (socket: LegacyConnection): Promise => { const res = await socket.getObjectViewSystem('host', 'system.host.', 'system.host.\u9999'); const hosts = Object.keys(res).map(id => `${id}.alive`); if (!hosts.length) { @@ -50,33 +45,37 @@ const ImportProjectDialog: React.FC = props => { const [askOpenProject, setAskOpenProject] = useState(false); const [working, setWorking] = useState(false); - const importProject = () => { + const importProject = (): void => { if (props.projects.includes(projectName) && !showConfirmation) { setShowConfirmation(true); - return false; + return; } setWorking(true); - getLiveHost(props.socket) - .then(host => { - if (!host) { - window.alert(I18n.t('No live hosts found!')); - setWorking(false); - return; - } - - let timeout = setTimeout(() => { - timeout = null; - setWorking(false); - window.alert(I18n.t('Cannot upload project: timeout')); - }, 60_000); - - props.socket.getRawSocket().emit('sendToHost', host, 'writeDirAsZip', { + void getLiveHost(props.socket).then(host => { + if (!host) { + window.alert(I18n.t('No live hosts found!')); + setWorking(false); + return; + } + + let timeout = setTimeout(() => { + timeout = null; + setWorking(false); + window.alert(I18n.t('Cannot upload project: timeout')); + }, 60_000); + + props.socket.getRawSocket().emit( + 'sendToHost', + host, + 'writeDirAsZip', + { id: `${props.adapterName}.${props.instance}`, name: projectName || 'main', data: projectData.split(',')[1], - }, async (result: {error?: string}) => { + }, + async (result: { error?: string }) => { setWorking(false); timeout && clearTimeout(timeout); timeout = null; @@ -94,76 +93,83 @@ const ImportProjectDialog: React.FC = props => { await props.refreshProjects(true); props.onClose(true, projectName); } - }); - }); + }, + ); + }); - return false; + return; }; - const confirmDialog = showConfirmation ? { - setShowConfirmation(false); - isYes && importProject(); - }} - /> : null; - - const askOpenDialog = askOpenProject ? { - setAskOpenProject(false); - isYes && props.loadProject(projectName); - props.onClose(isYes, projectName); - }} - /> : null; - - return props.onClose()} - actionNoClose - action={importProject} - actionTitle="Import" - ActionIcon={BiImport} - actionDisabled={!projectName?.length || !projectData || working} - closeDisabled={working} - > - { - if (name.match(/^\d\d\d\d-\d\d-\d\d-/)) { - setProjectName(name.substring(11).replace(/\.zip$/i, '')); - } else { - setProjectName(name.replace(/\.zip$/i, '')); - } - setProjectData(data as string); + const confirmDialog = showConfirmation ? ( + { + setShowConfirmation(false); + isYes && importProject(); }} - accept={{ - 'application/zip': ['.zip'], + /> + ) : null; + + const askOpenDialog = askOpenProject ? ( + { + setAskOpenProject(false); + isYes && props.loadProject(projectName); + props.onClose(isYes, projectName); }} /> -
    - props.onClose()} + actionNoClose + action={importProject} + actionTitle="Import" + ActionIcon={BiImport} + actionDisabled={!projectName?.length || !projectData || working} + closeDisabled={working} + > + setProjectName(e.target.value.replace(/[^\da-zA-Z\-_.]/, ''))} + themeType={props.themeType} + onUpload={(name, data) => { + if (name.match(/^\d\d\d\d-\d\d-\d\d-/)) { + setProjectName(name.substring(11).replace(/\.zip$/i, '')); + } else { + setProjectName(name.replace(/\.zip$/i, '')); + } + setProjectData(data as string); + }} + accept={{ + 'application/zip': ['.zip'], + }} /> -
    - {confirmDialog} - {askOpenDialog} -
    ; +
    + setProjectName(e.target.value.replace(/[^\da-zA-Z\-_.]/, ''))} + /> +
    + {confirmDialog} + {askOpenDialog} + + ); }; export default ImportProjectDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/PermissionsDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/PermissionsDialog.tsx index 9795d4b79..8c33d4e64 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/PermissionsDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/PermissionsDialog.tsx @@ -1,12 +1,7 @@ import React from 'react'; -import { - Check as SaveIcon, - Info as InfoIcon, -} from '@mui/icons-material'; -import { - Checkbox, -} from '@mui/material'; +import { Check as SaveIcon, Info as InfoIcon } from '@mui/icons-material'; +import { Checkbox } from '@mui/material'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; import IconButton from '@mui/material/IconButton'; @@ -15,9 +10,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import Collapse from '@mui/material/Collapse'; import { type LegacyConnection, I18n } from '@iobroker/adapter-react-v5'; -import type { - AnyWidgetId, Permissions, Project, Widget, -} from '@iobroker/types-vis-2'; +import type { AnyWidgetId, Permissions, Project, Widget } from '@iobroker/types-vis-2'; import { store } from '@/Store'; import { deepClone, DEFAULT_PERMISSIONS } from '@/Utils/utils'; import IODialog from '../../Components/IODialog'; @@ -32,7 +25,7 @@ interface PermissionsDialogProps { } /** Permissions assignment to username */ -type PermissionsMap = Map +type PermissionsMap = Map; interface PermissionsDialogState { /** Contains all existing users */ @@ -43,7 +36,7 @@ interface PermissionsDialogState { viewPermissions: Record; /** The permissions assignment to users for each widget */ widgetPermissions: Record; - /** Id for each card and open status */ + /** ID for each card and open status */ cardOpen: Record; } @@ -85,7 +78,12 @@ export default class PermissionsDialog extends React.Component { - const userView: Record = await this.props.socket.getObjectViewSystem('user', 'system.user.', 'system.user.\u9999') as Record; + const userView: Record = + await this.props.socket.getObjectViewSystem( + 'user', + 'system.user.', + 'system.user.\u9999', + ); const { visProject } = store.getState(); const projectPermissions = new Map(); const viewPermissions: Record = {}; @@ -119,7 +117,10 @@ export default class PermissionsDialog extends React.Component - -
    -

    - {I18n.t('Only the admin user can change permissions')} -
    - {I18n.t('Read = Runtime access')} -
    - {I18n.t('Write = Edit mode access')} -

    + return ( +
    + +
    +

    + {I18n.t('Only the admin user can change permissions')} +
    + {I18n.t('Read = Runtime access')} +
    + {I18n.t('Write = Edit mode access')} +

    +
    -
    ; + ); } /** @@ -196,52 +204,68 @@ export default class PermissionsDialog extends React.Component -

    {`${widget.data.name ? `${widget.data.name} (${wid})` : wid}:`}

    -
    - { - const newState = this.state; - const currVal = this.state.widgetPermissions[wid].get(user); - - newState.widgetPermissions[wid].set(user, { - read: !currVal?.read, - write: !!currVal?.write, - }); - this.setState(newState); - }} - /> - {I18n.t('Read')} - { - const newState = this.state; - const currVal = this.state.widgetPermissions[wid].get(user); - - newState.widgetPermissions[wid].set(user, { - read: !!currVal?.read, - write: !currVal?.write, - }); - this.setState(newState); +

    {`${widget.data.name ? `${widget.data.name} (${wid})` : wid}:`}

    +
    - {I18n.t('Write')} + > + { + const newState = this.state; + const currVal = this.state.widgetPermissions[wid].get(user); + + newState.widgetPermissions[wid].set(user, { + read: !currVal?.read, + write: !!currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Read')} + { + const newState = this.state; + const currVal = this.state.widgetPermissions[wid].get(user); + + newState.widgetPermissions[wid].set(user, { + read: !!currVal?.read, + write: !currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Write')} +
    -
    ; + ); } /** @@ -250,177 +274,212 @@ export default class PermissionsDialog extends React.Component - -
    -
    - { - const newState = this.state; - const currVal = this.state.viewPermissions[view].get(user); - - newState.viewPermissions[view].set(user, { - read: !currVal?.read, - write: !!currVal?.write, - }); - this.setState(newState); - }} - /> - {I18n.t('Read')} - { - const newState = this.state; - const currVal = this.state.viewPermissions[view].get(user); - - newState.viewPermissions[view].set(user, { - read: !!currVal?.read, - write: !currVal?.write, - }); - this.setState(newState); - }} - /> - {I18n.t('Write')} -
    - { - this.setState({ - cardOpen: { - ...this.state.cardOpen, - [viewId]: !this.state.cardOpen[viewId], - }, - }); - }} - aria-label="expand" - size="small" - > - {this.state.cardOpen[viewId] ? : } - -
    -
    } - /> - - - {this.state.cardOpen[viewId] ? Object.entries(visProject[view].widgets).map(([wid, widget]) => this.renderWidgetPermissions({ ...options, wid: wid as AnyWidgetId, widget })) : null} - - - ; - } - - /** - * Render the actual component - */ - render(): React.JSX.Element { - const { activeUser, visProject } = store.getState(); - - return this.props.onClose()} - actionNoClose - action={() => this.onSave()} - actionTitle="Save" - ActionIcon={SaveIcon} - actionDisabled={false} - closeDisabled={false} - minWidth="600px" - > - {PermissionsDialog.renderInfoDialog()} - {this.state.users.map(user => - - + -
    - { - const newState = this.state; - const currVal = this.state.projectPermissions.get(user); - - newState.projectPermissions.set(user, { - read: !currVal?.read, - write: !!currVal?.write, - }); - this.setState(newState); - }} - /> - {I18n.t('Read')} - { - const newState = this.state; - const currVal = this.state.projectPermissions.get(user); - - newState.projectPermissions.set(user, { - read: !!currVal?.read, - write: !currVal?.write, - }); - this.setState(newState); +
    +
    - {I18n.t('Write')} - + > + { + const newState = this.state; + const currVal = this.state.viewPermissions[view].get(user); + + newState.viewPermissions[view].set(user, { + read: !currVal?.read, + write: !!currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Read')} + { + const newState = this.state; + const currVal = this.state.viewPermissions[view].get(user); + + newState.viewPermissions[view].set(user, { + read: !!currVal?.read, + write: !currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Write')} +
    { this.setState({ cardOpen: { ...this.state.cardOpen, - [user]: !this.state.cardOpen[user], + [viewId]: !this.state.cardOpen[viewId], }, }); }} aria-label="expand" size="small" > - {this.state.cardOpen[user] ? : } + {this.state.cardOpen[viewId] ? : }
    -
    } - > -
    - + } + /> + + + {this.state.cardOpen[viewId] + ? Object.entries(visProject[view].widgets).map(([wid, widget]) => + this.renderWidgetPermissions({ ...options, wid: wid as AnyWidgetId, widget }), + ) + : null} + + +
    + ); + } + + /** + * Render the actual component + */ + render(): React.JSX.Element { + const { activeUser, visProject } = store.getState(); + + return ( + this.props.onClose()} + actionNoClose + action={() => this.onSave()} + actionTitle="Save" + ActionIcon={SaveIcon} + actionDisabled={false} + closeDisabled={false} + minWidth="600px" + > + {PermissionsDialog.renderInfoDialog()} + {this.state.users.map(user => ( + - - {this.state.cardOpen[user] ? Object.keys(visProject).map(view => (view === '___settings' ? null : this.renderViewPermissions({ - visProject, view, user, activeUser, - }))) : null} - - - )} - ; + +
    + { + const newState = this.state; + const currVal = this.state.projectPermissions.get(user); + + newState.projectPermissions.set(user, { + read: !currVal?.read, + write: !!currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Read')} + { + const newState = this.state; + const currVal = this.state.projectPermissions.get(user); + + newState.projectPermissions.set(user, { + read: !!currVal?.read, + write: !currVal?.write, + }); + this.setState(newState); + }} + /> + {I18n.t('Write')} + + { + this.setState({ + cardOpen: { + ...this.state.cardOpen, + [user]: !this.state.cardOpen[user], + }, + }); + }} + aria-label="expand" + size="small" + > + {this.state.cardOpen[user] ? ( + + ) : ( + + )} + +
    +
    + } + > + + + {this.state.cardOpen[user] + ? Object.keys(visProject).map(view => + view === '___settings' + ? null + : this.renderViewPermissions({ + visProject, + view, + user, + activeUser, + }), + ) + : null} + + + + ))} + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ProjectDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ProjectDialog.tsx index b1c963ccd..a679ca619 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ProjectDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/ProjectDialog.tsx @@ -1,14 +1,8 @@ import React from 'react'; -import { - TextField, -} from '@mui/material'; +import { TextField } from '@mui/material'; -import { - Add as AddIcon, - Edit as EditIcon, - Delete as DeleteIcon, -} from '@mui/icons-material'; +import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material'; import { I18n } from '@iobroker/adapter-react-v5'; @@ -47,11 +41,11 @@ const ProjectDialog: React.FC = props => { add: I18n.t('Add'), }; - const addProject = () => props.addProject(props.dialogName); + const addProject = (): void => props.addProject(props.dialogName); - const deleteProject = () => props.deleteProject(props.dialogProject); + const deleteProject = (): void => props.deleteProject(props.dialogProject); - const renameProject = () => props.renameProject(props.dialogProject, props.dialogName); + const renameProject = (): void => props.renameProject(props.dialogProject, props.dialogName); const dialogActions = { delete: deleteProject, @@ -77,30 +71,33 @@ const ProjectDialog: React.FC = props => { dialogDisabled = props.dialogName === '' || props.projects.includes(props.dialogName); } - return { - props.setDialog(null); - props.setDialogProject(null); - }} - ActionIcon={DialogIcon || null} - action={dialogActions[props.dialog]} - actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} - actionDisabled={dialogDisabled} - > - {props.dialog === 'delete' ? null - : props.setDialogName(e.target.value)} - /> } - ; + return ( + { + props.setDialog(null); + props.setDialogProject(null); + }} + ActionIcon={DialogIcon || null} + action={dialogActions[props.dialog]} + actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} + actionDisabled={dialogDisabled} + > + {props.dialog === 'delete' ? null : ( + props.setDialogName(e.target.value)} + /> + )} + + ); }; export default ProjectDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/index.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/index.tsx index e4854f8fd..757ec432c 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/index.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ProjectsManager/index.tsx @@ -1,7 +1,13 @@ import React, { useState } from 'react'; -import type { PopoverProps } from '@mui/material'; import { - AppBar, Button, IconButton, Tooltip, Menu, MenuItem, CircularProgress, + AppBar, + Button, + IconButton, + Tooltip, + Menu, + MenuItem, + CircularProgress, + type PopoverProps, } from '@mui/material'; import type JQuery from 'jquery'; @@ -19,7 +25,6 @@ import { I18n, type ThemeType, type LegacyConnection } from '@iobroker/adapter-r import type Editor from '@/Editor'; import type { VisTheme } from '@iobroker/types-vis-2'; -import commonStyles from '@/Utils/styles'; import IODialog from '../../Components/IODialog'; import ImportProjectDialog, { getLiveHost } from './ImportProjectDialog'; import ProjectDialog from './ProjectDialog'; @@ -95,7 +100,7 @@ const ProjectsManage: React.FC = props => { return null; } - const showDialog = (type: 'add' | 'rename' | 'delete', project?: string) => { + const showDialog = (type: 'add' | 'rename' | 'delete', project?: string): void => { project = project || props.selectedView; const dialogDefaultName = { @@ -109,7 +114,7 @@ const ProjectsManage: React.FC = props => { setDialogName(dialogDefaultName[type]); }; - const exportProject = async (projectName: string | false, isAnonymize?: boolean) => { + const exportProject = async (projectName: string | false, isAnonymize?: boolean): Promise => { setWorking(projectName); const host = await getLiveHost(props.socket); @@ -120,75 +125,83 @@ const ProjectsManage: React.FC = props => { } // to do find active host - props.socket.getRawSocket().emit('sendToHost', host, 'readDirAsZip', { - id: `${props.adapterName}.${props.instance}`, - name: projectName || 'main', - options: { - settings: isAnonymize, + props.socket.getRawSocket().emit( + 'sendToHost', + host, + 'readDirAsZip', + { + id: `${props.adapterName}.${props.instance}`, + name: projectName || 'main', + options: { + settings: isAnonymize, + }, }, - }, (data: { - error?: string; - data?: string; - }) => { - if (data.error) { - setWorking(false); - window.alert(data.error); - } else { - const d = new Date(); - let date = d.getFullYear().toString(); - let m = d.getMonth() + 1; - let mString = m.toString(); - if (m < 10) { - mString = `0${m}`; + (data: { error?: string; data?: string }) => { + if (data.error) { + setWorking(false); + window.alert(data.error); + } else { + const d = new Date(); + let date = d.getFullYear().toString(); + let m = d.getMonth() + 1; + let mString = m.toString(); + if (m < 10) { + mString = `0${m}`; + } + date += `-${mString}`; + m = d.getDate(); + if (m < 10) { + mString = `0${m}`; + } + date += `-${mString}-`; + setWorking(false); + (window.$ as any)('body').append( + ``, + ); + document.getElementById('zip_download').click(); + document.getElementById('zip_download').remove(); } - date += `-${mString}`; - m = d.getDate(); - if (m < 10) { - mString = `0${m}`; - } - date += `-${mString}-`; - setWorking(false); - (window.$ as any)('body').append(``); - document.getElementById('zip_download').click(); - document.getElementById('zip_download').remove(); - } - }); + }, + ); }; - const exportDialog = setShowExportDialog(false)} - open={!!showExportDialog} - anchorEl={anchorEl} - > - { - setAnchorEl(null); - setShowExportDialog(null); - await exportProject(showExportDialog); - }} - > - {I18n.t('normal')} - - { - setAnchorEl(null); - setShowExportDialog(null); - await exportProject(showExportDialog, true); - }} + const exportDialog = ( + setShowExportDialog(false)} + open={!!showExportDialog} + anchorEl={anchorEl} > - {I18n.t('anonymize')} - - ; + { + setAnchorEl(null); + setShowExportDialog(null); + await exportProject(showExportDialog); + }} + > + {I18n.t('normal')} + + { + setAnchorEl(null); + setShowExportDialog(null); + await exportProject(showExportDialog, true); + }} + > + {I18n.t('anonymize')} + + + ); - return props.open ? - -
    - - +
    + - showDialog('add')} - size="small" - style={{ ...styles.button, ...(!props.projects.length ? styles.blink : undefined) }} + - - - - - setImportDialog(true)} size="small" style={{ ...styles.button, width: 34 }}> - - - - - {props.projects.sort((projName1, projName2) => projName1.toLowerCase().localeCompare(projName2)).map((projectName, key) =>
    - - - - {working === projectName ? : - { - setAnchorEl(event.currentTarget); - // TODO ensure correct project is opened - if (props.projectName !== projectName) { - props.loadProject(projectName); - } - setShowPermissionsDialog(!!projectName); - }} - size="small" - > - - } + showDialog('add')} + size="small" + style={{ ...styles.button, ...(!props.projects.length ? styles.blink : undefined) }} + > + + - - {working === projectName ? : - { - setAnchorEl(event.currentTarget); - setShowExportDialog(projectName); - }} - size="small" - > - - } + + setImportDialog(true)} + size="small" + style={{ ...styles.button, width: 34 }} + > + + - - - showDialog('rename', projectName)} - disabled={!!working} + + {props.projects + .sort((projName1, projName2) => projName1.toLowerCase().localeCompare(projName2)) + .map((projectName, key) => ( +
    +
    )} -
    - {exportDialog} - {dialog ? : null} - {showPermissionsDialog ? setShowPermissionsDialog(false)} - // loadProject={props.loadProject} - /> : null} - {importDialog ? { - setImportDialog(false); - if (created && props.projectName !== newProjectName) { - window.location.href = `?${newProjectName}`; - } else if (created) { - props.onClose(); - } - }} - projectName={props.projectName} - socket={props.socket} - refreshProjects={props.refreshProjects} - loadProject={props.loadProject} - adapterName={props.adapterName} - instance={props.instance} - openNewProjectOnCreate - /> : null} - : null; + {projectName} + + + + {working === projectName ? ( + + ) : ( + { + setAnchorEl(event.currentTarget); + // TODO ensure correct project is opened + if (props.projectName !== projectName) { + props.loadProject(projectName); + } + setShowPermissionsDialog(!!projectName); + }} + size="small" + > + + + )} + + + {working === projectName ? ( + + ) : ( + { + setAnchorEl(event.currentTarget); + setShowExportDialog(projectName); + }} + size="small" + > + + + )} + + + + showDialog('rename', projectName)} + disabled={!!working} + > + + + + + showDialog('delete', projectName)} + slotProps={{ popper: { sx: { pointerEvents: 'none' } } }} + > + + + + + + + +
    + ))} +
    + {exportDialog} + {dialog ? ( + + ) : null} + {showPermissionsDialog ? ( + setShowPermissionsDialog(false)} + // loadProject={props.loadProject} + /> + ) : null} + {importDialog ? ( + { + setImportDialog(false); + if (created && props.projectName !== newProjectName) { + window.location.href = `?${newProjectName}`; + } else if (created) { + props.onClose(); + } + }} + projectName={props.projectName} + socket={props.socket} + refreshProjects={props.refreshProjects} + loadProject={props.loadProject} + adapterName={props.adapterName} + instance={props.instance} + openNewProjectOnCreate + /> + ) : null} +
    + ) : null; }; export default ProjectsManage; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/Settings.tsx b/packages/iobroker.vis-2/src/src/Toolbar/Settings.tsx index d21621b78..e98ecf873 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/Settings.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/Settings.tsx @@ -10,7 +10,8 @@ import { Select, Switch, TextField, - FormHelperText, IconButton, + FormHelperText, + IconButton, } from '@mui/material'; import { ContentCopy, Save as SaveIcon, Refresh } from '@mui/icons-material'; @@ -64,7 +65,12 @@ interface SettingsFieldNumber extends SettingsFieldBase { type: 'number'; } -type SettingsField = SettingsFieldSelect | SettingsFieldRaw | SettingsFieldCheckbox | SettingsFieldSwitchMode | SettingsFieldNumber; +type SettingsField = + | SettingsFieldSelect + | SettingsFieldRaw + | SettingsFieldCheckbox + | SettingsFieldSwitchMode + | SettingsFieldNumber; interface SettingsProps { changeProject: Editor['changeProject']; @@ -91,13 +97,12 @@ const Settings: React.FC = props => { setInstance(_instance); // read project settings - props.socket.readDir(`${props.adapterName}.${props.instance}`, props.projectName) - .then(files => { - const file = files.find(f => f.file === 'vis-views.json'); - if ((file as any)?.mode || file?.acl?.permissions) { - setProjectMode((file as any).mode || file.acl.permissions); - } - }); + void props.socket.readDir(`${props.adapterName}.${props.instance}`, props.projectName).then(files => { + const file = files.find(f => f.file === 'vis-views.json'); + if ((file as any)?.mode || file?.acl?.permissions) { + setProjectMode((file as any).mode || file.acl.permissions); + } + }); setSettings(_settings); }, []); @@ -172,48 +177,50 @@ const Settings: React.FC = props => { { name: 'States Debounce Time (millis)', field: 'statesDebounceTime', type: 'number' }, { type: 'raw', - Node:
    - setInstance(e.target.value)} - InputProps={{ - endAdornment: instance ? Utils.copyToClipboard(instance)} - > - - : null, - sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, - }} - /> - -
    , + setInstance(e.target.value)} + InputProps={{ + endAdornment: instance ? ( + Utils.copyToClipboard(instance)}> + + + ) : null, + sx: { ...commonStyles.clearPadding, ...commonStyles.fieldContent }, + }} + /> + +
    + ), }, { type: 'switchMode' }, // very specific control { @@ -231,136 +238,180 @@ const Settings: React.FC = props => { }, ]; - const save = () => { + const save = (): void => { const project = deepClone(store.getState().visProject); project.___settings = settings; - props.changeProject(project); + void props.changeProject(project); props.onClose(); }; - return -
    - {fields.map((field, key) => { - const value = settings[field.field as keyof ProjectSettings]; + return ( + +
    + {fields.map((field, key) => { + const value = settings[field.field as keyof ProjectSettings]; - const change = (changeValue: any) => { - const newSettings = deepClone(settings); - (newSettings[field.field as keyof ProjectSettings] as any) = changeValue; - setSettings(newSettings); - }; + const change = (changeValue: any): void => { + const newSettings = deepClone(settings); + (newSettings[field.field as keyof ProjectSettings] as any) = changeValue; + setSettings(newSettings); + }; - let result; + let result; - if (field.type === 'checkbox') { - result = change(e.target.checked)} />} - label={I18n.t(field.name)} - />; - } else if (field.type === 'select') { - result = - {I18n.t(field.name)} - - {field.help ? {I18n.t(field.help)} : null} - ; - } else if (field.type === 'raw') { - result = field.Node; - } else if (field.type === 'switchMode') { - result = { - // eslint-disable-next-line no-bitwise - props.socket.getRawSocket().emit( - 'chmodFile', - `${props.adapterName}.${props.instance}`, - `${props.projectName}/*`, - { mode: e.target.checked ? 0x644 : 0x600 }, - (err: string, files: { - file: string; - mode: number; - acl: { owner: string; ownerGroup: string; permissions: number }; - }[]) => { - if (err) { - window.alert(err); - } else { - const file = files.find(f => f.file === 'vis-views.json'); - if (file?.mode || file?.acl?.permissions) { - setProjectMode(file.mode || file.acl.permissions); - } - } - }, - ); - }} - />} - />; - } else { - result = change(e.target.value)} - label={I18n.t(field.name)} - helperText={field.help ? I18n.t(field.help) : null} - type={field.type} - />; - } - - return
    {result}
    ; - })} - -
    -
    ; + + return ( +
    + {result} +
    + ); + })} + +
    +
    + ); }; export default Settings; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ToolbarItems.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ToolbarItems.tsx index 386757558..f0d329c97 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ToolbarItems.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ToolbarItems.tsx @@ -12,7 +12,8 @@ import { Select, TextField, Tooltip, - Menu, type SelectChangeEvent, + Menu, + type SelectChangeEvent, } from '@mui/material'; import { Menu as MenuIcon } from '@mui/icons-material'; @@ -23,7 +24,6 @@ import type { ViewSettings, VisTheme } from '@iobroker/types-vis-2'; import { deepClone } from '@/Utils/utils'; import { store } from '@/Store'; import type Editor from '@/Editor'; -import commonStyles from '@/Utils/styles'; import MultiSelect from './MultiSelect'; const styles: Record = { @@ -39,13 +39,17 @@ const styles: Record = { color: theme.palette.action.disabled, }), toolbarItems: { - display: 'flex', flexDirection: 'row', flex: 1, + display: 'flex', + flexDirection: 'row', + flex: 1, }, toolbarCol: { - display: 'flex', flexDirection: 'column', + display: 'flex', + flexDirection: 'column', }, toolbarRow: { - display: 'flex', flexDirection: 'row', + display: 'flex', + flexDirection: 'row', }, toolbarLabel: { fontSize: '72%', @@ -75,12 +79,7 @@ export interface SelectToolbarItem extends BaseToolbarItem { export interface MultiselectToolbarItem extends BaseToolbarItem { type: 'multiselect'; - items: { name: string | React.JSX.Element; - subName?: string; - value: string; - color?: string; - icon?: string; - }[]; + items: { name: string | React.JSX.Element; subName?: string; value: string; color?: string; icon?: string }[]; width: number; value?: string[]; onAction: (value: string[]) => void; @@ -123,7 +122,15 @@ export interface TextFieldToolbarItem extends BaseToolbarItem { onAction: (e: React.ChangeEvent) => void; } -export type ToolbarItem = SelectToolbarItem | MultiselectToolbarItem | CheckboxToolbarItem | IconButtonToolbarItem | TextToolbarItem | ButtonToolbarItem | DividerToolbarItem | TextFieldToolbarItem; +export type ToolbarItem = + | SelectToolbarItem + | MultiselectToolbarItem + | CheckboxToolbarItem + | IconButtonToolbarItem + | TextToolbarItem + | ButtonToolbarItem + | DividerToolbarItem + | TextFieldToolbarItem; export interface ToolbarGroup { name: string | React.JSX.Element; @@ -157,7 +164,7 @@ class ToolbarItems extends React.Component this.state = { opened: null }; } - closeMenu() { + closeMenu(): void { this.setState({ opened: null }); } @@ -174,48 +181,64 @@ class ToolbarItems extends React.Component const project = deepClone(visProject); (project[this.props.selectedView].settings[item.field] as any) = changeValue; - this.props.changeProject(project); + void this.props.changeProject(project); } }; getItemSelect(item: SelectToolbarItem, key: number, value: string): React.JSX.Element { - return - {this.props.toolbarHeight !== 'veryNarrow' ? {I18n.t(item.name)} : null} - this.onAction(item, e.target.value)} > - {I18n.t(selectItem.name)} - )} - - ; + {item.items.map(selectItem => ( + + {I18n.t(selectItem.name)} + + ))} + + + ); } getItemMultiselect(item: MultiselectToolbarItem, key: number, value: string[]): React.JSX.Element { - return this.onAction(item, _value)} - setSelectedWidgets={this.props.setSelectedWidgets} - options={item.items.map(option => ({ - name: option.name as string, - subname: option.subName, - value: option.value, - color: option.color, - icon: option.icon, - }))} - themeType={this.props.themeType} - />; + return ( + this.onAction(item, _value)} + setSelectedWidgets={this.props.setSelectedWidgets} + options={item.items.map(option => ({ + name: option.name as string, + subname: option.subName, + value: option.value, + color: option.color, + icon: option.icon, + }))} + themeType={this.props.themeType} + /> + ); /* return {props.toolbarHeight !== 'veryNarrow' ? {I18n.t(item.name)} : null} @@ -254,20 +277,27 @@ class ToolbarItems extends React.Component } getItemCheckbox(item: CheckboxToolbarItem, key: number, value: boolean): React.JSX.Element { - return this.onAction(item, e.target.checked)} - size="small" - />} - label={I18n.t(item.name)} - />; + return ( + this.onAction(item, e.target.checked)} + size="small" + /> + } + label={I18n.t(item.name)} + /> + ); } getItemIconButton(item: IconButtonToolbarItem, key: number, full: boolean): React.JSX.Element { - return this.props.toolbarHeight !== 'veryNarrow' && full ? -
    + return this.props.toolbarHeight !== 'veryNarrow' && full ? ( +
    this.onAction(item)} @@ -281,13 +311,19 @@ class ToolbarItems extends React.Component color: item.color || undefined, }} > -
    +
    + +
    {I18n.t(item.name)}
    {item.subName ?
    {item.subName}
    : null}
    - : - + ) : ( +
    -
    ; +
    + ); } - getItem( - item: ToolbarItem, - key: number, - full?: boolean, - ): null | React.JSX.Element { + getItem(item: ToolbarItem, key: number, full?: boolean): null | React.JSX.Element { const { visProject } = store.getState(); const view = visProject[this.props.selectedView]; @@ -337,35 +370,51 @@ class ToolbarItems extends React.Component } if (item.type === 'text') { - return {`${I18n.t(item.text)}:`}; + return ( + {`${I18n.t(item.text)}:`} + ); } if (item.type === 'button') { - return ; + return ( + + ); } if (item.type === 'divider') { - return ; + return ( + + ); } - return this.onAction(item, e.target.value)} - InputLabelProps={{ shrink: true }} - label={this.props.toolbarHeight !== 'veryNarrow' ? I18n.t(item.name) : null} - style={styles.textInput} - />; + return ( + this.onAction(item, e.target.value)} + InputLabelProps={{ shrink: true }} + label={this.props.toolbarHeight !== 'veryNarrow' ? I18n.t(item.name) : null} + style={styles.textInput} + /> + ); } render(): React.JSX.Element | React.JSX.Element[] { @@ -392,32 +441,44 @@ class ToolbarItems extends React.Component items = _items; } - const div =
    -
    - {items.map((item, key) => { - if (Array.isArray(item)) { - return
    - {(item as ToolbarItem[][]).map((subItem, subKey) =>
    - {subItem.map((subItem2, subKey2) => this.getItem(subItem2, subKey2, false))} -
    )} -
    ; - } - return this.getItem(item, key, true); - })} + const div = ( +
    +
    + {items.map((item, key) => { + if (Array.isArray(item)) { + return ( +
    + {(item as ToolbarItem[][]).map((subItem, subKey) => ( +
    + {subItem.map((subItem2, subKey2) => this.getItem(subItem2, subKey2, false))} +
    + ))} +
    + ); + } + return this.getItem(item, key, true); + })} +
    + {this.props.toolbarHeight === 'full' ? ( +
    + {typeof name === 'string' ? (doNotTranslateName ? name : I18n.t(name)) : name} +
    + ) : null}
    - {this.props.toolbarHeight === 'full' ?
    - {typeof name === 'string' ? (doNotTranslateName ? name : I18n.t(name)) : name} -
    : null} -
    ; + ); if (this.props.group.compact) { return [ this.setState({ opened: this.state.opened ? null : e.currentTarget })} diff --git a/packages/iobroker.vis-2/src/src/Toolbar/Views.tsx b/packages/iobroker.vis-2/src/src/Toolbar/Views.tsx index fd14c519e..f3aaaf886 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/Views.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/Views.tsx @@ -1,26 +1,16 @@ import React, { useState } from 'react'; -import { - Tooltip, -} from '@mui/material'; +import { Tooltip } from '@mui/material'; -import { - Add as AddIcon, - Edit as EditIcon, - Delete as DeleteIcon, - Menu as MenuIcon, -} from '@mui/icons-material'; +import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon, Menu as MenuIcon } from '@mui/icons-material'; -import type { ThemeName, ThemeType } from '@iobroker/adapter-react-v5'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { I18n, type ThemeName, type ThemeType } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; import type { VisTheme } from '@iobroker/types-vis-2'; -import commonStyles from '@/Utils/styles'; import ViewsManager from './ViewsManager'; -import type { ToolbarItem } from './ToolbarItems'; -import ToolbarItems from './ToolbarItems'; +import ToolbarItems, { type ToolbarItem } from './ToolbarItems'; import ViewDialog from './ViewsManager/ViewDialog'; const styles: Record = { @@ -55,7 +45,7 @@ interface ViewsProps { toggleView: Editor['toggleView']; } -const Views = (props: ViewsProps) => { +const Views = (props: ViewsProps): React.JSX.Element => { const [dialog, setDialog] = useState(null); // eslint-disable-next-line no-spaced-func, func-call-spacing const [dialogCallback, setDialogCallback] = useState<{ cb: (dialogName: string) => void }>(null); @@ -69,7 +59,7 @@ const Views = (props: ViewsProps) => { parentId?: string, // eslint-disable-next-line no-shadow cb?: (dialogName: string) => void, - ) => { + ): void => { view = view || props.selectedView; const dialogDefaultName: Record = { @@ -89,76 +79,99 @@ const Views = (props: ViewsProps) => { name: React.JSX.Element; items: (ToolbarItem | ToolbarItem[] | ToolbarItem[][])[]; } = { - name: - - props.setProjectsDialog(true)} + name: ( + + - {props.projectName} - - - , + props.setProjectsDialog(true)} + > + {props.projectName} + + + + ), items: [ { - type: 'icon-button', Icon: AddIcon, name: 'Add new view', onAction: () => showDialog('add'), disabled: !!props.selectedGroup || !props.editMode, + type: 'icon-button', + Icon: AddIcon, + name: 'Add new view', + onAction: () => showDialog('add'), + disabled: !!props.selectedGroup || !props.editMode, }, [ [ { - type: 'icon-button', Icon: EditIcon, name: 'Rename view', onAction: () => showDialog('rename'), disabled: !!props.selectedGroup || !props.editMode, + type: 'icon-button', + Icon: EditIcon, + name: 'Rename view', + onAction: () => showDialog('rename'), + disabled: !!props.selectedGroup || !props.editMode, }, ], [ { - type: 'icon-button', Icon: DeleteIcon, name: 'Delete actual view', onAction: () => showDialog('delete'), disabled: !!props.selectedGroup || !props.editMode, + type: 'icon-button', + Icon: DeleteIcon, + name: 'Delete actual view', + onAction: () => showDialog('delete'), + disabled: !!props.selectedGroup || !props.editMode, }, ], ], { - type: 'icon-button', Icon: MenuIcon, name: 'Manage views', onAction: () => props.setViewsManager(true), disabled: !!props.selectedGroup, + type: 'icon-button', + Icon: MenuIcon, + name: 'Manage views', + onAction: () => props.setViewsManager(true), + disabled: !!props.selectedGroup, }, ], }; - return <> - - props.setViewsManager(false)} - showDialog={showDialog} - theme={props.theme} - changeProject={props.changeProject} - editMode={props.editMode} - selectedView={props.selectedView} - themeName={props.themeName} - themeType={props.themeType} - toggleView={props.toggleView} - /> - - ; + return ( + <> + + props.setViewsManager(false)} + showDialog={showDialog} + theme={props.theme} + changeProject={props.changeProject} + editMode={props.editMode} + selectedView={props.selectedView} + themeName={props.themeName} + themeType={props.themeType} + toggleView={props.toggleView} + /> + + + ); }; export default Views; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ExportDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ExportDialog.tsx index 0a3fa98c7..9f8658e21 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ExportDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ExportDialog.tsx @@ -14,22 +14,24 @@ interface ExportDialogProps { view: string; } -const ExportDialog: React.FC = props => Utils.copyToClipboard(JSON.stringify(store.getState().visProject[props.view], null, 2))} - actionTitle="Copy to clipboard" - actionNoClose - ActionIcon={FileCopyIcon} -> - -; +const ExportDialog: React.FC = props => ( + Utils.copyToClipboard(JSON.stringify(store.getState().visProject[props.view], null, 2))} + actionTitle="Copy to clipboard" + actionNoClose + ActionIcon={FileCopyIcon} + > + + +); export default ExportDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Folder.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Folder.tsx index fa92d728b..d20ae6399 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Folder.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Folder.tsx @@ -2,10 +2,7 @@ import React, { useEffect } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; -import { - Box, - IconButton, Tooltip, -} from '@mui/material'; +import { Box, IconButton, Tooltip } from '@mui/material'; import { Add as AddIcon, @@ -17,7 +14,6 @@ import { FaFolder as FolderClosedIcon, FaFolderOpen as FolderOpenedIcon } from ' import { Utils, I18n } from '@iobroker/adapter-react-v5'; import type { VisTheme } from '@iobroker/types-vis-2'; -import commonStyles from '@/Utils/styles'; import { store } from '@/Store'; const styles: Record = { @@ -73,63 +69,85 @@ interface FolderProps { } const Folder: React.FC = props => { - const folderBlock = - {props.foldersCollapsed.includes(props.folder.id) - ? - : } - {props.folder.name} - ; + const folderBlock = ( + + {props.foldersCollapsed.includes(props.folder.id) ? ( + + ) : ( + + )} + {props.folder.name} + + ); - const [{ canDrop }, drop] = useDrop<{ - name: string; - folder: FolderType; - }, unknown, { isOver: boolean; canDrop: boolean }>(() => ({ - accept: ['view', 'folder'], - drop: () => ({ folder: props.folder }), - canDrop: (item, monitor) => { - if (monitor.getItemType() === 'view') { - return store.getState().visProject[item.name].parentId !== props.folder.id; - } - if (monitor.getItemType() === 'folder') { - let currentFolder = props.folder; - if (currentFolder.id === item.folder.parentId) { - return false; + const [{ canDrop }, drop] = useDrop< + { + name: string; + folder: FolderType; + }, + unknown, + { isOver: boolean; canDrop: boolean } + >( + () => ({ + accept: ['view', 'folder'], + drop: () => ({ folder: props.folder }), + canDrop: (item, monitor) => { + if (monitor.getItemType() === 'view') { + return store.getState().visProject[item.name].parentId !== props.folder.id; } - while (true) { - if (currentFolder.id === item.folder.id) { + if (monitor.getItemType() === 'folder') { + let currentFolder = props.folder; + if (currentFolder.id === item.folder.parentId) { return false; } - if (!currentFolder.parentId) { - return true; + // eslint-disable-next-line no-constant-condition + while (true) { + if (currentFolder.id === item.folder.id) { + return false; + } + if (!currentFolder.parentId) { + return true; + } + currentFolder = store + .getState() + .visProject.___settings.folders.find( + foundFolder => foundFolder.id === currentFolder.parentId, + ); } - currentFolder = store.getState().visProject.___settings.folders.find(foundFolder => foundFolder.id === currentFolder.parentId); } - } - return false; - }, - collect: monitor => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), + return false; + }, + collect: monitor => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), }), - }), [store.getState().visProject]); + [store.getState().visProject], + ); - const [{ isDraggingThisItem }, dragRef, preview] = useDrag({ - type: 'folder', - item: () => ({ - folder: props.folder, - preview:
    {folderBlock}
    , - }), - end: (item, monitor) => { - const dropResult = monitor.getDropResult<{folder: FolderType}>(); - if (item && dropResult) { - props.moveFolder(item.folder.id, dropResult.folder.id); - } + const [{ isDraggingThisItem }, dragRef, preview] = useDrag( + { + type: 'folder', + item: () => ({ + folder: props.folder, + preview:
    {folderBlock}
    , + }), + end: (item, monitor) => { + const dropResult = monitor.getDropResult<{ folder: FolderType }>(); + if (item && dropResult) { + props.moveFolder(item.folder.id, dropResult.folder.id); + } + }, + collect: monitor => ({ + isDraggingThisItem: monitor.isDragging(), + handlerId: monitor.getHandlerId(), + }), }, - collect: monitor => ({ - isDraggingThisItem: monitor.isDragging(), - handlerId: monitor.getHandlerId(), - }), - }, [store.getState().visProject]); + [store.getState().visProject], + ); useEffect(() => { preview(getEmptyImage(), { captureDraggingState: true }); @@ -141,109 +159,152 @@ const Folder: React.FC = props => { console.log(`${props.folder.name} ${props.isDragging} ${canDrop}`); - return + return ( - {props.foldersCollapsed.includes(props.folder.id) - ? { - const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); - foldersCollapsed.splice(foldersCollapsed.indexOf(props.folder.id), 1); - props.setFoldersCollapsed(foldersCollapsed); - window.localStorage.setItem('ViewsManager.foldersCollapsed', JSON.stringify(foldersCollapsed)); - }} - /> - : { - const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); - foldersCollapsed.push(props.folder.id); - props.setFoldersCollapsed(foldersCollapsed); - window.localStorage.setItem('ViewsManager.foldersCollapsed', JSON.stringify(foldersCollapsed)); - }} - />} - - { - const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); - const index = foldersCollapsed.indexOf(props.folder.id); - if (index !== -1) { - foldersCollapsed.splice(index, 1); - } else { - foldersCollapsed.push(props.folder.id); - } - props.setFoldersCollapsed(foldersCollapsed); - window.localStorage.setItem('ViewsManager.foldersCollapsed', JSON.stringify(foldersCollapsed)); - }} - > - {props.folder.name} - - - {props.editMode ? - props.showDialog('add', null, props.folder.id)} - > - - - : null} - {props.editMode ? - { - props.setFolderDialog('add'); - props.setFolderDialogName(''); - props.setFolderDialogParentId(props.folder.id); - }} - > - - - : null} - {props.editMode ? - { - props.setFolderDialog('rename'); - props.setFolderDialogName(props.folder.name); - props.setFolderDialogId(props.folder.id); - }} - > - - - : null} - {props.editMode ? - - + {props.foldersCollapsed.includes(props.folder.id) ? ( + { + const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); + foldersCollapsed.splice(foldersCollapsed.indexOf(props.folder.id), 1); + props.setFoldersCollapsed(foldersCollapsed); + window.localStorage.setItem( + 'ViewsManager.foldersCollapsed', + JSON.stringify(foldersCollapsed), + ); + }} + /> + ) : ( + { - props.setFolderDialog('delete'); - props.setFolderDialogId(props.folder.id); + const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); + foldersCollapsed.push(props.folder.id); + props.setFoldersCollapsed(foldersCollapsed); + window.localStorage.setItem( + 'ViewsManager.foldersCollapsed', + JSON.stringify(foldersCollapsed), + ); }} - disabled={!!(store.getState().visProject.___settings.folders.find(foundFolder => foundFolder.parentId === props.folder.id) - || Object.values(store.getState().visProject).find(foundView => foundView.parentId === props.folder.id))} + /> + )} + + { + const foldersCollapsed: string[] = JSON.parse(JSON.stringify(props.foldersCollapsed)); + const index = foldersCollapsed.indexOf(props.folder.id); + if (index !== -1) { + foldersCollapsed.splice(index, 1); + } else { + foldersCollapsed.push(props.folder.id); + } + props.setFoldersCollapsed(foldersCollapsed); + window.localStorage.setItem('ViewsManager.foldersCollapsed', JSON.stringify(foldersCollapsed)); + }} + > + {props.folder.name} + + + {props.editMode ? ( + + props.showDialog('add', null, props.folder.id)} + > + + + + ) : null} + {props.editMode ? ( + + { + props.setFolderDialog('add'); + props.setFolderDialogName(''); + props.setFolderDialogParentId(props.folder.id); + }} + > + + + + ) : null} + {props.editMode ? ( + + { + props.setFolderDialog('rename'); + props.setFolderDialogName(props.folder.name); + props.setFolderDialogId(props.folder.id); + }} + > + + + + ) : null} + {props.editMode ? ( + - - - - : null} + + { + props.setFolderDialog('delete'); + props.setFolderDialogId(props.folder.id); + }} + disabled={ + !!( + store + .getState() + .visProject.___settings.folders.find( + foundFolder => foundFolder.parentId === props.folder.id, + ) || + Object.values(store.getState().visProject).find( + foundView => foundView.parentId === props.folder.id, + ) + ) + } + > + + + + + ) : null} + - ; + ); }; export default Folder; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/FolderDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/FolderDialog.tsx index 28bde319a..b18a81783 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/FolderDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/FolderDialog.tsx @@ -1,8 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; -import { - TextField, -} from '@mui/material'; +import { TextField } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import EditIcon from '@mui/icons-material/Edit'; @@ -35,7 +33,9 @@ const FolderDialog: React.FC = props => { return null; } - const folderObject = store.getState().visProject.___settings.folders.find(folder => folder.id === props.dialogFolder); + const folderObject = store + .getState() + .visProject.___settings.folders.find(folder => folder.id === props.dialogFolder); const dialogTitles = { delete: `${I18n.t('Do you want to delete folder "%s"', folderObject?.name)}?`, @@ -49,43 +49,46 @@ const FolderDialog: React.FC = props => { add: I18n.t('Add'), }; - const addFolder = () => { + const addFolder = (): void => { const project = deepClone(store.getState().visProject); project.___settings.folders.push({ id: uuidv4(), name: props.dialogName, parentId: props.dialogParentId, }); - props.changeProject(project); + void props.changeProject(project); }; - const deleteFolder = () => { + const deleteFolder = (): void => { const project = deepClone(store.getState().visProject); - project.___settings.folders.splice(project.___settings.folders.findIndex(folder => folder.id === props.dialogFolder), 1); - props.changeProject(project); + project.___settings.folders.splice( + project.___settings.folders.findIndex(folder => folder.id === props.dialogFolder), + 1, + ); + void props.changeProject(project); }; - const renameFolder = () => { + const renameFolder = (): void => { const project = deepClone(store.getState().visProject); project.___settings.folders.find(folder => folder.id === props.dialogFolder).name = props.dialogName; - props.changeProject(project); + void props.changeProject(project); }; const dialogActions = { delete: deleteFolder, rename: renameFolder, - add: addFolder, + add: addFolder, }; const dialogInputs = { rename: I18n.t('New name'), - add: I18n.t('Name'), + add: I18n.t('Name'), }; const dialogIcons = { delete: DeleteIcon, rename: EditIcon, - add: AddIcon, + add: AddIcon, }; const DialogIcon = dialogIcons[props.dialog]; @@ -95,30 +98,33 @@ const FolderDialog: React.FC = props => { dialogDisabled = props.dialogName === '' || props.dialogName === folderObject?.name; } - return { - props.setDialog(null); - props.setDialogFolder(null); - }} - ActionIcon={DialogIcon || null} - action={dialogActions[props.dialog]} - actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} - actionDisabled={dialogDisabled} - > - {props.dialog === 'delete' ? null - : props.setDialogName(e.target.value)} - /> } - ; + return ( + { + props.setDialog(null); + props.setDialogFolder(null); + }} + ActionIcon={DialogIcon || null} + action={dialogActions[props.dialog]} + actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} + actionDisabled={dialogDisabled} + > + {props.dialog === 'delete' ? null : ( + props.setDialogName(e.target.value)} + /> + )} + + ); }; export default FolderDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ImportDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ImportDialog.tsx index 035b206da..55bfdbbdb 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ImportDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ImportDialog.tsx @@ -52,30 +52,38 @@ const ImportDialog: React.FC = props => { } }, [editor.current]); - return props.importViewAction(view, data)} - actionDisabled={!view.length || !!errors.length} - > - { - editor.current = node; - inputField.current = node; - }} - value={data} - onChange={newValue => setData(newValue)} - height={200} - /> -
    - setView(e.target.value)} /> -
    -
    ; + return ( + props.importViewAction(view, data)} + actionDisabled={!view.length || !!errors.length} + > + { + editor.current = node; + inputField.current = node; + }} + value={data} + onChange={newValue => setData(newValue)} + height={200} + /> +
    + setView(e.target.value)} + /> +
    +
    + ); }; export default ImportDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Root.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Root.tsx index a0eda759e..d7502243d 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Root.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/Root.tsx @@ -1,4 +1,4 @@ -import { useDrop } from 'react-dnd'; +import { useDrop } from 'react-dnd'; import React, { useEffect } from 'react'; import { I18n } from '@iobroker/adapter-react-v5'; @@ -11,36 +11,45 @@ interface RootProps { } const Root: React.FC = props => { - const [{ canDrop, isOver }, drop] = useDrop<{ - name: string; - folder: FolderType; - }, unknown, { isOver: boolean; canDrop: boolean }>(() => ({ - accept: ['view', 'folder'], - drop: () => ({ folder: { id: null } }), - canDrop: (item, monitor) => { - if (monitor.getItemType() === 'view') { - return !!store.getState().visProject[item.name].parentId; - } - if (monitor.getItemType() === 'folder') { - return !!item.folder.parentId; - } - return false; + const [{ canDrop, isOver }, drop] = useDrop< + { + name: string; + folder: FolderType; }, - collect: monitor => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), + unknown, + { isOver: boolean; canDrop: boolean } + >( + () => ({ + accept: ['view', 'folder'], + drop: () => ({ folder: { id: null } }), + canDrop: (item, monitor) => { + if (monitor.getItemType() === 'view') { + return !!store.getState().visProject[item.name].parentId; + } + if (monitor.getItemType() === 'folder') { + return !!item.folder.parentId; + } + return false; + }, + collect: monitor => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), }), - }), [store.getState().visProject]); + [store.getState().visProject], + ); useEffect(() => { props.setIsOverRoot(isOver && canDrop); }, [isOver]); - return props.isDragging && canDrop ?
    -
    {I18n.t('Drop here to add to root')}
    -
    : null; + return props.isDragging && canDrop ? ( +
    +
    + {I18n.t('Drop here to add to root')} +
    +
    + ) : null; }; export default Root; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/View.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/View.tsx index 79931e54b..77d7b5bb6 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/View.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/View.tsx @@ -2,10 +2,7 @@ import React, { useEffect } from 'react'; import { useDrag } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; -import { - Box, - IconButton, Tooltip, -} from '@mui/material'; +import { Box, IconButton, Tooltip } from '@mui/material'; import { Edit as EditIcon, @@ -21,7 +18,6 @@ import { I18n, Utils } from '@iobroker/adapter-react-v5'; import type { VisTheme } from '@iobroker/types-vis-2'; import { store } from '@/Store'; -import commonStyles from '@/Utils/styles'; const styles: Record = { viewManageBlock: (theme: VisTheme) => theme.classes.viewManageBlock, @@ -68,37 +64,48 @@ interface ViewProps { hasPermissions: boolean; } -const View = (props: ViewProps) => { - const viewBlockPreview = - - - props.toggleView(props.name, !props.openedViews.includes(props.name))} +const View = (props: ViewProps): React.JSX.Element => { + const viewBlockPreview = ( + + + - {props.openedViews.includes(props.name) ? : } - - - {props.name} - ; + props.toggleView(props.name, !props.openedViews.includes(props.name))} + > + {props.openedViews.includes(props.name) ? : } + + + {props.name} + + ); - const [{ isDraggingThisItem }, dragRef, preview] = useDrag({ - type: 'view', - item: () => ({ - name: props.name, - preview:
    {viewBlockPreview}
    , - }), - end: (item, monitor) => { - const dropResult = monitor.getDropResult(); - if (item && dropResult) { - props.moveView(item.name, (dropResult as any).folder.id); - } + const [{ isDraggingThisItem }, dragRef, preview] = useDrag( + { + type: 'view', + item: () => ({ + name: props.name, + preview:
    {viewBlockPreview}
    , + }), + end: (item, monitor) => { + const dropResult = monitor.getDropResult(); + if (item && dropResult) { + props.moveView(item.name, (dropResult as any).folder.id); + } + }, + collect: monitor => ({ + isDraggingThisItem: monitor.isDragging(), + handlerId: monitor.getHandlerId(), + }), }, - collect: monitor => ({ - isDraggingThisItem: monitor.isDragging(), - handlerId: monitor.getHandlerId(), - }), - }, [store.getState().visProject]); + [store.getState().visProject], + ); useEffect(() => { preview(getEmptyImage(), { captureDraggingState: true }); @@ -108,64 +115,138 @@ const View = (props: ViewProps) => { props.setIsDragging(isDraggingThisItem ? props.name : ''); }, [isDraggingThisItem]); - const selectView = () => { + const selectView = (): void => { props.toggleView(props.name, true, true); }; - return - - {props.hasPermissions ?
    - -
    : } - + - props.toggleView(props.name, !props.openedViews.includes(props.name))} - sx={props.isDragging === props.name ? styles.dragging : (props.isDragging ? styles.noDrop : undefined)} + {props.hasPermissions ? ( +
    + +
    + ) : ( + + )} + - {props.openedViews.includes(props.name) ? : } -
    -
    - {props.hasPermissions ? {props.name} : {props.name}} - - {props.editMode && props.hasPermissions ? - props.setImportDialog(props.name)} size="small"> - - - : null} - - props.setExportDialog(props.name)} size="small"> - + props.toggleView(props.name, !props.openedViews.includes(props.name))} + sx={ + props.isDragging === props.name + ? styles.dragging + : props.isDragging + ? styles.noDrop + : undefined + } + > + {props.openedViews.includes(props.name) ? ( + + ) : ( + + )} - {props.editMode && props.hasPermissions ? - props.showDialog('rename', props.name)} size="small"> - - - : null} - {props.editMode && props.hasPermissions ? - props.showDialog('copy', props.name)} size="small"> - - - : null} - {props.editMode && props.hasPermissions ? - props.showDialog('delete', props.name)} size="small"> - - - : null} + {props.hasPermissions ? ( + + {props.name} + + ) : ( + {props.name} + )} + + {props.editMode && props.hasPermissions ? ( + + props.setImportDialog(props.name)} + size="small" + > + + + + ) : null} + + props.setExportDialog(props.name)} + size="small" + > + + + + {props.editMode && props.hasPermissions ? ( + + props.showDialog('rename', props.name)} + size="small" + > + + + + ) : null} + {props.editMode && props.hasPermissions ? ( + + props.showDialog('copy', props.name)} + size="small" + > + + + + ) : null} + {props.editMode && props.hasPermissions ? ( + + props.showDialog('delete', props.name)} + size="small" + > + + + + ) : null} +
    -
    ; + ); }; export default View; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ViewDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ViewDialog.tsx index caa2996da..4bfe2cc26 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ViewDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/ViewDialog.tsx @@ -1,22 +1,12 @@ import React from 'react'; -import { - Add as AddIcon, - Edit as EditIcon, - Delete as DeleteIcon, - FileCopy as FileCopyIcon, -} from '@mui/icons-material'; +import { Add as AddIcon, Edit as EditIcon, Delete as DeleteIcon, FileCopy as FileCopyIcon } from '@mui/icons-material'; -import { - TextField, -} from '@mui/material'; +import { TextField } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; import { store } from '@/Store'; -import { - deepClone, getNewWidgetId, - isGroup, pasteGroup, -} from '@/Utils/utils'; +import { deepClone, getNewWidgetId, isGroup, pasteGroup } from '@/Utils/utils'; import { useFocus } from '@/Utils'; import IODialog from '@/Components/IODialog'; import type { Project, SingleWidgetId, View } from '@iobroker/types-vis-2'; @@ -38,10 +28,10 @@ interface ViewDialogProps { setDialogParentId: (action: null) => void; } -const ViewDialog = (props: ViewDialogProps) => { +const ViewDialog = (props: ViewDialogProps): React.JSX.Element => { const inputField = useFocus(!!props.dialog && props.dialog !== 'delete', props.dialog === 'add'); - const deleteView = async () => { + const deleteView = async (): Promise => { const view = props.dialogView || props.selectedView; const project = deepClone(store.getState().visProject); delete project[view]; @@ -50,7 +40,7 @@ const ViewDialog = (props: ViewDialogProps) => { props.setDialog(null); // close dialog }; - const addView = async () => { + const addView = async (): Promise => { const project: Project = deepClone(store.getState().visProject); project[props.dialogName.trim()] = { name: props.dialogName, @@ -80,11 +70,11 @@ const ViewDialog = (props: ViewDialogProps) => { * * @param options the project to rename the references in and the old view name */ - const renameReferences = (options: RenameReferencesOptions) => { + const renameReferences = (options: RenameReferencesOptions): void => { const { project, oldViewName } = options; for (const [viewName, view] of Object.entries(project)) { - if (viewName === '___settings') { + if (viewName === '___settings') { continue; } @@ -106,7 +96,7 @@ const ViewDialog = (props: ViewDialogProps) => { } }; - const renameView = async () => { + const renameView = async (): Promise => { const oldViewName = props.dialogView || props.selectedView; const newViewName = props.dialogName.trim(); const project = deepClone(store.getState().visProject); @@ -122,7 +112,7 @@ const ViewDialog = (props: ViewDialogProps) => { props.dialogCallback?.cb(newViewName); }; - const copyView = async () => { + const copyView = async (): Promise => { const view = props.dialogView || props.selectedView; const project = deepClone(store.getState().visProject); project[props.dialogName] = { ...project[view], widgets: {}, activeWidgets: [] } as View; @@ -131,7 +121,10 @@ const ViewDialog = (props: ViewDialogProps) => { for (const [wid, widget] of Object.entries(originalWidgets)) { if (isGroup(widget)) { pasteGroup({ - group: widget, widgets: project[props.dialogName].widgets, groupMembers: originalWidgets, project, + group: widget, + widgets: project[props.dialogName].widgets, + groupMembers: originalWidgets, + project, }); } else if (!widget.groupid) { const newWid = getNewWidgetId(project); @@ -192,30 +185,34 @@ const ViewDialog = (props: ViewDialogProps) => { return null; } - return { - props.setDialog(null); - props.setDialogView(null); - props.setDialogParentId(null); - }} - ActionIcon={DialogIcon || null} - action={dialogActions[props.dialog]} - actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} - actionDisabled={dialogDisabled} - > - {props.dialog === 'delete' ? null : props.setDialogName(e.target.value)} - />} - ; + return ( + { + props.setDialog(null); + props.setDialogView(null); + props.setDialogParentId(null); + }} + ActionIcon={DialogIcon || null} + action={dialogActions[props.dialog]} + actionColor={props.dialog === 'delete' ? 'secondary' : 'primary'} + actionDisabled={dialogDisabled} + > + {props.dialog === 'delete' ? null : ( + props.setDialogName(e.target.value)} + /> + )} + + ); }; export default ViewDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/index.tsx b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/index.tsx index a170944c2..fc2d5ae46 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/index.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/ViewsManager/index.tsx @@ -17,11 +17,7 @@ import { BiImport } from 'react-icons/bi'; import { I18n, type ThemeName, type ThemeType } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; -import type { - View as ViewType, - AnyWidgetId, VisTheme, -} from '@iobroker/types-vis-2'; -import commonStyles from '@/Utils/styles'; +import type { View as ViewType, AnyWidgetId, VisTheme } from '@iobroker/types-vis-2'; import IODialog from '../../Components/IODialog'; import Folder from './Folder'; import Root from './Root'; @@ -31,11 +27,7 @@ import ImportDialog from './ImportDialog'; import FolderDialog from './FolderDialog'; import { DndPreview, isTouchDevice } from '../../Utils'; import { store } from '../../Store'; -import { - deepClone, getNewWidgetId, - hasViewAccess, isGroup, - pasteGroup, -} from '../../Utils/utils'; +import { deepClone, getNewWidgetId, hasViewAccess, isGroup, pasteGroup } from '../../Utils/utils'; const styles: Record = { dialog: { @@ -106,19 +98,19 @@ const ViewsManager: React.FC = props => { const { visProject, activeUser } = store.getState(); - const moveFolder = (id: string, parentId: string) => { + const moveFolder = (id: string, parentId: string): void => { const project = deepClone(visProject); project.___settings.folders.find(folder => folder.id === id).parentId = parentId; - props.changeProject(project); + void props.changeProject(project); }; - const moveView = (name: string, parentId: string) => { + const moveView = (name: string, parentId: string): void => { const project = deepClone(visProject); project[name].parentId = parentId; - props.changeProject(project); + void props.changeProject(project); }; - const importViewAction = (view: string, data: string) => { + const importViewAction = (view: string, data: string): void => { const project = deepClone(visProject); const viewObject: ViewType = JSON.parse(data); @@ -127,18 +119,23 @@ const ViewsManager: React.FC = props => { } if (!viewObject || !viewObject.settings || !viewObject.widgets) { - console.warn('Cannot import view: view is non-existing or missing one of the required properties "settings, widgets"'); + console.warn( + 'Cannot import view: view is non-existing or missing one of the required properties "settings, widgets"', + ); return; } const originalWidgets = deepClone(viewObject.widgets); - project[view] = { ...viewObject, widgets: {}, activeWidgets: [] }; + project[view] = { ...viewObject, widgets: {}, activeWidgets: [] }; for (const [wid, widget] of Object.entries(originalWidgets)) { if (isGroup(widget)) { pasteGroup({ - group: widget, widgets: project[view].widgets, groupMembers: originalWidgets, project, + group: widget, + widgets: project[view].widgets, + groupMembers: originalWidgets, + project, }); } else if (!widget.groupid) { const newWid = getNewWidgetId(project); @@ -147,173 +144,228 @@ const ViewsManager: React.FC = props => { } viewObject.name = view; - props.changeProject(project); + void props.changeProject(project); }; - const renderViews = (parentId?: string) => Object.keys(visProject) - .filter(name => !name.startsWith('___')) - .filter(name => (parentId ? visProject[name].parentId === parentId : !visProject[name].parentId)) - .sort((name1, name2) => (name1.toLowerCase().localeCompare(name2.toLowerCase()))) - .map((name, key) =>
    - -
    ); + const renderViews = (parentId?: string): React.JSX.Element[] => + Object.keys(visProject) + .filter(name => !name.startsWith('___')) + .filter(name => (parentId ? visProject[name].parentId === parentId : !visProject[name].parentId)) + .sort((name1, name2) => name1.toLowerCase().localeCompare(name2.toLowerCase())) + .map((name, key) => ( +
    + +
    + )); - const renderFolders = (parentId?: string) => { + const renderFolders = (parentId?: string): React.JSX.Element[] => { const folders = visProject.___settings.folders .filter(folder => (parentId ? folder.parentId === parentId : !folder.parentId)) .sort((folder1, folder2) => folder1.name.toLowerCase().localeCompare(folder2.name.toLowerCase())); - return folders.map((folder, key) =>
    -
    - + return folders.map((folder, key) => ( +
    +
    + +
    + {foldersCollapsed.includes(folder.id) ? null : ( +
    + {renderFolders(folder.id)} + {renderViews(folder.id)} +
    + )}
    - {foldersCollapsed.includes(folder.id) ? null :
    - {renderFolders(folder.id)} - {renderViews(folder.id)} -
    } -
    ); + )); }; - return -
    - - - {props.editMode ? - {props.editMode ? - props.showDialog('add', props.name, null, (newView: string) => { - newView && props.onClose(); - })} - > - - - : null} - {props.editMode ? - setImportDialog('')} size="small"> - - - : null} - {props.editMode ? - { - setFolderDialog('add'); - setFolderDialogName(''); - setFolderDialogParentId(null); - }} + return ( + +
    + + + {props.editMode ? ( + - - - : null} - {props.editMode ? - { - const proj = deepClone(store.getState().visProject); + {props.editMode ? ( + + + props.showDialog('add', props.name, null, (newView: string) => { + newView && props.onClose(); + }) + } + > + + + + ) : null} + {props.editMode ? ( + + setImportDialog('')} + size="small" + > + + + + ) : null} + {props.editMode ? ( + + { + setFolderDialog('add'); + setFolderDialogName(''); + setFolderDialogParentId(null); + }} + > + + + + ) : null} + {props.editMode ? ( + + { + const proj = deepClone(store.getState().visProject); - const views = Object.keys(proj).filter(name => !name.startsWith('___')); + const views = Object.keys(proj).filter(name => !name.startsWith('___')); - for (const view of views) { - proj.___settings.openedViews.push(view); - } + for (const view of views) { + proj.___settings.openedViews.push(view); + } - await props.changeProject(proj, false); - }} - > - - - : null} - {props.editMode ? - { - const proj = deepClone(store.getState().visProject); - proj.___settings.openedViews = []; + await props.changeProject(proj, false); + }} + > + + + + ) : null} + {props.editMode ? ( + + { + const proj = deepClone(store.getState().visProject); + proj.___settings.openedViews = []; - await props.changeProject(proj, false); - }} - > - - - : null} - : null} -
    - {renderFolders()} - {renderViews()} - -
    -
    -
    - - {importDialog !== false ? setImportDialog(false)} - view={importDialog || ''} - importViewAction={importViewAction} - themeType={props.themeType} - /> : null} - {exportDialog !== false ? setExportDialog(false)} - view={exportDialog || ''} - themeType={props.themeType} - /> : null} -
    ; + await props.changeProject(proj, false); + }} + > + +
    +
    + ) : null} +
    + ) : null} +
    + {renderFolders()} + {renderViews()} + +
    +
    +
    + + {importDialog !== false ? ( + setImportDialog(false)} + view={importDialog || ''} + importViewAction={importViewAction} + themeType={props.themeType} + /> + ) : null} + {exportDialog !== false ? ( + setExportDialog(false)} + view={exportDialog || ''} + themeType={props.themeType} + /> + ) : null} +
    + ); }; -export default ViewsManager as React.FC; +export default ViewsManager; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/WidgetExportDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/WidgetExportDialog.tsx index ccf1db966..01f8b01ed 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/WidgetExportDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/WidgetExportDialog.tsx @@ -1,7 +1,6 @@ import { FileCopy as FileCopyIcon } from '@mui/icons-material'; -import type { ThemeType } from '@iobroker/adapter-react-v5'; -import { Utils, I18n } from '@iobroker/adapter-react-v5'; +import { Utils, I18n, type ThemeType } from '@iobroker/adapter-react-v5'; import type { AnyWidgetId, GroupWidgetId, Widget } from '@iobroker/types-vis-2'; import React from 'react'; @@ -65,27 +64,29 @@ const WidgetExportDialog: React.FC = props => { } } - return { - Utils.copyToClipboard(JSON.stringify(widgets, null, 2)); - props.onClose(); - window.alert(I18n.t('Copied to clipboard')); - }} - actionTitle="Copy to clipboard" - actionNoClose - ActionIcon={FileCopyIcon} - > - - ; + return ( + { + Utils.copyToClipboard(JSON.stringify(widgets, null, 2)); + props.onClose(); + window.alert(I18n.t('Copied to clipboard')); + }} + actionTitle="Copy to clipboard" + actionNoClose + ActionIcon={FileCopyIcon} + > + + + ); }; export default WidgetExportDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/WidgetFilterDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/WidgetFilterDialog.tsx index 4eb205e65..435dae80f 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/WidgetFilterDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/WidgetFilterDialog.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState } from 'react'; import { - Button, Checkbox, + Button, + Checkbox, ListItemAvatar, ListItemButton, Dialog, @@ -9,15 +10,10 @@ import { DialogTitle, DialogActions, ListItemText, - Switch, Box, + Switch, + Box, } from '@mui/material'; -import { - Close, - FilterAlt, - LayersClear as Clear, - Visibility, - VisibilityOff, -} from '@mui/icons-material'; +import { Close, FilterAlt, LayersClear as Clear, Visibility, VisibilityOff } from '@mui/icons-material'; import { I18n } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; @@ -31,13 +27,17 @@ interface WidgetFilterDialogProps { } const WidgetFilterDialog: React.FC = props => { - const [filters, setFilters] = useState<{key: AnyWidgetId; count: number}[]>([]); - const [filterWidgets, setFilterWidgets] = useState(store.getState().visProject[props.selectedView].filterWidgets || []); - const [filterInvert, setFilterInvert] = useState(store.getState().visProject[props.selectedView].filterInvert || false); + const [filters, setFilters] = useState<{ key: AnyWidgetId; count: number }[]>([]); + const [filterWidgets, setFilterWidgets] = useState( + store.getState().visProject[props.selectedView].filterWidgets || [], + ); + const [filterInvert, setFilterInvert] = useState( + store.getState().visProject[props.selectedView].filterInvert || false, + ); useEffect(() => { // Collect all filters of all widgets - const _filters: {key: AnyWidgetId; count: number}[] = []; + const _filters: { key: AnyWidgetId; count: number }[] = []; const widgets = store.getState().visProject[props.selectedView].widgets; Object.values(widgets).forEach(widget => { if (widget.data && widget.data.filterkey) { @@ -55,128 +55,145 @@ const WidgetFilterDialog: React.FC = props => { setFilters(_filters); }, [store.getState().visProject, props.selectedView]); - return - {I18n.t('Set widgets filter for edit mode')} - - {filters.length > 1 ? : null} - {filters.length > 1 ? : null} -
    - - {I18n.t('Hide selected widgets')} - - - setFilterInvert(e.target.checked)} - /> - - - {I18n.t('Show only selected widgets')} - -
    -
    - {!filters.length ?
    - {I18n.t('To use it, define by some widget the filter key')} -
    : null} - {filters.map(filter => { - const _filterWidgets = [...filterWidgets]; - const pos = _filterWidgets.indexOf(filter.key); - if (pos !== -1) { - _filterWidgets.splice(pos, 1); - } else { - _filterWidgets.push(filter.key); - } - setFilterWidgets(_filterWidgets); + return ( + + {I18n.t('Set widgets filter for edit mode')} + + {filters.length > 1 ? ( + + ) : null} + {filters.length > 1 ? ( + + ) : null} +
    - - - - - {filter.key} - {filter.count > 1 ? + {I18n.t('Hide selected widgets')} + + + setFilterInvert(e.target.checked)} + /> + + + {I18n.t('Show only selected widgets')} + +
    +
    + {!filters.length ? ( +
    + {I18n.t('To use it, define by some widget the filter key')} +
    + ) : null} + {filters.map(filter => ( + { + const _filterWidgets = [...filterWidgets]; + const pos = _filterWidgets.indexOf(filter.key); + if (pos !== -1) { + _filterWidgets.splice(pos, 1); + } else { + _filterWidgets.push(filter.key); + } + setFilterWidgets(_filterWidgets); }} > - {I18n.t('%s widgets', filter.count)} - : null} - } - /> - )} -
    -
    - - {store.getState().visProject[props.selectedView].filterWidgets?.length ? : null} - - - -
    ; + + + + + {filter.key} + {filter.count > 1 ? ( + + {I18n.t('%s widgets', filter.count)} + + ) : null} + + } + /> +
    + ))} +
    +
    + + {store.getState().visProject[props.selectedView].filterWidgets?.length ? ( + + ) : null} + + + +
    + ); }; export default WidgetFilterDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/WidgetImportDialog.tsx b/packages/iobroker.vis-2/src/src/Toolbar/WidgetImportDialog.tsx index 92c6f0917..1cbcf03df 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/WidgetImportDialog.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/WidgetImportDialog.tsx @@ -1,22 +1,15 @@ import React, { useRef, useState } from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, DialogTitle, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import { Close as CloseIcon, ImportExport } from '@mui/icons-material'; import { I18n, type ThemeType } from '@iobroker/adapter-react-v5'; -import { - isGroup, getNewGroupId, getNewWidgetId, deepClone, -} from '@/Utils/utils'; +import { isGroup, getNewGroupId, getNewWidgetId, deepClone } from '@/Utils/utils'; import { useFocus } from '@/Utils'; import { store } from '@/Store'; -import type { - AnyWidgetId, - GroupWidget, GroupWidgetId, Project, Widget, -} from '@iobroker/types-vis-2'; +import type { AnyWidgetId, GroupWidget, GroupWidgetId, Project, Widget } from '@iobroker/types-vis-2'; import CustomAceEditor from '../Components/CustomAceEditor'; interface WidgetImportDialogProps { @@ -27,7 +20,7 @@ interface WidgetImportDialogProps { selectedGroup?: GroupWidgetId; } -const WidgetImportDialog = (props: WidgetImportDialogProps) => { +const WidgetImportDialog = (props: WidgetImportDialogProps): React.JSX.Element => { const [data, setData] = useState(''); const [error, setError] = useState(false); @@ -35,7 +28,7 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => { const editor = useRef(null); - const importWidgets = () => { + const importWidgets = (): void => { const { visProject } = store.getState(); const project = deepClone(visProject); const widgets: Widget[] = JSON.parse(data); @@ -60,7 +53,9 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => { newWidgets[newKey] = widget; if (widget.grouped && widget.groupid && newWidgets[widget.groupid]?.data?.members) { // find group - const pos = (newWidgets[widget.groupid] as GroupWidget).data.members.indexOf(widget._id as AnyWidgetId); + const pos = (newWidgets[widget.groupid] as GroupWidget).data.members.indexOf( + widget._id as AnyWidgetId, + ); if (pos !== -1) { (newWidgets[widget.groupid] as GroupWidget).data.members[pos] = newKey; } @@ -74,7 +69,7 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => { if (!isGroup(newWidgets[wid]) && props.selectedGroup !== undefined) { newWidgets[wid].grouped = true; newWidgets[wid].groupid = props.selectedGroup; - (project[props.selectedView].widgets[props.selectedGroup] as GroupWidget).data.members.push(wid as AnyWidgetId); + project[props.selectedView].widgets[props.selectedGroup].data.members.push(wid as AnyWidgetId); } }); @@ -83,58 +78,60 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => { props.changeProject(project); }; - return - {I18n.t('Import widgets')} - - { - editor.current = node; - inputField.current = node; - }} - value={data} - onChange={newValue => { - try { - newValue && JSON.parse(newValue); - setError(false); - } catch (e) { - setError(true); - } - setData(newValue); - }} - height={300} - /> - - - - - - ; + return ( + + {I18n.t('Import widgets')} + + { + editor.current = node; + inputField.current = node; + }} + value={data} + onChange={newValue => { + try { + newValue && JSON.parse(newValue); + setError(false); + } catch { + setError(true); + } + setData(newValue); + }} + height={300} + /> + + + + + + + ); }; export default WidgetImportDialog; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/Widgets.tsx b/packages/iobroker.vis-2/src/src/Toolbar/Widgets.tsx index 3729dcb31..9f6620b47 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/Widgets.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/Widgets.tsx @@ -1,17 +1,17 @@ import React, { useState, useMemo } from 'react'; import { - MdAlignHorizontalCenter, MdAlignHorizontalLeft, MdAlignHorizontalRight, - MdAlignVerticalBottom, MdAlignVerticalCenter, MdAlignVerticalTop, + MdAlignHorizontalCenter, + MdAlignHorizontalLeft, + MdAlignHorizontalRight, + MdAlignVerticalBottom, + MdAlignVerticalCenter, + MdAlignVerticalTop, } from 'react-icons/md'; import { CgArrowAlignH, CgArrowAlignV } from 'react-icons/cg'; import { AiOutlineColumnWidth, AiOutlineColumnHeight } from 'react-icons/ai'; -import { - BiImport, BiExport, BiCut, BiCopy, BiPaste, -} from 'react-icons/bi'; -import { - RiBringToFront, RiSendToBack, -} from 'react-icons/ri'; +import { BiImport, BiExport, BiCut, BiCopy, BiPaste } from 'react-icons/bi'; +import { RiBringToFront, RiSendToBack } from 'react-icons/ri'; import { Delete as DeleteIcon, FilterAlt as FilterIcon, @@ -28,8 +28,7 @@ import type { AnyWidgetId, GroupWidgetId, VisTheme } from '@iobroker/types-vis-2 import type Editor from '@/Editor'; import { store } from '../Store'; -import {ToolbarGroup, ToolbarItem} from './ToolbarItems'; -import ToolbarItems from './ToolbarItems'; +import ToolbarItems, { type ToolbarGroup, type ToolbarItem } from './ToolbarItems'; import { getWidgetTypes } from '../Vis/visWidgetsCatalog'; import WidgetImportDialog from './WidgetImportDialog'; import WidgetExportDialog from './WidgetExportDialog'; @@ -87,10 +86,11 @@ const Widgets: React.FC = props => { const widgetTypes = getWidgetTypes(); const widgets = project[props.selectedView].widgets; - const shownWidgets = Object.keys(widgets) - .filter((widget: AnyWidgetId) => (props.selectedGroup ? - widgets[widget].groupid === props.selectedGroup || widget === props.selectedGroup : - !widgets[widget].groupid)); + const shownWidgets = Object.keys(widgets).filter((widget: AnyWidgetId) => + props.selectedGroup + ? widgets[widget].groupid === props.selectedGroup || widget === props.selectedGroup + : !widgets[widget].groupid, + ); return { name: 'Widgets', @@ -107,42 +107,42 @@ const Widgets: React.FC = props => { type: 'multiselect', name: I18n.t('Active widget(s) from %s', shownWidgets.length), doNotTranslateName: true, - items: shownWidgets - .map((widgetId: AnyWidgetId) => { - const tpl = widgets[widgetId].tpl; - const widgetType = widgetTypes.find(w => w.name === tpl); - let widgetLabel = widgetType?.title || ''; - let widgetColor = widgetType ? widgetType.setColor : '#FF0000'; - if (widgetType?.label) { - widgetLabel = I18n.t(widgetType.label); - } + items: shownWidgets.map((widgetId: AnyWidgetId) => { + const tpl = widgets[widgetId].tpl; + const widgetType = widgetTypes.find(w => w.name === tpl); + let widgetLabel = widgetType?.title || ''; + let widgetColor = widgetType ? widgetType.setColor : '#FF0000'; + if (widgetType?.label) { + widgetLabel = I18n.t(widgetType.label); + } - // remove legacy stuff - widgetLabel = widgetLabel.split(' w.set === setLabel && w.setLabel); - if (widgetWithSetLabel) { - widgetColor = widgetWithSetLabel.setColor; - setLabel = I18n.t(widgetWithSetLabel.setLabel); - } + let setLabel = widgetType?.set; + if (widgetType?.setLabel) { + setLabel = I18n.t(widgetType.setLabel); + } else if (setLabel) { + const widgetWithSetLabel = widgetTypes.find(w => w.set === setLabel && w.setLabel); + if (widgetWithSetLabel) { + widgetColor = widgetWithSetLabel.setColor; + setLabel = I18n.t(widgetWithSetLabel.setLabel); } + } - let widgetIcon = widgetType ? (widgetType.preview || '') : 'icon/question.svg'; - if (widgetIcon.startsWith(' + } + let name; + if (widgets[widgetId] && widgets[widgetId].data?.name) { + name = ( + {widgets[widgetId].data?.name} = props => { > {`[${widgetId}]`} - ; - } else { - name = widgetId; - } + + ); + } else { + name = widgetId; + } - let subName = widgetType ? `(${setLabel} - ${tpl === '_tplGroup' ? I18n.t('group') : widgetLabel})` : tpl; + let subName = widgetType + ? `(${setLabel} - ${tpl === '_tplGroup' ? I18n.t('group') : widgetLabel})` + : tpl; - if (widgets[widgetId].marketplace) { - subName = `${widgets[widgetId].marketplace.name} (${I18n.t('version')} ${widgets[widgetId].marketplace.version})`; - } + if (widgets[widgetId].marketplace) { + subName = `${widgets[widgetId].marketplace.name} (${I18n.t('version')} ${widgets[widgetId].marketplace.version})`; + } - return { - name, - subName, - value: widgetId, - color: widgetColor, - icon: widgetIcon.startsWith('<') ? '' : widgetIcon, - }; - }), + return { + name, + subName, + value: widgetId, + color: widgetColor, + icon: widgetIcon.startsWith('<') ? '' : widgetIcon, + }; + }), width: 240, value: props.selectedWidgets, onAction: value => props.setSelectedWidgets(value as AnyWidgetId[]), @@ -183,7 +186,9 @@ const Widgets: React.FC = props => { type: 'icon-button', Icon: DeleteIcon, name: 'Delete widgets', - disabled: !props.selectedWidgets.length || (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), + disabled: + !props.selectedWidgets.length || + (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), onAction: () => props.deleteWidgets(), } as ToolbarItem, ], @@ -192,7 +197,9 @@ const Widgets: React.FC = props => { type: 'icon-button', Icon: FileCopyIcon, name: 'Clone widget', - disabled: !props.selectedWidgets.length || (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), + disabled: + !props.selectedWidgets.length || + (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), onAction: () => props.cloneWidgets(), } as ToolbarItem, ], @@ -200,33 +207,40 @@ const Widgets: React.FC = props => { { type: 'divider' }, - [[ - { - type: 'icon-button', - Icon: BiCut, - name: 'Cut', - size: 'normal', - disabled: !props.selectedWidgets.length || (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), - onAction: () => props.cutWidgets(), - } as ToolbarItem, - { - type: 'icon-button', - Icon: BiCopy, - name: 'Copy', - size: 'normal', - disabled: !props.selectedWidgets.length || (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), - onAction: () => props.copyWidgets(), - } as ToolbarItem, - ], [ - { - type: 'icon-button', - Icon: BiPaste, - name: 'Paste', - size: 'normal', - disabled: !Object.keys(props.widgetsClipboard.widgets).length, - onAction: () => props.pasteWidgets(), - } as ToolbarItem, - ]], + [ + [ + { + type: 'icon-button', + Icon: BiCut, + name: 'Cut', + size: 'normal', + disabled: + !props.selectedWidgets.length || + (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), + onAction: () => props.cutWidgets(), + } as ToolbarItem, + { + type: 'icon-button', + Icon: BiCopy, + name: 'Copy', + size: 'normal', + disabled: + !props.selectedWidgets.length || + (props.selectedGroup && props.selectedWidgets.includes(props.selectedGroup)), + onAction: () => props.copyWidgets(), + } as ToolbarItem, + ], + [ + { + type: 'icon-button', + Icon: BiPaste, + name: 'Paste', + size: 'normal', + disabled: !Object.keys(props.widgetsClipboard.widgets).length, + onAction: () => props.pasteWidgets(), + } as ToolbarItem, + ], + ], { type: 'icon-button', Icon: UndoIcon, @@ -245,143 +259,153 @@ const Widgets: React.FC = props => { { type: 'divider' }, - window.innerWidth > 1410 ? [ + window.innerWidth > 1410 + ? [ + [ + { + type: 'icon-button', + Icon: MdAlignHorizontalLeft, + name: 'Align horizontal/left', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('left'), + }, + { + type: 'icon-button', + Icon: MdAlignVerticalTop, + name: 'Align vertical/top', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('top'), + }, + { + type: 'icon-button', + Icon: MdAlignHorizontalCenter, + name: 'Align horizontal/center', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('horizontal-center'), + }, + { + type: 'icon-button', + Icon: CgArrowAlignH, + name: 'Align horizontal/equal', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('horizontal-equal'), + }, + { + type: 'icon-button', + Icon: AiOutlineColumnWidth, + name: 'Align width. Press more time to get the desired width.', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('width'), + }, + { + type: 'icon-button', + Icon: RiBringToFront, + name: 'Bring to front', + size: 'normal', + disabled: !props.selectedWidgets.length, + onAction: () => props.orderWidgets('front'), + }, + ] as ToolbarItem[], + [ + { + type: 'icon-button', + Icon: MdAlignHorizontalRight, + name: 'Align horizontal/right', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('right'), + }, + { + type: 'icon-button', + Icon: MdAlignVerticalBottom, + name: 'Align vertical/bottom', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('bottom'), + }, + { + type: 'icon-button', + Icon: MdAlignVerticalCenter, + name: 'Align vertical/center', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('vertical-center'), + }, + { + type: 'icon-button', + Icon: CgArrowAlignV, + name: 'Align vertical/equal', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('vertical-equal'), + }, + { + type: 'icon-button', + Icon: AiOutlineColumnHeight, + name: 'Align height. Press more time to get the desired height.', + size: 'normal', + disabled: props.selectedWidgets.length < 2, + onAction: () => props.alignWidgets('height'), + }, + { + type: 'icon-button', + Icon: RiSendToBack, + name: 'Send to back', + size: 'normal', + disabled: !props.selectedWidgets.length, + onAction: () => props.orderWidgets('back'), + }, + ] as ToolbarItem[], + ] + : null, + window.innerWidth > 1410 ? { type: 'divider' } : null, + [ [ { type: 'icon-button', - Icon: MdAlignHorizontalLeft, - name: 'Align horizontal/left', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('left'), - }, - { - type: 'icon-button', - Icon: MdAlignVerticalTop, - name: 'Align vertical/top', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('top'), - }, - { - type: 'icon-button', - Icon: MdAlignHorizontalCenter, - name: 'Align horizontal/center', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('horizontal-center'), - }, - { - type: 'icon-button', - Icon: CgArrowAlignH, - name: 'Align horizontal/equal', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('horizontal-equal'), - }, - { - type: 'icon-button', - Icon: AiOutlineColumnWidth, - name: 'Align width. Press more time to get the desired width.', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('width'), - }, - { - type: 'icon-button', - Icon: RiBringToFront, - name: 'Bring to front', - size: 'normal', - disabled: !props.selectedWidgets.length, - onAction: () => props.orderWidgets('front'), - }, - ] as ToolbarItem[], + Icon: OpenInNewIcon, + name: 'Lock dragging', + selected: props.lockDragging, + onAction: () => props.toggleLockDragging(), + } as ToolbarItem, + ], [ { type: 'icon-button', - Icon: MdAlignHorizontalRight, - name: 'Align horizontal/right', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('right'), - }, - { - type: 'icon-button', - Icon: MdAlignVerticalBottom, - name: 'Align vertical/bottom', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('bottom'), - }, - { - type: 'icon-button', - Icon: MdAlignVerticalCenter, - name: 'Align vertical/center', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('vertical-center'), - }, - { - type: 'icon-button', - Icon: CgArrowAlignV, - name: 'Align vertical/equal', - size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('vertical-equal'), - }, + Icon: props.widgetHint === 'hide' ? VisibilityOffIcon : VisibilityIcon, + color: props.widgetHint === 'light' ? 'white' : 'black', + name: `Toggle widget hint (${props.widgetHint})`, + onAction: () => props.toggleWidgetHint(), + } as ToolbarItem, + ], + ], + { type: 'divider' }, + [ + [ { type: 'icon-button', - Icon: AiOutlineColumnHeight, - name: 'Align height. Press more time to get the desired height.', + Icon: BiImport, + name: 'Import widgets', size: 'normal', - disabled: props.selectedWidgets.length < 2, - onAction: () => props.alignWidgets('height'), - }, + disabled: !props.editMode, + onAction: () => setImportDialog(true), + } as ToolbarItem, + ], + [ { type: 'icon-button', - Icon: RiSendToBack, - name: 'Send to back', + Icon: BiExport, + name: 'Export widgets', size: 'normal', disabled: !props.selectedWidgets.length, - onAction: () => props.orderWidgets('back'), - }, - ] as ToolbarItem[], - ] : null, - window.innerWidth > 1410 ? { type: 'divider' } : null, - [ - [{ - type: 'icon-button', - Icon: OpenInNewIcon, - name: 'Lock dragging', - selected: props.lockDragging, - onAction: () => props.toggleLockDragging(), - } as ToolbarItem], - [{ - type: 'icon-button', - Icon: props.widgetHint === 'hide' ? VisibilityOffIcon : VisibilityIcon, - color: props.widgetHint === 'light' ? 'white' : 'black', - name: `Toggle widget hint (${props.widgetHint})`, - onAction: () => props.toggleWidgetHint(), - } as ToolbarItem], - ], - { type: 'divider' }, - [ - [{ - type: 'icon-button', - Icon: BiImport, - name: 'Import widgets', - size: 'normal', - disabled: !props.editMode, - onAction: () => setImportDialog(true), - } as ToolbarItem], - [{ - type: 'icon-button', - Icon: BiExport, - name: 'Export widgets', - size: 'normal', - disabled: !props.selectedWidgets.length, - onAction: () => setExportDialog(true), - } as ToolbarItem], + onAction: () => setExportDialog(true), + } as ToolbarItem, + ], ], ], } as ToolbarGroup; @@ -408,35 +432,43 @@ const Widgets: React.FC = props => { return null; } - return <> - - {importDialog ? setImportDialog(false)} - changeProject={props.changeProject} - selectedView={props.selectedView} - selectedGroup={props.selectedGroup} - themeType={props.themeType} - /> : null} - {exportDialog ? setExportDialog(false)} - widgets={store.getState().visProject[props.selectedView].widgets} - selectedWidgets={props.selectedWidgets} - themeType={props.themeType} - /> : null} - {filterDialog ? setFilterDialog(false)} - changeProject={props.changeProject} - selectedView={props.selectedView} - /> : null} - ; + return ( + <> + + {importDialog ? ( + setImportDialog(false)} + changeProject={props.changeProject} + selectedView={props.selectedView} + selectedGroup={props.selectedGroup} + themeType={props.themeType} + /> + ) : null} + {exportDialog ? ( + setExportDialog(false)} + widgets={store.getState().visProject[props.selectedView].widgets} + selectedWidgets={props.selectedWidgets} + themeType={props.themeType} + /> + ) : null} + {filterDialog ? ( + setFilterDialog(false)} + changeProject={props.changeProject} + selectedView={props.selectedView} + /> + ) : null} + + ); }; export default Widgets; diff --git a/packages/iobroker.vis-2/src/src/Toolbar/index.tsx b/packages/iobroker.vis-2/src/src/Toolbar/index.tsx index 4535b6157..942fdad52 100644 --- a/packages/iobroker.vis-2/src/src/Toolbar/index.tsx +++ b/packages/iobroker.vis-2/src/src/Toolbar/index.tsx @@ -1,11 +1,6 @@ import React, { useRef, useState } from 'react'; -import { - IconButton, - Tooltip, - Menu as DropMenu, - MenuItem as DropMenuItem, Box, -} from '@mui/material'; +import { IconButton, Tooltip, Menu as DropMenu, MenuItem as DropMenuItem, Box } from '@mui/material'; import { Close as CloseIcon, @@ -20,20 +15,17 @@ import { Save as SaveIcon, } from '@mui/icons-material'; -import type { - LegacyConnection, - ThemeName, - ThemeType, -} from '@iobroker/adapter-react-v5'; import { Icon, Utils, I18n, ToggleThemeMenu, + type LegacyConnection, + type ThemeName, + type ThemeType, } from '@iobroker/adapter-react-v5'; import type Editor from '@/Editor'; -import commonStyles from '@/Utils/styles'; import type { AnyWidgetId, GroupWidgetId, VisTheme } from '@iobroker/types-vis-2'; import Views from './Views'; import Widgets from './Widgets'; @@ -70,9 +62,7 @@ const styles: Record = { pt: '4px', pb: '4px', }, - heightButton: { - - }, + heightButton: {}, saveIcon: (theme: VisTheme): React.CSSProperties => ({ animation: `blink 2000ms ${theme.transitions.easing.easeInOut}`, color: theme.palette.primary.main, @@ -145,94 +135,125 @@ const Toolbar = (props: ToolbarProps): React.JSX.Element => { const lang = I18n.getLanguage(); - const runtimeURL = window.location.pathname.endsWith('/edit.html') ? - `./?${props.projectName}#${props.selectedView}` - : - `?${props.projectName}&runtime=true#${props.selectedView}`; + const runtimeURL = window.location.pathname.endsWith('/edit.html') + ? `./?${props.projectName}#${props.selectedView}` + : `?${props.projectName}&runtime=true#${props.selectedView}`; - const onReload = () => { + const onReload = (): void => { window.localStorage.setItem('Vis.lastCommand', 'reload'); setLastCommand('reload'); - props.socket.setState(`${props.adapterName}.${props.instance}.control.instance`, { val: '*', ack: true }); - props.socket.setState(`${props.adapterName}.${props.instance}.control.data`, { val: null, ack: true }); - props.socket.setState(`${props.adapterName}.${props.instance}.control.command`, { val: 'refresh', ack: true }); + void props.socket.setState(`${props.adapterName}.${props.instance}.control.instance`, { val: '*', ack: true }); + void props.socket.setState(`${props.adapterName}.${props.instance}.control.data`, { val: null, ack: true }); + void props.socket.setState(`${props.adapterName}.${props.instance}.control.command`, { + val: 'refresh', + ack: true, + }); setRight(false); }; - const dropMenu = setRight(false)} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - // getContentAnchorEl={null} - > - { - window.localStorage.setItem('Vis.lastCommand', 'close'); - setLastCommand('close'); - setRight(false); - window.location.href = runtimeURL; - }} + const dropMenu = ( + setRight(false)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + // getContentAnchorEl={null} > - - {I18n.t('Close editor')} - - { - window.localStorage.setItem('Vis.lastCommand', 'open'); - setLastCommand('open'); - setRight(false); - window.open(runtimeURL, 'vis-2.runtime'); - }} - > - - {I18n.t('Open runtime in new window')} - - - - {I18n.t('Reload all runtimes')} - - ; + { + window.localStorage.setItem('Vis.lastCommand', 'close'); + setLastCommand('close'); + setRight(false); + window.location.href = runtimeURL; + }} + > + + {I18n.t('Close editor')} + + { + window.localStorage.setItem('Vis.lastCommand', 'open'); + setLastCommand('open'); + setRight(false); + window.open(runtimeURL, 'vis-2.runtime'); + }} + > + + {I18n.t('Open runtime in new window')} + + + + {I18n.t('Reload all runtimes')} + + + ); let heightButton; if (props.toolbarHeight === 'narrow') { - heightButton = - props.setToolbarHeight('veryNarrow')} + heightButton = ( + - - - ; + props.setToolbarHeight('veryNarrow')} + > + + + + ); } else if (props.toolbarHeight === 'veryNarrow') { - heightButton = - props.setToolbarHeight('full')} + heightButton = ( + - - - ; + props.setToolbarHeight('full')} + > + + + + ); } else { - heightButton = - props.setToolbarHeight('narrow')} + heightButton = ( + - - - ; + props.setToolbarHeight('narrow')} + > + + + + ); } - const currentUser = props.currentUser ? + const currentUser = props.currentUser ? (
    - {props.currentUser?.common?.icon ? : } + {props.currentUser?.common?.icon ? ( + + ) : ( + + )} - { Utils.getObjectNameFromObj(props.currentUser, lang) } + {Utils.getObjectNameFromObj(props.currentUser, lang)} - { props.socket.isSecure - ? + {props.socket.isSecure ? ( + { @@ -248,35 +269,62 @@ const Toolbar = (props: ToolbarProps): React.JSX.Element => { - : null} + ) : null}
    - : - null; + ) : null; let lastCommandButton; if (lastCommand === 'close') { - lastCommandButton = - window.location.href = runtimeURL}> - - - ; + lastCommandButton = ( + + (window.location.href = runtimeURL)} + > + + + + ); } else if (lastCommand === 'open') { - lastCommandButton = - window.open(runtimeURL, 'vis-2.runtime')}> - - - ; + lastCommandButton = ( + + window.open(runtimeURL, 'vis-2.runtime')} + > + + + + ); } else if (lastCommand === 'reload') { - lastCommandButton = - - - - ; + lastCommandButton = ( + + + + + + ); } - return - - -
    - {props.needSave ? : null} - {props.toolbarHeight === 'veryNarrow' ? currentUser : null} - {heightButton} - + +
    + {props.needSave ? ( + + ) : null} + {props.toolbarHeight === 'veryNarrow' ? currentUser : null} + {heightButton} + + {lastCommandButton} + setRight(!right)} + size="small" + > + + + {dropMenu} +
    + {props.toolbarHeight !== 'veryNarrow' ? currentUser : null} + {props.toolbarHeight === 'full' && props.version ? ( + v{props.version} + ) : null} +
    + + - {lastCommandButton} - setRight(!right)} size="small"> - - - {dropMenu} -
    - {props.toolbarHeight !== 'veryNarrow' ? currentUser : null} - {props.toolbarHeight === 'full' && props.version ? - v - {props.version} - : null} -
    - - - - + + +
    - ; + ); }; export default Toolbar; diff --git a/packages/iobroker.vis-2/src/src/Utils.tsx b/packages/iobroker.vis-2/src/src/Utils.tsx index 99e55a5d8..ac1fd7335 100644 --- a/packages/iobroker.vis-2/src/src/Utils.tsx +++ b/packages/iobroker.vis-2/src/src/Utils.tsx @@ -1,12 +1,12 @@ -import React, { useEffect, useRef } from 'react'; +import React, { type MutableRefObject, useEffect, useRef } from 'react'; import { usePreview } from 'react-dnd-preview'; import type { Timer } from '@iobroker/types-vis-2'; -export const DndPreview = () => { +export const DndPreview = (): React.JSX.Element | null => { const preview = usePreview(); const display = preview.display; // TODO: How to fix this? - const { item, style } = (preview as unknown as { item: { preview: React.JSX.Element }; style: React.CSSProperties }); + const { item, style } = preview as unknown as { item: { preview: React.JSX.Element }; style: React.CSSProperties }; if (!display) { return null; } @@ -20,8 +20,12 @@ export function mobileCheck(): boolean { let check = false; const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; if ( - /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i - .test(userAgent.substr(0, 4)) + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series([46])0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + userAgent, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br([ev])w|bumb|bw-([nu])|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do([cp])o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly([-_])|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-([mpt])|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c([- _agpst])|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac([ \-/])|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja([tv])a|jbro|jemu|jigs|kddi|keji|kgt([ /])|klon|kpt |kwc-|kyo([ck])|le(no|xi)|lg( g|\/([klu])|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t([- ov])|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30([02])|n50([025])|n7(0([01])|10)|ne(([cm])-|on|tf|wf|wg|wt)|nok([6i])|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan([adt])|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c([-01])|47|mc|nd|ri)|sgh-|shar|sie([-m])|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel([im])|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c([- ])|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( + userAgent.substr(0, 4), + ) ) { check = true; } @@ -35,13 +39,15 @@ export function isTouchDevice(): boolean { if (!mobileCheck()) { return false; } - return (('ontouchstart' in window) - || (navigator.maxTouchPoints > 0) + return ( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || // @ts-expect-error this is only for Internet Explorer 10, if unsupported remove it. - || (navigator.msMaxTouchPoints > 0)); + navigator.msMaxTouchPoints > 0 + ); } -export const useFocus = (open: boolean, select: boolean, isAce = false) => { +export const useFocus = (open: boolean, select: boolean, isAce = false): MutableRefObject => { // TODO: find correct type const inputField = useRef(null); diff --git a/packages/iobroker.vis-2/src/src/Utils/styles.tsx b/packages/iobroker.vis-2/src/src/Utils/styles.tsx index 2555fcef4..c847fb842 100644 --- a/packages/iobroker.vis-2/src/src/Utils/styles.tsx +++ b/packages/iobroker.vis-2/src/src/Utils/styles.tsx @@ -15,7 +15,6 @@ const commonStyles: { iconFolder: React.CSSProperties; fieldContentSliderClear: React.CSSProperties; fieldHelpText: React.CSSProperties; - tooltip: React.CSSProperties; iconPreview: React.CSSProperties; } = { backgroundClass: { @@ -82,12 +81,7 @@ const commonStyles: { float: 'right', fontSize: 16, }, - tooltip: { - pointerEvents: 'none', - }, - iconPreview: { - - }, + iconPreview: {}, }; export default commonStyles; diff --git a/packages/iobroker.vis-2/src/src/Utils/usePrevious.tsx b/packages/iobroker.vis-2/src/src/Utils/usePrevious.tsx index ce3db297e..896d9f7f9 100644 --- a/packages/iobroker.vis-2/src/src/Utils/usePrevious.tsx +++ b/packages/iobroker.vis-2/src/src/Utils/usePrevious.tsx @@ -1,10 +1,13 @@ import { useEffect, useRef } from 'react'; -function usePrevious(value: unknown) { +function usePrevious(value: string[]): string[] { const ref = useRef(); + useEffect(() => { ref.current = value; // assign the value of ref to the argument }, [value]); // this code will run when the value of 'value' changes + return ref.current; // in the end, return the current ref value. } + export default usePrevious; diff --git a/packages/iobroker.vis-2/src/src/Utils/utils.tsx b/packages/iobroker.vis-2/src/src/Utils/utils.tsx index 4aa54c644..dced4fb81 100644 --- a/packages/iobroker.vis-2/src/src/Utils/utils.tsx +++ b/packages/iobroker.vis-2/src/src/Utils/utils.tsx @@ -4,8 +4,13 @@ import type React from 'react'; import { store } from '@/Store'; import type { - GroupWidget, Widget, Project, SingleWidget, - SingleWidgetId, GroupWidgetId, AnyWidgetId, + GroupWidget, + Widget, + Project, + SingleWidget, + SingleWidgetId, + GroupWidgetId, + AnyWidgetId, Permissions, } from '@iobroker/types-vis-2'; @@ -64,12 +69,12 @@ export function deepClone>(object: T): T { * @param project current project * @param offset offset if multiple widgets are created and not yet in project */ -export function getNewWidgetIdNumber(isWidgetGroup: boolean, project: Project, offset = 0): number { +export function getNewWidgetIdNumber(isWidgetGroup: boolean, project: Project, offset = 0): number { const widgets: string[] = []; project = project || store.getState().visProject; - Object.keys(project).forEach(view => - project[view].widgets && Object.keys(project[view].widgets).forEach(widget => - widgets.push(widget))); + Object.keys(project).forEach( + view => project[view].widgets && Object.keys(project[view].widgets).forEach(widget => widgets.push(widget)), + ); let newKey = 1; widgets.forEach(name => { const matches = isWidgetGroup ? name.match(/^g([0-9]+)$/) : name.match(/^w([0-9]+)$/); @@ -86,17 +91,19 @@ export function getNewWidgetIdNumber(isWidgetGroup: boolean, project: Project, o /** * Get new widget id from the project + * * @param project project to determine next widget id for * @param offset offset, if multiple widgets are created and not yet in the project */ export function getNewWidgetId(project: Project, offset = 0): SingleWidgetId { const newKey = getNewWidgetIdNumber(false, project, offset); - return `w${(newKey).toString().padStart(6, '0')}`; + return `w${newKey.toString().padStart(6, '0')}`; } /** * Get new group id from the project + * * @param project project to determine next group id for * @param offset offset, if multiple groups are created and not yet in the project */ @@ -136,16 +143,14 @@ interface CopyGroupOptions extends CopyWidgetOptions { * @param options selected group, widgets and offset information */ export function pasteSingleWidget(options: CopySingleWidgetOptions): SingleWidgetId { - const { - widgets, offset, project, widget, selectedGroup, - } = options; + const { widgets, offset, project, widget, selectedGroup } = options; const newKey = getNewWidgetId(project, offset); if (selectedGroup && isGroup(widgets[selectedGroup])) { widget.grouped = true; widget.groupid = selectedGroup; - (widgets[selectedGroup] as GroupWidget).data.members.push(newKey); + widgets[selectedGroup].data.members.push(newKey); } widgets[newKey] = widget; @@ -160,9 +165,7 @@ export function pasteSingleWidget(options: CopySingleWidgetOptions): SingleWidge * @param options group, widgets and offset information */ export function pasteGroup(options: CopyGroupOptions): GroupWidgetId { - const { - widgets, group, groupMembers, offset, project, - } = options; + const { widgets, group, groupMembers, offset, project } = options; const newGroupId = getNewGroupId(project, offset ?? 0); for (let i = 0; i < group.data.members.length; i++) { @@ -188,7 +191,7 @@ export function pasteGroup(options: CopyGroupOptions): GroupWidgetId { */ export function unsyncMultipleWidgets(project: Project): Project { project = deepClone(project || store.getState().visProject); - for (const [viewName, view] of Object.entries(project)) { + for (const [viewName, view] of Object.entries(project)) { if (viewName === '___settings') { continue; } @@ -232,7 +235,7 @@ export function hasProjectAccess(options: CheckAccessOptions): boolean { return !editMode && permissions.read; } -interface CheckViewAccessOptions extends CheckAccessOptions{ +interface CheckViewAccessOptions extends CheckAccessOptions { /** Name of the view */ view: string; } @@ -248,9 +251,7 @@ interface CheckWidgetAccessOptions extends CheckViewAccessOptions { * @param options project, view, user and mode information */ export function hasViewAccess(options: CheckViewAccessOptions): boolean { - const { - project, user, editMode, view, - } = options; + const { project, user, editMode, view } = options; const permissions = project[view]?.settings?.permissions?.[user] ?? DEFAULT_PERMISSIONS; @@ -267,9 +268,7 @@ export function hasViewAccess(options: CheckViewAccessOptions): boolean { * @param options project, view, widget, user and mode information */ export function hasWidgetAccess(options: CheckWidgetAccessOptions): boolean { - const { - project, user, editMode, view, wid, - } = options; + const { project, user, editMode, view, wid } = options; const permissions = project[view]?.widgets[wid]?.permissions?.[user] ?? DEFAULT_PERMISSIONS; diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBar.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBar.tsx index b639f2fd3..fa9c73971 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBar.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBar.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { RxRenderWidgetProps } from '@iobroker/types-vis-2'; +import type { RxRenderWidgetProps, RxWidgetInfo } from '@iobroker/types-vis-2'; import VisRxWidget from '@/Vis/visRxWidget'; type RxData = { @@ -18,57 +18,59 @@ export default class BasicBar extends VisRxWidget { /** * Returns the widget info which is rendered in the edit mode */ - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplValueFloatBar', visSet: 'basic', visName: 'Bar', visPrev: 'widgets/basic/img/Prev_ValueFloatBar.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'oid', - type: 'id', - }, - { - name: 'min', - type: 'number', - default: 0, - }, - { - name: 'max', - type: 'number', - default: 100, - }, - { - name: 'orientation', - type: 'select', - default: 'horizontal', - options: [ - { value: 'horizontal', label: 'horizontal' }, - { value: 'vertical', label: 'vertical' }, - ], - }, - { - name: 'color', - type: 'color', - default: 'blue', - }, - { - name: 'border', - type: 'text', - }, - { - name: 'shadow', - type: 'text', - }, - { - name: 'reverse', - type: 'checkbox', - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'oid', + type: 'id', + }, + { + name: 'min', + type: 'number', + default: 0, + }, + { + name: 'max', + type: 'number', + default: 100, + }, + { + name: 'orientation', + type: 'select', + default: 'horizontal', + options: [ + { value: 'horizontal', label: 'horizontal' }, + { value: 'vertical', label: 'vertical' }, + ], + }, + { + name: 'color', + type: 'color', + default: 'blue', + }, + { + name: 'border', + type: 'text', + }, + { + name: 'shadow', + type: 'text', + }, + { + name: 'reverse', + type: 'checkbox', + }, + ], + }, + ], visDefaultStyle: { width: 200, height: 130, @@ -80,7 +82,7 @@ export default class BasicBar extends VisRxWidget { * Enables calling widget info on the class instance itself */ // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { return BasicBar.getWidgetInfo(); } @@ -112,7 +114,9 @@ export default class BasicBar extends VisRxWidget { const max = this.state.rxData.max || this.state.rxData.max === 0 ? Number(this.state.rxData.max) : 100; let val = parseFloat(this.state.values[`${this.state.rxData.oid}.val`]) || 0; val = (val - min) / (max - min); - return (this.state.rxData.border) ? (`calc(${Math.round(val * 100)}% - ${this.extractWidth(this.state.rxData.border as string, 2)})`) : (`${Math.round(val * 100)}%`); + return this.state.rxData.border + ? `calc(${Math.round(val * 100)}% - ${this.extractWidth(this.state.rxData.border, 2)})` + : `${Math.round(val * 100)}%`; } /** @@ -129,23 +133,35 @@ export default class BasicBar extends VisRxWidget { style = { height: this.getCalc() }; if (this.state.rxData.reverse) { style = { - ...style, left: 0, position: 'absolute', bottom: '0', + ...style, + left: 0, + position: 'absolute', + bottom: '0', }; } if (this.state.rxData.border) { - style = { ...style, border: this.state.rxData.border, width: `calc(100% - ${this.extractWidth(this.state.rxData.border, 2)}` }; + style = { + ...style, + border: this.state.rxData.border, + width: `calc(100% - ${this.extractWidth(this.state.rxData.border, 2)}`, + }; } } else { style = { width: this.getCalc() }; if (this.state.rxData.reverse) { style = { - ...style, float: 'right', + ...style, + float: 'right', }; } if (this.state.rxData.border) { - style = { ...style, border: this.state.rxData.border, height: `calc(100% - ${this.extractWidth(this.state.rxData.border, 2)}` }; + style = { + ...style, + border: this.state.rxData.border, + height: `calc(100% - ${this.extractWidth(this.state.rxData.border, 2)}`, + }; } } @@ -157,9 +173,14 @@ export default class BasicBar extends VisRxWidget { style = { ...style, backgroundColor: this.state.rxData.color }; } - return
    -
    + return ( +
    +
    -
    ; + ); } } diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBulb.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBulb.tsx index 5f08fac97..d744cb965 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBulb.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicBulb.tsx @@ -1,6 +1,6 @@ import React from 'react'; import VisRxWidget from '@/Vis/visRxWidget'; -import type { RxRenderWidgetProps } from '@iobroker/types-vis-2'; +import type { RxRenderWidgetProps, RxWidgetInfo } from '@iobroker/types-vis-2'; import { NOTHING_SELECTED } from '@/Utils/utils'; type RxData = { @@ -23,73 +23,75 @@ export default class BasicBulb extends VisRxWidget { * Enables calling widget info on the class instance itself */ // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { return BasicBulb.getWidgetInfo(); } /** * Returns the widget info which is rendered in the edit mode */ - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplBulbOnOffCtrl', visSet: 'basic', visName: 'Bulb on/off', visPrev: 'widgets/basic/img/Prev_BulbOnOffCtrl.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'oid', - type: 'id', - }, - { - name: 'min', - }, - { - name: 'max', - }, - { - name: 'icon_off', - type: 'image', - default: 'img/bulb_off.png', - }, - { - name: 'icon_on', - type: 'image', - default: 'img/bulb_on.png', - }, - { - name: 'readOnly', - type: 'checkbox', - }, - ], - }, - { - name: 'ccontrol', - fields: [ - { - name: 'urlTrue', - }, - { - name: 'urlFalse', - }, - { - name: 'oidTrue', - type: 'id', - }, - { - name: 'oidFalse', - type: 'id', - }, - { - name: 'oidTrueValue', - }, - { - name: 'oidFalseValue', - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'oid', + type: 'id', + }, + { + name: 'min', + }, + { + name: 'max', + }, + { + name: 'icon_off', + type: 'image', + default: 'img/bulb_off.png', + }, + { + name: 'icon_on', + type: 'image', + default: 'img/bulb_on.png', + }, + { + name: 'readOnly', + type: 'checkbox', + }, + ], + }, + { + name: 'ccontrol', + fields: [ + { + name: 'urlTrue', + }, + { + name: 'urlFalse', + }, + { + name: 'oidTrue', + type: 'id', + }, + { + name: 'oidFalse', + type: 'id', + }, + { + name: 'oidTrueValue', + }, + { + name: 'oidFalseValue', + }, + ], + }, + ], }; } @@ -103,13 +105,8 @@ export default class BasicBulb extends VisRxWidget { let val = this.state.values[`${this.state.rxData.oid}.val`]; - const { - oidTrue, urlTrue, oid, min, - max, oidTrueValue, oidFalseValue, - } = this.state.rxData; - let { - urlFalse, oidFalse, - } = this.state.rxData; + const { oidTrue, urlTrue, oid, min, max, oidTrueValue, oidFalseValue } = this.state.rxData; + let { urlFalse, oidFalse } = this.state.rxData; let finalMin: string | boolean = min ?? ''; let finalMax: string | boolean = max ?? ''; @@ -143,7 +140,7 @@ export default class BasicBulb extends VisRxWidget { if (finalMin === '' || finalMin === 'false' || finalMin === null) { finalMin = false; - } else if (finalMax === '' || finalMax === 'true' || finalMax === null) { + } else if (finalMax === '' || finalMax === 'true' || finalMax === null) { finalMax = true; } @@ -174,7 +171,9 @@ export default class BasicBulb extends VisRxWidget { } if (typeof oidFalseValueFinal === 'string') { const f = parseFloat(oidFalseValueFinal); - if (f.toString() === oidFalseValueFinal) oidFalseValueFinal = f; + if (f.toString() === oidFalseValueFinal) { + oidFalseValueFinal = f; + } } this.props.context.setValue(oidFalse, oidFalseValueFinal); } @@ -187,33 +186,35 @@ export default class BasicBulb extends VisRxWidget { this.props.context.socket.getRawSocket().emit('httpGet', urlFalse); } } - } else if ((finalMin === '' && + } else if ( + (finalMin === '' && (val === null || val === '' || val === undefined || val === false || val === 'false')) || - (finalMin !== '' && finalMin === val)) { + (finalMin !== '' && finalMin === val) + ) { typeof oid === 'string' && this.props.context.setValue(oid, finalMax !== '' ? finalMax : true); - } else - if ((finalMax === '' && (val === true || val === 'true')) || (finalMax !== '' && val === finalMax)) { - typeof oid === 'string' && this.props.context.setValue(oid, finalMin !== '' ? finalMin : false); - } else if (typeof oid === 'string') { - val = parseFloat(val); - if (finalMin !== '' && - finalMax !== '' && - (typeof finalMax === 'number' || typeof finalMax === 'string') && - (typeof finalMin === 'number' || typeof finalMin === 'string') - ) { - if (val >= (parseFloat(finalMax) - parseFloat(finalMin)) / 2) { - val = finalMin; - } else { - val = finalMax; - } - } else if (val >= 0.5) { - val = 0; + } else if ((finalMax === '' && (val === true || val === 'true')) || (finalMax !== '' && val === finalMax)) { + typeof oid === 'string' && this.props.context.setValue(oid, finalMin !== '' ? finalMin : false); + } else if (typeof oid === 'string') { + val = parseFloat(val); + if ( + finalMin !== '' && + finalMax !== '' && + (typeof finalMax === 'number' || typeof finalMax === 'string') && + (typeof finalMin === 'number' || typeof finalMin === 'string') + ) { + if (val >= (parseFloat(finalMax) - parseFloat(finalMin)) / 2) { + val = finalMin; } else { - val = 1; + val = finalMax; } - - this.props.context.setValue(oid, val); + } else if (val >= 0.5) { + val = 0; + } else { + val = 1; } + + this.props.context.setValue(oid, val); + } } /** @@ -237,7 +238,18 @@ export default class BasicBulb extends VisRxWidget { return val === min; } - if (val === undefined || val === null || val === false || val === 'false' || val === 'FALSE' || val === 'False' || val === 'OFF' || val === 'Off' || val === 'off' || val === '') { + if ( + val === undefined || + val === null || + val === false || + val === 'false' || + val === 'FALSE' || + val === 'False' || + val === 'OFF' || + val === 'Off' || + val === 'off' || + val === '' + ) { return true; } if (val === '0' || val === 0) { @@ -259,7 +271,7 @@ export default class BasicBulb extends VisRxWidget { super.renderWidgetBody(props); const val = this.state.values[`${this.state.rxData.oid}.val`]; - const isOff = this.isFalse(val, this.state.rxData.min as string, this.state.rxData.max as string); + const isOff = this.isFalse(val, this.state.rxData.min, this.state.rxData.max); let src; @@ -269,8 +281,17 @@ export default class BasicBulb extends VisRxWidget { src = this.state.rxData.icon_on || 'img/bulb_on.png'; } - return
    this.toggle()}> - tplBulbOnOffCtrl -
    ; + return ( +
    this.toggle()} + > + tplBulbOnOffCtrl +
    + ); } } diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicFilterDropdown.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicFilterDropdown.tsx index a9f9563ed..31eb42346 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicFilterDropdown.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicFilterDropdown.tsx @@ -15,18 +15,15 @@ import React, { useState } from 'react'; -import { - Button, ButtonGroup, - FormControl, - InputLabel, MenuItem, - Select, -} from '@mui/material'; +import { Button, ButtonGroup, FormControl, InputLabel, MenuItem, Select } from '@mui/material'; import { Edit } from '@mui/icons-material'; import { I18n, Icon } from '@iobroker/adapter-react-v5'; import type { - RxRenderWidgetProps, RxWidgetInfo, WidgetData, + RxRenderWidgetProps, + RxWidgetInfo, + WidgetData, RxWidgetInfoAttributesField, RxWidgetInfoCustomComponentProperties, RxWidgetInfoCustomComponentContext, @@ -57,14 +54,17 @@ type RxData = { dropdownVariant?: 'standard' | 'outlined' | 'filled'; buttonsVariant?: 'outlined' | 'contained' | 'text'; dropdownSmall?: boolean; -} +}; -function processFilter(filters: string[]) { +function processFilter(filters: string[]): void { const len = filters.length; for (let f = 0; f < len; f++) { const filter = filters[f]; if (filter.includes(',')) { - const ff = filter.split(',').map(t => t.trim()).filter(t => t); + const ff = filter + .split(',') + .map(t => t.trim()) + .filter(t => t); const first = ff.shift(); if (first) { filters[f] = first; @@ -73,7 +73,10 @@ function processFilter(filters: string[]) { } } } else if (filter.includes(';')) { - const ff = filter.split(';').map(t => t.trim()).filter(t => t); + const ff = filter + .split(';') + .map(t => t.trim()) + .filter(t => t); const first = ff.shift(); if (first) { filters[f] = first; @@ -91,177 +94,186 @@ interface ItemsEditorProps { context: RxWidgetInfoCustomComponentContext; } -const ItemsEditor = (props: ItemsEditorProps) => { +const ItemsEditor = (props: ItemsEditorProps): React.JSX.Element => { const [open, setOpen] = useState(false); let items = props.data.items; // convert data from "filters" to "items" if (open && !items && props.data.filters) { - items = props.data.filters.split(';') + items = props.data.filters + .split(';') .map((item: string) => ({ label: item.trim(), value: item.trim() })) .filter((item: Item) => item.value); } if (open && typeof items === 'string') { try { items = JSON.parse(items); - } catch (e) { + } catch { items = []; } } - return <> - - {open ? { - if (newItems) { - const data = JSON.parse(JSON.stringify(props.data)); - data.items = JSON.stringify(newItems); - props.setData(data); - } - setOpen(false); - }} - /> : null} - ; + return ( + <> + + {open ? ( + { + if (newItems) { + const data = JSON.parse(JSON.stringify(props.data)); + data.items = JSON.stringify(newItems); + props.setData(data); + } + setOpen(false); + }} + /> + ) : null} + + ); }; class BasicFilterDropdown extends VisRxWidget { private editMode: boolean | undefined; - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplFilterDropdown', visSet: 'basic', visName: 'filter - dropdown', visPrev: 'widgets/basic/img/Prev_FilterDropdown.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'items', - label: 'editor', - type: 'custom', - noBinding: true, - component: ( - _field: RxWidgetInfoAttributesField, - data: WidgetData, - onDataChange: (newData: WidgetData) => void, - props: RxWidgetInfoCustomComponentProperties, - ) => , - default: '[]', - }, - { - name: 'type', - label: 'Type', - type: 'select', - options: [ - { - value: 'dropdown', - label: 'basic_filter_type_dropdown', - }, - { - value: 'horizontal_buttons', - label: 'basic_filter_type_horizontal_buttons', - }, - { - value: 'vertical_buttons', - label: 'basic_filter_type_vertical_buttons', - }, - ], - default: 'horizontal_buttons', - }, - { - name: 'widgetTitle', - label: 'name', - hidden: 'data.type !== "dropdown"', - }, - { - name: 'autoFocus', - type: 'checkbox', - hidden: 'data.type !== "dropdown"', - }, - { - name: 'multiple', - label: 'basic_filter_multiple', - type: 'checkbox', - }, - { - name: 'noAllOption', - label: 'basic_no_all_option', - type: 'checkbox', - }, - { - name: 'noFilterText', - label: 'basic_no_filter_text', - type: 'text', - hidden: '!!data.noAllOption', - }, - { - name: 'dropdownVariant', - label: 'jqui_variant', - type: 'select', - noTranslation: true, - options: [ - { - value: 'standard', - label: 'standard', - }, - { - value: 'outlined', - label: 'outlined', - }, - { - value: 'filled', - label: 'filled', - }, - ], - default: 'standard', - hidden: 'data.type !== "dropdown"', - }, - { - name: 'buttonsVariant', - label: 'jqui_variant', - type: 'select', - noTranslation: true, - options: [ - { - value: 'outlined', - label: 'outlined', - }, - { - value: 'contained', - label: 'contained', - }, - { - value: 'text', - label: 'text', - }, - ], - default: 'outlined', - hidden: 'data.type === "dropdown"', - }, - { - name: 'dropdownSmall', - label: 'basic_small', - default: false, - type: 'checkbox', - hidden: 'data.type !== "dropdown"', - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'items', + label: 'editor', + type: 'custom', + noBinding: true, + component: ( + _field: RxWidgetInfoAttributesField, + data: WidgetData, + onDataChange: (newData: WidgetData) => void, + props: RxWidgetInfoCustomComponentProperties, + ) => ( + + ), + default: '[]', + }, + { + name: 'type', + label: 'Type', + type: 'select', + options: [ + { + value: 'dropdown', + label: 'basic_filter_type_dropdown', + }, + { + value: 'horizontal_buttons', + label: 'basic_filter_type_horizontal_buttons', + }, + { + value: 'vertical_buttons', + label: 'basic_filter_type_vertical_buttons', + }, + ], + default: 'horizontal_buttons', + }, + { + name: 'widgetTitle', + label: 'name', + hidden: 'data.type !== "dropdown"', + }, + { + name: 'autoFocus', + type: 'checkbox', + hidden: 'data.type !== "dropdown"', + }, + { + name: 'multiple', + label: 'basic_filter_multiple', + type: 'checkbox', + }, + { + name: 'noAllOption', + label: 'basic_no_all_option', + type: 'checkbox', + }, + { + name: 'noFilterText', + label: 'basic_no_filter_text', + type: 'text', + hidden: '!!data.noAllOption', + }, + { + name: 'dropdownVariant', + label: 'jqui_variant', + type: 'select', + noTranslation: true, + options: [ + { + value: 'standard', + label: 'standard', + }, + { + value: 'outlined', + label: 'outlined', + }, + { + value: 'filled', + label: 'filled', + }, + ], + default: 'standard', + hidden: 'data.type !== "dropdown"', + }, + { + name: 'buttonsVariant', + label: 'jqui_variant', + type: 'select', + noTranslation: true, + options: [ + { + value: 'outlined', + label: 'outlined', + }, + { + value: 'contained', + label: 'contained', + }, + { + value: 'text', + label: 'text', + }, + ], + default: 'outlined', + hidden: 'data.type === "dropdown"', + }, + { + name: 'dropdownSmall', + label: 'basic_small', + default: false, + type: 'checkbox', + hidden: 'data.type !== "dropdown"', + }, + ], + }, + ], // visWidgetLabel: 'value_string', // Label of widget visDefaultStyle: { width: 200, @@ -271,11 +283,11 @@ class BasicFilterDropdown extends VisRxWidget { } // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { - return BasicFilterDropdown.getWidgetInfo() as RxWidgetInfo; + getWidgetInfo(): RxWidgetInfo { + return BasicFilterDropdown.getWidgetInfo(); } - async componentDidMount() { + componentDidMount(): void { super.componentDidMount(); // apply default filter if (!this.props.editMode) { @@ -304,147 +316,200 @@ class BasicFilterDropdown extends VisRxWidget { } renderDropdown(items: Item[]): React.JSX.Element { - const viewsActiveFilter: string[] = (this.props.viewsActiveFilter[this.props.view] as string[]) || []; + const viewsActiveFilter: string[] = this.props.viewsActiveFilter[this.props.view] || []; let value; if (this.state.rxData.multiple) { value = viewsActiveFilter; } else { value = viewsActiveFilter[0] || ''; } - return - {this.state.rxData.widgetTitle ? {this.state.rxData.widgetTitle} : null} - { + let filter: string | string[] = e.target.value === '_' ? [] : e.target.value; + if (typeof filter === 'string') { + filter = filter.split(','); + } + if (filter.includes('') || filter.includes('_')) { + filter = []; + } + processFilter(filter); + const view = this.props.askView('getViewClass'); + view.onCommand('changeFilter', { filter }); + }} + renderValue={val => { + const option = items.find(item => item.value === val); + if (option) { + return {option.label}; + } + if (val === '_' || !val) { + return this.state.rxData.noAllOption ? '' : I18n.t('basic_no_filter'); + } + return val; + }} + multiple={!!this.state.rxData.multiple} + autoFocus={!!this.state.rxData.autoFocus} + > + {this.state.rxData.noAllOption ? null : ( + + {this.state.rxData.noFilterText || I18n.t('basic_no_filter')} + + )} + {items.map(option => { + let image = option.icon; + if (!image && option.image) { + image = option.image; + if (image.startsWith('_PRJ_NAME/')) { + image = image.replace( + '_PRJ_NAME/', + `../${this.props.context.adapterName}.${this.props.context.instance}/${this.props.context.projectName}/`, + ); + } + } + + return ( + + {image ? ( + + ) : null} + {option.label} + + ); + })} + + + ); + } + + renderButtons(items: Item[]): React.JSX.Element { + const viewsActiveFilter: string[] = this.props.viewsActiveFilter[this.props.view] || []; + return ( + - {this.state.rxData.noAllOption ? null : {this.state.rxData.noFilterText || I18n.t('basic_no_filter')}} + {this.state.rxData.noAllOption ? null : ( + + )} {items.map(option => { let image = option.icon; if (!image && option.image) { image = option.image; if (image.startsWith('_PRJ_NAME/')) { - image = image.replace('_PRJ_NAME/', `../${this.props.context.adapterName}.${this.props.context.instance}/${this.props.context.projectName}/`); + image = image.replace( + '_PRJ_NAME/', + `../${this.props.context.adapterName}.${this.props.context.instance}/${this.props.context.projectName}/`, + ); } } - - return - {image ? : null} - {option.label} - ; + return ( + + ); })} - - ; - } - - renderButtons(items: Item[]): React.JSX.Element { - const viewsActiveFilter: string[] = (this.props.viewsActiveFilter[this.props.view] as string[]) || []; - return - {this.state.rxData.noAllOption ? null : } - {items.map(option => { - let image = option.icon; - if (!image && option.image) { - image = option.image; - if (image.startsWith('_PRJ_NAME/')) { - image = image.replace('_PRJ_NAME/', `../${this.props.context.adapterName}.${this.props.context.instance}/${this.props.context.projectName}/`); - } - } - return ; - })} - ; + + ); } - getItems() { + getItems(): { label: string; value: string }[] { let items = this.state.data.items; // convert data from "filters" to "items" if (!items && this.state.data.filters) { - items = this.state.data.filters.split(';') + items = this.state.data.filters + .split(';') .map((item: string) => ({ label: item.trim(), value: item.trim() })) .filter((item: Item) => item.value); } if (typeof items === 'string') { try { items = JSON.parse(items); - } catch (e) { + } catch { items = []; } } diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicGroup.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicGroup.tsx index 6abb7211b..09be77fdd 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicGroup.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicGroup.tsx @@ -23,21 +23,20 @@ import type { WidgetData, RxWidgetInfoAttributesField, Project, - SingleWidget, SingleWidgetId, RxWidgetInfoAttributesFieldHelp, RxWidgetInfoGroup, - RxWidgetInfoAttributesFieldSelect, RxWidgetInfoAttributesFieldText, + RxWidgetInfoAttributesFieldSelect, + RxWidgetInfoAttributesFieldText, } from '@iobroker/types-vis-2'; import VisView from '@/Vis/visView'; -// eslint-disable-next-line import/no-cycle import VisRxWidget, { type VisRxWidgetState } from '../../visRxWidget'; type RxData = { [key: string]: string | boolean | number; -} +}; interface BasicGroupState extends VisRxWidgetState { mounted: boolean; @@ -49,7 +48,7 @@ interface RxWidgetInfoGroupReadWrite extends RxWidgetInfoGroup { } class BasicGroup extends VisRxWidget { - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: '_tplGroup', visSet: 'basic', @@ -61,118 +60,120 @@ class BasicGroup extends VisRxWidget { fields: [], }, ] as RxWidgetInfoGroupReadWrite[], - _visAttrs: (data: Record, views: Project, view: string) => { // this will be dynamically rendered in src/src/Attributes/Widget/index.jsx => Widget class - // Try to find all fields where could be groupAttrX - const listOfWidgets: SingleWidgetId[] = data.members; - const attributes: string[] = []; - - listOfWidgets.forEach(wid => { - const widgetData: WidgetData | undefined = (views[view].widgets[wid] as SingleWidget)?.data; - widgetData && Object.keys(widgetData).forEach(attr => { - if (typeof widgetData[attr] === 'string') { - let ms = widgetData[attr].match(/(groupAttr\d+)+?/g); - if (ms) { - ms.forEach((m: string) => !attributes.includes(m) && attributes.push(m)); - } - - // new style: {html}, {myAttr}, ... - ms = widgetData[attr].match(/%([-_a-zA-Z\d]+)+?%/g); - if (ms) { - ms.forEach((m: string) => { - const _attr = m.substring(1, m.length - 1); - !attributes.includes(_attr) && attributes.push(_attr); - }); - } + }; + } + + componentDidMount(): void { + super.componentDidMount(); + this.setState({ mounted: true }); + } + + static _visAttrs(data: Record, views: Project, view: string): RxWidgetInfoGroupReadWrite[] { + // this will be dynamically rendered in src/src/Attributes/Widget/index.jsx => Widget class + // Try to find all fields where could be groupAttrX + const listOfWidgets: SingleWidgetId[] = data.members; + const attributes: string[] = []; + + listOfWidgets.forEach(wid => { + const widgetData: WidgetData | undefined = views[view].widgets[wid]?.data; + widgetData && + Object.keys(widgetData).forEach(attr => { + if (typeof widgetData[attr] === 'string') { + let ms = widgetData[attr].match(/(groupAttr\d+)+?/g); + if (ms) { + ms.forEach((m: string) => !attributes.includes(m) && attributes.push(m)); + } + + // new style: {html}, {myAttr}, ... + ms = widgetData[attr].match(/%([-_a-zA-Z\d]+)+?%/g); + if (ms) { + ms.forEach((m: string) => { + const _attr = m.substring(1, m.length - 1); + !attributes.includes(_attr) && attributes.push(_attr); + }); } - }); + } }); + }); - const common: RxWidgetInfoGroupReadWrite = { - name: 'common', - fields: [ - { - name: 'group_hint', - label: 'group_hint', - type: 'help', - text: 'group_help', - } as RxWidgetInfoAttributesFieldHelp, - ], - }; - - const objects: RxWidgetInfoGroupReadWrite = { - name: 'objects', - label: 'group_fields', - fields: [], - }; - - const groupFields = [common, objects]; - - for (let i = 0; i < attributes.length; i++) { - const attrName = attributes[i]; - const num = attrName.startsWith('groupAttr') ? parseInt(attrName.substring(9), 10) : 0; - // Add to common - common.fields.push({ - name: attrName, - title: data[`attrName_${attrName}`] || data[`attrName${num}`] || attrName, - type: data[`attrType_${attrName}`] || data[`attrType${num}`] || '', - } as RxWidgetInfoAttributesField); - // add to objects - objects.fields.push({ - name: `attrName_${attrName}`, - title: `${I18n.t('group_attrName')} [${attrName}]`, - type: 'text', - } as RxWidgetInfoAttributesFieldText); - objects.fields.push({ - name: `attrType_${attrName}`, - title: `${I18n.t('group_attrType')} [${attrName}]`, - type: 'select', - noTranslation: true, - options: [ - '', - 'text', - 'checkbox', - 'number', - 'html', - 'image', - 'icon', - 'icon64', - 'id', - 'color', - 'views', - 'widget', - 'history', - 'password', - 'fontname', - 'widget', - 'groups', - 'class', - 'filters', - 'json', - ], - } as RxWidgetInfoAttributesFieldSelect); - } + const common: RxWidgetInfoGroupReadWrite = { + name: 'common', + fields: [ + { + name: 'group_hint', + label: 'group_hint', + type: 'help', + text: 'group_help', + } as RxWidgetInfoAttributesFieldHelp, + ], + }; - return groupFields; - }, + const objects: RxWidgetInfoGroupReadWrite = { + name: 'objects', + label: 'group_fields', + fields: [], }; - } - async componentDidMount() { - await super.componentDidMount(); - this.setState({ mounted: true }); + const groupFields = [common, objects]; + + for (let i = 0; i < attributes.length; i++) { + const attrName = attributes[i]; + const num = attrName.startsWith('groupAttr') ? parseInt(attrName.substring(9), 10) : 0; + // Add to common + common.fields.push({ + name: attrName, + title: data[`attrName_${attrName}`] || data[`attrName${num}`] || attrName, + type: data[`attrType_${attrName}`] || data[`attrType${num}`] || '', + } as RxWidgetInfoAttributesField); + // add to objects + objects.fields.push({ + name: `attrName_${attrName}`, + title: `${I18n.t('group_attrName')} [${attrName}]`, + type: 'text', + } as RxWidgetInfoAttributesFieldText); + objects.fields.push({ + name: `attrType_${attrName}`, + title: `${I18n.t('group_attrType')} [${attrName}]`, + type: 'select', + noTranslation: true, + options: [ + '', + 'text', + 'checkbox', + 'number', + 'html', + 'image', + 'icon', + 'icon64', + 'id', + 'color', + 'views', + 'widget', + 'history', + 'password', + 'fontname', + 'widget', + 'groups', + 'class', + 'filters', + 'json', + ], + } as RxWidgetInfoAttributesFieldSelect); + } + + return groupFields; } // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { // render dynamical attributes - const info = BasicGroup.getWidgetInfo(); - info.visAttrs = info._visAttrs( + const info: RxWidgetInfo = BasicGroup.getWidgetInfo(); + info.visAttrs = BasicGroup._visAttrs( this.props.context.views[this.props.view].widgets[this.props.id].data, this.props.context.views, this.props.view, ); - delete info._visAttrs; - return info as RxWidgetInfo; + return info; } renderWidgetBody(props: RxRenderWidgetProps): React.JSX.Element | null { @@ -193,16 +194,16 @@ class BasicGroup extends VisRxWidget { groupWidgets.sort((a, b) => { const widgetA = context.views[this.props.view].widgets[a]; const widgetB = context.views[this.props.view].widgets[b]; - const isRelativeA = widgetA?.style && ( - widgetA.style.position === 'relative' || - widgetA.style.position === 'static' || - widgetA.style.position === 'sticky' - ); - const isRelativeB = widgetB?.style && ( - widgetB.style.position === 'relative' || - widgetB.style.position === 'static' || - widgetB.style.position === 'sticky' - ); + const isRelativeA = + widgetA?.style && + (widgetA.style.position === 'relative' || + widgetA.style.position === 'static' || + widgetA.style.position === 'sticky'); + const isRelativeB = + widgetB?.style && + (widgetB.style.position === 'relative' || + widgetB.style.position === 'static' || + widgetB.style.position === 'sticky'); if (isRelativeA && isRelativeB) { return 0; } @@ -218,11 +219,11 @@ class BasicGroup extends VisRxWidget { return null; } - const isRelative = _widget.style && ( - _widget.style.position === 'relative' || - _widget.style.position === 'static' || - _widget.style.position === 'sticky' - ); + const isRelative = + _widget.style && + (_widget.style.position === 'relative' || + _widget.style.position === 'static' || + _widget.style.position === 'sticky'); // use the same container for relative and absolute widgets (props.refService) return VisView.getOneWidget(index, _widget, { @@ -237,7 +238,7 @@ class BasicGroup extends VisRxWidget { view: this.props.view, isRelative, askView: this.props.askView, - refParent: props.refService as React.RefObject, + refParent: props.refService, relativeWidgetOrder: groupWidgets, viewsActiveFilter: this.props.viewsActiveFilter, customSettings: this.props.customSettings, diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtml.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtml.tsx index 41f52cd97..2d7d6eb2c 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtml.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtml.tsx @@ -15,8 +15,7 @@ import React from 'react'; -// eslint-disable-next-line import/no-cycle -import type { GetRxDataFromWidget, RxRenderWidgetProps } from '@iobroker/types-vis-2'; +import type { GetRxDataFromWidget, RxRenderWidgetProps, RxWidgetInfo } from '@iobroker/types-vis-2'; import VisRxWidget from '../../visRxWidget'; import DangerousHtmlWithScript from '../Utils/DangerousHtmlWithScript'; @@ -26,28 +25,30 @@ type RxData = GetRxDataFromWidget; class BasicHtml extends VisRxWidget { interval: ReturnType | null = null; - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplHtml', visSet: 'basic', visName: 'HTML', visPrev: 'widgets/basic/img/Prev_HTML.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'html', - type: 'html', - }, - { - name: 'refreshInterval', - type: 'slider', - min: 0, - max: 180000, - step: 100, - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'html', + type: 'html', + }, + { + name: 'refreshInterval', + type: 'slider', + min: 0, + max: 180000, + step: 100, + }, + ], + }, + ], // visWidgetLabel: 'value_string', // Label of widget visDefaultStyle: { width: 200, @@ -57,43 +58,56 @@ class BasicHtml extends VisRxWidget { } // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { return BasicHtml.getWidgetInfo(); } - async componentWillUnmount() { - this.interval && clearInterval(this.interval); - this.interval = null; + componentWillUnmount(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } super.componentWillUnmount(); } - async componentDidMount() { + componentDidMount(): void { super.componentDidMount(); if (parseInt(this.state.rxData.refreshInterval as unknown as string, 10)) { - this.interval = setInterval(() => this.forceUpdate(), parseInt(this.state.rxData.refreshInterval as unknown as string, 10)); + this.interval = setInterval( + () => this.forceUpdate(), + parseInt(this.state.rxData.refreshInterval as unknown as string, 10), + ); } } - onRxDataChanged(prevRxData: typeof this.state.rxData) { + onRxDataChanged(prevRxData: typeof this.state.rxData): void { super.onRxDataChanged(prevRxData); if (this.interval) { clearInterval(this.interval); this.interval = null; } if (parseInt(this.state.rxData.refreshInterval as unknown as string, 10)) { - this.interval = setInterval(() => this.forceUpdate(), parseInt(this.state.rxData.refreshInterval as unknown as string, 10)); + this.interval = setInterval( + () => this.forceUpdate(), + parseInt(this.state.rxData.refreshInterval as unknown as string, 10), + ); } } /** * Renders the widget - * - * @return {Element} */ - renderWidgetBody(props: RxRenderWidgetProps) { + renderWidgetBody(props: RxRenderWidgetProps): React.JSX.Element { super.renderWidgetBody(props); - return ; + return ( + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtmlNav.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtmlNav.tsx index 5510aafe3..51ba70386 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtmlNav.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicHtmlNav.tsx @@ -15,7 +15,7 @@ import React from 'react'; -import type { GetRxDataFromWidget, RxRenderWidgetProps } from '@iobroker/types-vis-2'; +import type { GetRxDataFromWidget, RxRenderWidgetProps, RxWidgetInfo } from '@iobroker/types-vis-2'; import VisRxWidget from '@/Vis/visRxWidget'; import DangerousHtmlWithScript from '../Utils/DangerousHtmlWithScript'; @@ -24,32 +24,34 @@ import DangerousHtmlWithScript from '../Utils/DangerousHtmlWithScript'; type RxData = GetRxDataFromWidget; class BasicHtmlNav extends VisRxWidget { - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplHtmlNav', visSet: 'basic', visName: 'HTML navigation', visPrev: 'widgets/basic/img/Prev_HTMLnavigation.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'html', - type: 'html', - }, - { - name: 'nav_view', - type: 'views', - }, - { - name: 'sub_view', - label: 'basic_sub_view', - type: 'text', - tooltip: 'sub_view_tooltip', - hidden: '!data.nav_view', - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'html', + type: 'html', + }, + { + name: 'nav_view', + type: 'views', + }, + { + name: 'sub_view', + label: 'basic_sub_view', + type: 'text', + tooltip: 'sub_view_tooltip', + hidden: '!data.nav_view', + }, + ], + }, + ], // visWidgetLabel: 'value_string', // Label of widget visDefaultStyle: { width: 200, @@ -59,20 +61,21 @@ class BasicHtmlNav extends VisRxWidget { } // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { return BasicHtmlNav.getWidgetInfo(); } - onNavigate = () => { + onNavigate = (): void => { if (this.state.rxData.nav_view) { - this.props.context.changeView(this.state.rxData.nav_view, this.state.rxData.sub_view || undefined); + this.props.context.changeView( + (this.state.rxData.nav_view || '').toString(), + this.state.rxData.sub_view ? this.state.rxData.sub_view.toString() : undefined, + ); } }; /** * Renders the widget - * - * @return {Element} */ renderWidgetBody(props: RxRenderWidgetProps): React.JSX.Element { super.renderWidgetBody(props); @@ -83,13 +86,15 @@ class BasicHtmlNav extends VisRxWidget { props.style.height = 130; } - return ; + return ( + + ); } } diff --git a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicIFrame.tsx b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicIFrame.tsx index 8ff5beff7..8d444530f 100644 --- a/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicIFrame.tsx +++ b/packages/iobroker.vis-2/src/src/Vis/Widgets/Basic/BasicIFrame.tsx @@ -1,9 +1,6 @@ import React from 'react'; -import type { - RxRenderWidgetProps, - VisBaseWidgetProps, -} from '@iobroker/types-vis-2'; +import type { RxRenderWidgetProps, RxWidgetInfo, VisBaseWidgetProps } from '@iobroker/types-vis-2'; import VisRxWidget from '@/Vis/visRxWidget'; type RxData = { @@ -37,59 +34,61 @@ export default class BasicIFrame extends VisRxWidget { /** * Returns the widget info which is rendered in the edit mode */ - static getWidgetInfo() { + static getWidgetInfo(): RxWidgetInfo { return { id: 'tplIFrame', visSet: 'basic', visName: 'iFrame', visPrev: 'widgets/basic/img/Prev_iFrame.png', - visAttrs: [{ - name: 'common', - fields: [ - { - name: 'src', - type: 'url', - }, - { - name: 'refreshInterval', - tooltip: 'basic_refreshInterval_tooltip', - type: 'slider', - min: 0, - max: 180000, - step: 100, - default: 0, - }, - { - name: 'noSandbox', - type: 'checkbox', - }, - { - name: 'refreshOnWakeUp', - type: 'checkbox', - }, - { - name: 'refreshOnViewChange', - type: 'checkbox', - }, - { - name: 'refreshWithNoQuery', - type: 'checkbox', - }, - { - name: 'scrollX', - type: 'checkbox', - }, - { - name: 'scrollY', - type: 'checkbox', - }, - { - name: 'seamless', - type: 'checkbox', - default: true, - }, - ], - }], + visAttrs: [ + { + name: 'common', + fields: [ + { + name: 'src', + type: 'url', + }, + { + name: 'refreshInterval', + tooltip: 'basic_refreshInterval_tooltip', + type: 'slider', + min: 0, + max: 180000, + step: 100, + default: 0, + }, + { + name: 'noSandbox', + type: 'checkbox', + }, + { + name: 'refreshOnWakeUp', + type: 'checkbox', + }, + { + name: 'refreshOnViewChange', + type: 'checkbox', + }, + { + name: 'refreshWithNoQuery', + type: 'checkbox', + }, + { + name: 'scrollX', + type: 'checkbox', + }, + { + name: 'scrollY', + type: 'checkbox', + }, + { + name: 'seamless', + type: 'checkbox', + default: true, + }, + ], + }, + ], // visWidgetLabel: 'value_string', // Label of widget visDefaultStyle: { width: 600, @@ -98,14 +97,17 @@ export default class BasicIFrame extends VisRxWidget { } as const; } - async componentDidMount(): Promise { + componentDidMount(): void { super.componentDidMount(); this.onPropertyUpdate(); } - async componentWillUnmount(): Promise { + componentWillUnmount(): void { super.componentWillUnmount(); - this.refreshInterval && clearInterval(this.refreshInterval); + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + this.refreshInterval = null; + } if (this.hashInstalled) { this.hashInstalled = false; window.removeEventListener('hashchange', this.onHashChange); @@ -116,17 +118,17 @@ export default class BasicIFrame extends VisRxWidget { * Enables calling widget info on the class instance itself */ // eslint-disable-next-line class-methods-use-this - getWidgetInfo() { + getWidgetInfo(): RxWidgetInfo { return BasicIFrame.getWidgetInfo(); } - onHashChange = () => { + onHashChange = (): void => { if (this.props.context.activeView === this.props.view) { this.refreshIFrame(); } }; - refreshIFrame() { + refreshIFrame(): void { if (this.state.rxData.refreshWithNoQuery === true) { this.frameRef.current?.contentWindow?.location.reload(); } else if (this.frameRef.current) { @@ -134,7 +136,7 @@ export default class BasicIFrame extends VisRxWidget { } } - static isHidden(el: HTMLElement) { + static isHidden(el: HTMLElement): boolean { return el.offsetParent === null; } @@ -149,8 +151,8 @@ export default class BasicIFrame extends VisRxWidget { return els; } - onPropertyUpdate() { - const src = this.state.rxData.src || ''; + onPropertyUpdate(): void { + const src = this.state.rxData.src || ''; const refreshInterval = Number(this.state.rxData.refreshInterval) || 0; const refreshOnViewChange = this.state.rxData.refreshOnViewChange === true; const refreshOnWakeUp = this.state.rxData.refreshOnWakeUp === true; @@ -174,14 +176,22 @@ export default class BasicIFrame extends VisRxWidget { this.refreshInterval = null; } // install refresh handler - this.refreshInterval = this.refreshInterval || setInterval(() => { - if (this.frameRef.current && !BasicIFrame.isHidden(this.frameRef.current as HTMLElement)) { - const parents = BasicIFrame.getParents(this.frameRef.current).filter(el => BasicIFrame.isHidden(el)); - if (!parents.length || parents[0].tagName === 'BODY' || parents[0].id === 'materialdesign-vuetify-container') { - this.refreshIFrame(); + this.refreshInterval = + this.refreshInterval || + setInterval(() => { + if (this.frameRef.current && !BasicIFrame.isHidden(this.frameRef.current as HTMLElement)) { + const parents = BasicIFrame.getParents(this.frameRef.current).filter(el => + BasicIFrame.isHidden(el), + ); + if ( + !parents.length || + parents[0].tagName === 'BODY' || + parents[0].id === 'materialdesign-vuetify-container' + ) { + this.refreshIFrame(); + } } - } - }, refreshInterval); + }, refreshInterval); } else if (this.refreshInterval) { this.startedInterval = 0; clearInterval(this.refreshInterval); @@ -201,7 +211,7 @@ export default class BasicIFrame extends VisRxWidget { } } - onRxDataChanged() { + onRxDataChanged(): void { this.onPropertyUpdate(); } @@ -230,15 +240,21 @@ export default class BasicIFrame extends VisRxWidget { }; const src = this.state.rxData.src || ''; - return src ?
    -