diff --git a/src-editor/src/App.tsx b/src-editor/src/App.tsx index d0d8b071..d20215df 100644 --- a/src-editor/src/App.tsx +++ b/src-editor/src/App.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ReactSplit, { SplitDirection } from '@devbookhq/splitter'; import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; +import type { IobTheme } from '@iobroker/adapter-react-v5'; import { I18n, Utils, @@ -13,11 +13,7 @@ import { Confirm as DialogConfirm, } from '@iobroker/adapter-react-v5'; -import { - MdMenu as IconMenuClosed, - MdArrowBack as IconMenuOpened, - MdVisibility as IconShowLog, -} from 'react-icons/md'; +import { MdMenu as IconMenuClosed, MdArrowBack as IconMenuOpened, MdVisibility as IconShowLog } from 'react-icons/md'; import SideMenu from './SideMenu'; import Log from './Log'; @@ -26,9 +22,23 @@ import DialogError from './Dialogs/Error'; import DialogImportFile from './Dialogs/ImportFile'; import BlocklyEditor from './Components/BlocklyEditor'; import { ContextWrapper } from './Components/RulesEditor/components/ContextWrapper'; -import {Box} from '@mui/material'; - -const styles = { +import { Box } from '@mui/material'; +import type { GenericAppProps, GenericAppState } from '@iobroker/adapter-react-v5/build/types'; + +import enLang from './i18n/en.json'; +import deLang from './i18n/de.json'; +import esLang from './i18n/es.json'; +import frLang from './i18n/fr.json'; +import itLang from './i18n/it.json'; +import nlLang from './i18n/nl.json'; +import plLang from './i18n/pl.json'; +import ptLang from './i18n/pt.json'; +import ruLang from './i18n/ru.json'; +import ukLang from './i18n/uk.json'; +import zhCnLang from './i18n/zh-cn.json'; +import type { ScriptType } from '@/types'; + +const styles: Record = { root: { flexGrow: 1, display: 'flex', @@ -38,7 +48,7 @@ const styles = { menuDiv: { overflow: 'hidden', }, - splitterDivs: theme => ({ + splitterDivs: (theme: IobTheme): any => ({ '&>div': { overflow: 'hidden', width: '100%', @@ -63,7 +73,7 @@ const styles = { marginLeft: 0, }, */ - content: theme => ({ + content: (theme: IobTheme): React.CSSProperties => ({ width: '100%', height: '100%', backgroundColor: theme.palette.background && theme.palette.background.default, @@ -84,7 +94,7 @@ const styles = { progress: { margin: 100, }, - menuOpenCloseButton: theme => ({ + menuOpenCloseButton: (theme: IobTheme): any => ({ position: 'absolute', left: 0, borderRadius: '0 5px 5px 0', @@ -101,7 +111,7 @@ const styles = { color: 'white', }, }), - showLogButton: theme => ({ + showLogButton: (theme: IobTheme): any => ({ position: 'absolute', right: 3, borderRadius: '5px 5px 0 0', @@ -120,22 +130,60 @@ const styles = { }), }; -class App extends GenericApp { - constructor(props) { +interface AppProps extends GenericAppProps { + version: string; +} + +interface AppState extends GenericAppState { + ready: boolean; + scriptsHash: number; + instances: number[]; + updating: boolean; + resizing: boolean; + selected: string | null; + logMessage: Record; + editing: string[]; + menuOpened: boolean; + menuSelectId: string; + expertMode: boolean; + logHorzLayout: boolean; + runningInstances: Record; + confirm: string; + importFile: boolean; + message: string; + searchText: string; + hideLog: boolean; + debugMode: boolean; + debugInstance: { adapter?: string; instance?: string } | null; + splitSizes: [number, number]; + logSizes: [number, number]; +} + +class App extends GenericApp { + private hosts: string[] = []; + + private importFile: string | null = null; + + private scripts: Record = {}; + + private confirmCallback: null | ((result: boolean) => void) = null; + + constructor(props: AppProps) { super(props, { + // @ts-expect-error fix later Connection: AdminConnection, translations: { - en: require('./i18n/en'), - de: require('./i18n/de'), - es: require('./i18n/es'), - fr: require('./i18n/fr'), - it: require('./i18n/it'), - nl: require('./i18n/nl'), - pl: require('./i18n/pl'), - pt: require('./i18n/pt'), - ru: require('./i18n/ru'), - uk: require('./i18n/uk'), - 'zh-cn': require('./i18n/zh-cn'), + en: enLang, + de: deLang, + es: esLang, + fr: frLang, + it: itLang, + nl: nlLang, + pl: plLang, + pt: ptLang, + ru: ruLang, + uk: ukLang, + 'zh-cn': zhCnLang, }, bottomButtons: false, socket: { @@ -146,45 +194,42 @@ class App extends GenericApp { // this.logIndex = 0; const logSizesStr = window.localStorage.getItem('JS.logSizes'); - let logSizes = [80, 20]; + let logSizes: [number, number] = [80, 20]; if (logSizesStr) { try { logSizes = JSON.parse(logSizesStr); - } catch (e) { + } catch { // ignore } } const splitSizesStr = window.localStorage.getItem('JS.splitSizes'); - let splitSizes = [20, 80]; + let splitSizes: [number, number] = [20, 80]; if (splitSizesStr) { try { splitSizes = JSON.parse(splitSizesStr); - } catch (e) { + } catch { // ignore } } - this.hosts = []; - this.importFile = null; - this.scripts = {}; Object.assign(this.state, { splitSizes, logSizes }); - window.alert = message => { + window.alert = (message: string): void => { console.error(message); - this.showError(message.toString()); + this.showJsError(message.toString()); }; } - onScriptsChanged = (id, obj) => { + onScriptsChanged = (id: string, obj: ioBroker.Object | null | undefined): void => { if (!id) { return; } let changed = false; - const newState = {}; + const newState: Partial = {}; if (id.startsWith('script.js.')) { if (obj) { if (JSON.stringify(this.scripts[id]) !== JSON.stringify(obj)) { - this.scripts[id] = obj; + this.scripts[id] = obj as ioBroker.ScriptObject; changed = true; newState.scriptsHash = this.state.scriptsHash + 1; } @@ -195,56 +240,54 @@ class App extends GenericApp { } } - changed && this.setState(newState); + changed && this.setState(newState as AppState); }; - onInstanceChanged = (id, obj) => { + onInstanceChanged = (id: string, obj: ioBroker.Object | null | undefined): void => { if (!id) { return; } let changed = false; - const newState = {}; + const newState: Partial = {}; if (id.match(/^system\.adapter\.[-_\w\d]+\$/)) { // update instances if (id.startsWith(`system.adapter.${this.adapterName}.`)) { - if (obj && obj.type === 'instance') { - if (!this.state.instances.includes(id)) { + const idNum = parseInt(id.split('.').pop() || '0', 10) || 0; + if (obj?.type === 'instance') { + if (!this.state.instances.includes(idNum)) { newState.instances = [...this.state.instances]; - newState.instances.push(id); + newState.instances.push(idNum); newState.instances.sort(); changed = true; // request alive - this.socket.subscribeState(`${obj._id}.alive`, this.onInstanceAliveChange); + void this.socket.subscribeState(`${obj._id}.alive`, this.onInstanceAliveChange); } - } else if (!obj && this.state.instances.includes(id)) { + } else if (!obj && this.state.instances.includes(idNum)) { this.socket.unsubscribeState(`${id}.alive`, this.onInstanceAliveChange); newState.instances = [...this.state.instances]; - const pos = newState.instances.indexOf(id); + const pos = newState.instances.indexOf(idNum); newState.instances.splice(pos, 1); changed = true; } } - if (obj && obj[id].common && obj[id].common.blockly) { + if (obj?.common?.blockly) { this.confirmCallback = result => result && window.location.reload(); newState.confirm = I18n.t('Some blocks were updated. Reload admin?'); changed = true; } } - changed && this.setState(newState); + changed && this.setState(newState as AppState); }; - onHostChanged = (id, obj) => { + onHostChanged = (id: string, obj: ioBroker.Object | null | undefined): void => { if (!id) { return; } - let changed = false; - const newState = {}; - if (id.startsWith('system.host.')) { - if (obj && obj.type === 'host') { + if (obj?.type === 'host') { if (!this.hosts.includes(id)) { this.hosts.push(id); this.hosts.sort(); @@ -254,57 +297,58 @@ class App extends GenericApp { this.hosts.splice(pos, 1); } } - - changed && this.setState(newState); }; - onConnectionReady() { + onConnectionReady(): void { window.systemLang = this.socket.systemLang; - this.setState({ - ready: false, - updateScripts: 0, - scriptsHash: 0, - instances: [], - updating: false, - resizing: false, - selected: null, - logMessage: {}, - editing: [], - menuOpened: window.localStorage.getItem('App.menuOpened') !== 'false', - menuSelectId: '', - expertMode: window.localStorage.getItem('App.expertMode') === 'true', - logHorzLayout: window.localStorage.getItem('App.logHorzLayout') === 'true', - runningInstances: {}, - confirm: '', - importFile: false, - message: '', - searchText: '', - hideLog: window.localStorage.getItem('App.hideLog') === 'true', - debugMode: false, - debugInstance: null, - }); - - const newState = {}; + this.setState( + { + ready: false, + scriptsHash: 0, + instances: [], + updating: false, + resizing: false, + selected: null, + logMessage: {}, + editing: [], + menuOpened: window.localStorage.getItem('App.menuOpened') !== 'false', + menuSelectId: '', + expertMode: window.localStorage.getItem('App.expertMode') === 'true', + logHorzLayout: window.localStorage.getItem('App.logHorzLayout') === 'true', + runningInstances: {}, + confirm: '', + importFile: false, + message: '', + searchText: '', + hideLog: window.localStorage.getItem('App.hideLog') === 'true', + debugMode: false, + debugInstance: null, + splitSizes: [20, 80], + }, + async (): Promise => { + const newState: Partial = {}; - // load instances & scripts - // Read all instances - this.subscribeOnInstances() - .then(result => { - newState.instances = result.instances; - newState.runningInstances = result.runningInstances; + // load instances & scripts + // Read all instances + const instancesResult = await this.subscribeOnInstances(); + newState.instances = instancesResult.instances; + newState.runningInstances = instancesResult.runningInstances; - return this.readAdaptersWithBlockly(); - }) - .then(() => this.socket.getHosts()) - .then(hosts => { + await this.readAdaptersWithBlockly(); + const hosts = await this.socket.getHosts(); this.hosts = hosts.map(obj => obj._id); // load all scripts - return this.readAllScripts(); - }) - .then(scripts => { - if (window.localStorage.getItem('App.expertMode') !== 'true' && window.localStorage.getItem('App.expertMode') !== 'false') { + const scripts = await this.readAllScripts(); + if ( + window.localStorage.getItem('App.expertMode') !== 'true' && + window.localStorage.getItem('App.expertMode') !== 'false' + ) { // detect if some global scripts exists - if (Object.keys(scripts).find(id => id.startsWith('script.js.global.') && scripts.type === 'script')) { + if ( + Object.keys(scripts).find( + id => id.startsWith('script.js.global.') && scripts[id].type === 'script', + ) + ) { newState.expertMode = true; } } @@ -316,81 +360,78 @@ class App extends GenericApp { } newState.scriptsHash = scriptsHash; newState.ready = true; - this.socket.subscribeObject('script.*', this.onScriptsChanged); - this.socket.subscribeObject('system.adapter.*', this.onInstanceChanged); - this.socket.subscribeObject('system.host.*', this.onHostChanged); - this.setState(newState); - }); + this.setState(newState as AppState); + + await this.socket.subscribeObject('script.*', this.onScriptsChanged); + await this.socket.subscribeObject('system.adapter.*', this.onInstanceChanged); + await this.socket.subscribeObject('system.host.*', this.onHostChanged); + }, + ); } - subscribeOnInstances() { - return this.socket.getAdapterInstances(this.adapterName) - .then(instancesArray => { - const instances = instancesArray.map(obj => parseInt(obj._id.split('.').pop())).sort(); - const runningInstances = {}; - instances.forEach(id => runningInstances[`system.adapter.${this.adapterName}.${id}`] = false); - - const promises = []; - - // subscribe on instances - instances.forEach(instance => { - const instanceId = `system.adapter.${this.adapterName}.${instance}`; - const id = `${instanceId}.alive`; - promises.push(this.socket.getState(id) - .then(state => { - runningInstances[instanceId] = state ? state.val : false; - this.socket.subscribeState(id, this.onInstanceAliveChange); - })); - }); - - return Promise.all(promises) - .then(() => ({ instances, runningInstances })); - }) + async subscribeOnInstances(): Promise<{ instances: number[]; runningInstances: Record }> { + const instancesArray = await this.socket.getAdapterInstances(this.adapterName); + const instances: number[] = instancesArray.map(obj => parseInt(obj._id.split('.').pop() || '0')).sort(); + const runningInstances: Record = {}; + instances.forEach(id => (runningInstances[`system.adapter.${this.adapterName}.${id}`] = false)); + + // subscribe on instances + for (let i = 0; i < instances.length; i++) { + const instanceId = `system.adapter.${this.adapterName}.${instances[i]}`; + const id = `${instanceId}.alive`; + const state = await this.socket.getState(id); + runningInstances[instanceId] = state ? !!state.val : false; + await this.socket.subscribeState(id, this.onInstanceAliveChange); + } + + return { instances, runningInstances }; } - readAllScripts() { - return this.socket.getObjectView('script.js.', 'script.js.\u9999', 'channel') - .then(folders => - this.socket.getObjectView('script.js.', 'script.js.\u9999', 'script') - .then(scripts => { - Object.keys(scripts).forEach(id => folders[id] = scripts[id]); - return folders; - })); + async readAllScripts(): Promise> { + const folders: Record = + await this.socket.getObjectViewSystem('channel', 'script.js.', 'script.js.\u9999'); + const scripts = await this.socket.getObjectViewSystem('script', 'script.js.', 'script.js.\u9999'); + Object.keys(scripts).forEach(id => (folders[id] = scripts[id])); + return folders; } - readAdaptersWithBlockly() { - return this.socket.getObjectView('system.adapter.', 'system.adapter.\u9999', 'adapter') - .then(adapters => - new Promise(resolve => - BlocklyEditor.loadCustomBlockly(adapters, () => resolve()))); + async readAdaptersWithBlockly(): Promise { + const adapters: Record = await this.socket.getObjectViewSystem( + 'adapter', + 'system.adapter.', + 'system.adapter.\u9999', + ); + return new Promise(resolve => BlocklyEditor.loadCustomBlockly(adapters, () => resolve())); } - onInstanceAliveChange = (id, state) => { + onInstanceAliveChange = (id: string, state: ioBroker.State | null | undefined): void => { if (id) { - id = id && id.substring(0, id.length - 6); // - .alive + id = id ? id.substring(0, id.length - 6) : ''; // - .alive if (this.state.runningInstances[id] !== (state ? state.val : false)) { - const runningInstances = JSON.parse(JSON.stringify(this.state.runningInstances)); - runningInstances[id] = state ? state.val : false; + const runningInstances: Record = JSON.parse( + JSON.stringify(this.state.runningInstances), + ); + runningInstances[id] = state ? !!state.val : false; this.setState({ runningInstances }); } } }; - onToggleExpertMode(expertMode) { + onToggleExpertMode(expertMode: boolean): void { this.onExpertModeChange(expertMode); } - compareScripts(newScripts) { + compareScripts(newScripts: Record): boolean { const oldIds = Object.keys(this.scripts); const newIds = Object.keys(newScripts); if (oldIds.length !== newIds.length) { - this.scripts = this.newScripts; + this.scripts = newScripts; return true; } if (JSON.stringify(oldIds) !== JSON.stringify(newIds)) { - this.scripts = this.newScripts; + this.scripts = newScripts; return true; } for (let i = 0; i < oldIds.length; i++) { @@ -398,61 +439,65 @@ class App extends GenericApp { const newScript = newScripts[oldIds[i]].common; if (oldScript.name !== newScript.name) { - this.scripts = this.newScripts; + this.scripts = newScripts; return true; } - if (oldScript.engine !== newScript.engine) { - this.scripts = this.newScripts; + if ((oldScript as ioBroker.ScriptCommon).engine !== (newScript as ioBroker.ScriptCommon).engine) { + this.scripts = newScripts; return true; } - if (oldScript.engineType !== newScript.engineType) { - this.scripts = this.newScripts; + if ((oldScript as ioBroker.ScriptCommon).engineType !== (newScript as ioBroker.ScriptCommon).engineType) { + this.scripts = newScripts; return true; } - if (oldScript.enabled !== newScript.enabled) { - this.scripts = this.newScripts; + if ((oldScript as ioBroker.ScriptCommon).enabled !== (newScript as ioBroker.ScriptCommon).enabled) { + this.scripts = newScripts; return true; } } + return false; } - onRename(oldId, newId, newName, newInstance) { + async onRename(oldId: string, newId: string, newName?: string, newInstance?: number): Promise { if (newId.trim().endsWith('.')) { newId = newId.replace(/\.\s*$/, '_'); } console.log(`Rename ${oldId} => ${newId}`); - let promise; this.setState({ updating: true }); // Rename script.js.common.Skript_1 => script.js.common.New folder.Skript_1 - if (this.scripts[oldId] && this.scripts[oldId].type === 'script') { - const common = JSON.parse(JSON.stringify(this.scripts[oldId].common)); - common.name = newName || common.name; - if (newInstance !== undefined) { - common.engine = `system.adapter.javascript.${newInstance}`; - } - // Check if the script is not a children of other script - const parts = newId.split('.'); - parts.pop(); - const parentID = parts.join('.'); - - if (this.scripts[parentID] && this.scripts[parentID].type === 'script') { + try { + if (this.scripts[oldId]?.type === 'script') { + const common = JSON.parse(JSON.stringify(this.scripts[oldId].common)); + common.name = newName || common.name; + if (newInstance !== undefined) { + common.engine = `system.adapter.javascript.${newInstance}`; + } + // Check if the script is not a children of other script + const parts = newId.split('.'); parts.pop(); - newId = `${parts.join('.')}.${newId.split('.').pop()}`; - } + const parentID = parts.join('.'); - promise = this.updateScript(oldId, newId, common); - } else { - promise = this.renameGroup(oldId, newId, newName); + if (this.scripts[parentID] && this.scripts[parentID].type === 'script') { + parts.pop(); + newId = `${parts.join('.')}.${newId.split('.').pop()}`; + } + + await this.updateScript(oldId, newId, common); + } else { + await this.renameGroup(oldId, newId, newName); + } + } catch (err) { + if (!(err as Error).toString().includes('canceled')) { + this.showJsError(err as Error); + } } - promise - .then(() => this.setState({ updating: false })) - .catch(err => err !== 'canceled' && this.showError(err)); + this.setState({ updating: false }); } - renameGroup(id, newId, newName, _list) { + async renameGroup(id: string, newId: string, newName?: string, _list?: string[]): Promise { if (!_list) { _list = []; @@ -460,277 +505,310 @@ class App extends GenericApp { // find all elements _list = Object.keys(this.scripts).filter(_id => _id.startsWith(`${id}.`)); - return this.socket.getObject(id) - .then(obj => { - obj = obj || { common: {} }; - obj.common.name = newName || obj.common.name || id.split('.').pop(); - obj._id = newId; - - this.socket.delObject(id) - .catch(() => { }) - .then(() => this.socket.setObject(newId, obj)) - .then(() => this.renameGroup(id, newId, newName, _list)) - .catch(e => console.log(e)); - }) - .catch(e => { - console.log(e); - const obj = { - _id: newId, - type: 'channel', - common: { - name: newName || id.split('.').pop(), - expert: true, - }, - native: {}, - }; - // may be it is virtual folder - return this.socket.setObject(newId, obj) - .then(() => this.renameGroup(id, newId, newName, _list)); - }); + let obj = await this.socket.getObject(id); + obj = obj || ({ common: {}, type: 'channel' } as ioBroker.ChannelObject); + obj.common.name = newName || obj.common.name || id.split('.').pop() || ''; + obj._id = newId; + + // Delete root object/folder + try { + await this.socket.delObject(id); + } catch { + // ignore + } + + // recreate same object with new name + try { + await this.socket.setObject(newId, obj); + await this.renameGroup(id, newId, newName, _list); + } catch (err) { + console.log(err); + const obj: ioBroker.ChannelObject = { + _id: newId, + type: 'channel', + common: { + name: newName || id.split('.').pop() || '', + expert: true, + }, + native: {}, + }; + // may be it is virtual folder + await this.socket.setObject(newId, obj); + await this.renameGroup(id, newId, newName, _list); + } } else if (_list.length) { let nId = _list.pop(); - return this.socket.getObject(nId) - .then(obj => - this.socket.delObject(nId) - .catch(() => { }) - .then(() => { - nId = newId + nId.substring(id.length); - obj._id = nId; - obj.common = obj.common || {}; - obj.common.expert = true; - return this.socket.setObject(nId, obj); - }) - .then(() => this.renameGroup(id, newId, newName, _list)) - ); - } else { - return Promise.resolve(); + if (nId) { + const obj = await this.socket.getObject(nId); + if (obj) { + try { + await this.socket.delObject(nId); + } catch { + // ignore + } + nId = newId + nId.substring(id.length); + obj._id = nId; + obj.common = obj.common || {}; + obj.common.expert = true; + await this.socket.setObject(nId, obj); + await this.renameGroup(id, newId, newName, _list); + } + } } } - onUpdateScript(id, common) { + onUpdateScript(id: string, common: ioBroker.ScriptCommon): void { if (this.scripts[id] && this.scripts[id].type === 'script') { this.updateScript(id, id, common) - .then(() => { }) - .catch(err => err !== 'canceled' && this.showError(err)); + .then(() => {}) + .catch(err => !(err as Error).toString().includes('canceled') && this.showJsError(err)); } } - onSelect(selected) { + onSelect(selected: string): void { if (this.scripts[selected] && this.scripts[selected].common && this.scripts[selected].type === 'script') { this.setState({ selected, menuSelectId: selected }, () => - setTimeout(() => this.setState({ menuSelectId: '' })), 300); + setTimeout(() => this.setState({ menuSelectId: '' }), 300), + ); } } - onExpertModeChange(expertMode) { + onExpertModeChange(expertMode: boolean): void { if (this.state.expertMode !== expertMode) { window.localStorage.setItem('App.expertMode', expertMode ? 'true' : 'false'); this.setState({ expertMode }); } } - showError(err) { + showJsError(err: Error | string): void { this.setState({ errorText: err ? err.toString() : '' }); } - showMessage(message) { + showMessage(message: string): void { this.setState({ message: message ? message.toString() : '' }); } - onDelete(id) { - this.socket.delObject(id) - .then(() => { }) - .catch(err => - this.showError(err)); + onDelete(id: string): void { + this.socket.delObject(id).catch(err => this.showJsError(err)); } - onEdit(id) { + onEdit(id: string): void { if (this.state.selected !== id) { this.setState({ selected: id }); } } - onAddNew(id, name, isFolder, instance, type, source) { + onAddNew( + id: string, + name: string, + isFolder: boolean, + instance?: number, + type?: ScriptType | 'folder', + source?: string, + ): void { const reg = new RegExp(`^${id}\\.`); if (Object.keys(this.scripts).find(_id => id === _id || reg.test(id))) { - return this.showError(I18n.t('Yet exists!')); + return this.showJsError(I18n.t('Yet exists!')); } if (isFolder) { - this.socket.setObject(id, { - common: { - name, - expert: true, - }, - type: 'channel', - }) + this.socket + .setObject(id, { + _id: id, + type: 'channel', + common: { + name, + expert: true, + }, + native: {}, + }) .then(() => - setTimeout(() => this.setState({ menuSelectId: id }, () => - setTimeout(() => this.setState({ menuSelectId: '' })), 300), 1000)) - .catch(err => this.showError(err)); + setTimeout( + () => + this.setState({ menuSelectId: id }, () => + setTimeout(() => this.setState({ menuSelectId: '' }), 300), + ), + 1000, + ), + ) + .catch(err => this.showJsError(err)); } else { if (type === 'Blockly' && !source) { // Default Blockly XML for new scripts source = `\n//${btoa(encodeURIComponent(''))}`; } - this.socket.setObject(id, { - common: { - name, - expert: true, - engineType: type, - engine: `system.adapter.javascript.${instance || 0}`, - source: source || '', - debug: false, - verbose: false, - }, - type: 'script', - }) + this.socket + .setObject(id, { + _id: id, + type: 'script', + common: { + name, + expert: true, + engineType: type || 'Javascript/js', + enabled: false, + engine: `system.adapter.javascript.${instance || 0}`, + source: source || '', + debug: false, + verbose: false, + }, + native: {}, + }) .then(() => setTimeout(() => this.onSelect(id), 1000)) - .catch(err => this.showError(err)); + .catch(err => this.showJsError(err)); } } - updateScript(oldId, newId, newCommon) { - return this.socket.getObject(oldId) - .then(_obj => { - const obj = { common: {} }; + async updateScript(oldId: string, newId: string, newCommon: ioBroker.ScriptCommon): Promise { + let _obj = await this.socket.getObject(oldId); + const obj: ioBroker.ScriptObject = { common: {} } as ioBroker.ScriptObject; - if (newCommon.engine !== undefined) obj.common.engine = newCommon.engine; - if (newCommon.enabled !== undefined) obj.common.enabled = newCommon.enabled; - if (newCommon.source !== undefined) obj.common.source = newCommon.source; - if (newCommon.debug !== undefined) obj.common.debug = newCommon.debug; - if (newCommon.verbose !== undefined) obj.common.verbose = newCommon.verbose; + if (newCommon.engine !== undefined) { + obj.common.engine = newCommon.engine; + } + if (newCommon.enabled !== undefined) { + obj.common.enabled = newCommon.enabled; + } + if (newCommon.source !== undefined) { + obj.common.source = newCommon.source; + } + if (newCommon.debug !== undefined) { + obj.common.debug = newCommon.debug; + } + if (newCommon.verbose !== undefined) { + obj.common.verbose = newCommon.verbose; + } - obj.from = 'system.adapter.admin.0'; // we must distinguish between GUI(admin.0) and disk(javascript.0) + obj.from = 'system.adapter.admin.0'; // we must distinguish between GUI(admin.0) and disk(javascript.0) - if (oldId === newId && _obj && _obj.common && newCommon.name === _obj.common.name) { - if (!newCommon.engineType || newCommon.engineType !== _obj.common.engineType) { - if (newCommon.engineType !== undefined) { - obj.common.engineType = newCommon.engineType || 'Javascript/js'; - } - } - obj.type = 'script'; - return this.socket.extendObject(oldId, obj); - } else { - // let prefix; + if (oldId === newId && _obj?.common && newCommon.name === _obj.common.name) { + if (!newCommon.engineType || newCommon.engineType !== _obj.common.engineType) { + if (newCommon.engineType !== undefined) { + obj.common.engineType = newCommon.engineType || 'Javascript/js'; + } + } + obj.type = 'script'; + return this.socket.extendObject(oldId, obj); + } + // let prefix; - // let parts = _obj.common.engineType.split('/'); + // let parts = _obj.common.engineType.split('/'); - // prefix = 'script.' + (parts[1] || parts[0]) + '.'; + // prefix = 'script.' + (parts[1] || parts[0]) + '.'; - if (_obj && _obj.common) { - _obj.common.engineType = newCommon.engineType || _obj.common.engineType || 'Javascript/js'; - return this.socket.delObject(oldId) - .then(() => { - if (obj.common.engine !== undefined) _obj.common.engine = obj.common.engine; - if (obj.common.enabled !== undefined) _obj.common.enabled = obj.common.enabled; - if (obj.common.source !== undefined) _obj.common.source = obj.common.source; - if (obj.common.name !== undefined) _obj.common.name = obj.common.name; - if (obj.common.debug !== undefined) _obj.common.debug = obj.common.debug; - if (obj.common.verbose !== undefined) _obj.common.verbose = obj.common.verbose; + if (_obj?.common) { + _obj.common.engineType = newCommon.engineType || _obj.common.engineType || 'Javascript/js'; + await this.socket.delObject(oldId); + if (obj.common.engine !== undefined) { + _obj.common.engine = obj.common.engine; + } + if (obj.common.enabled !== undefined) { + _obj.common.enabled = obj.common.enabled; + } + if (obj.common.source !== undefined) { + _obj.common.source = obj.common.source; + } + if (obj.common.name !== undefined) { + _obj.common.name = obj.common.name; + } + if (obj.common.debug !== undefined) { + _obj.common.debug = obj.common.debug; + } + if (obj.common.verbose !== undefined) { + _obj.common.verbose = obj.common.verbose; + } - delete _obj._rev; + // @ts-expect-error deprecated + if (_obj._rev !== undefined) { + // @ts-expect-error deprecated + delete _obj._rev; + } - // Name must always exist - _obj.common.name = newCommon.name; - _obj.common.expert = true; - _obj.type = 'script'; + // Name must always exist + _obj.common.name = newCommon.name; + _obj.common.expert = true; + _obj.type = 'script'; - _obj._id = newId; // prefix + newCommon.name.replace(/[\s"']/g, '_'); + _obj._id = newId; // prefix + newCommon.name.replace(/[\s"']/g, '_'); - this.socket.setObject(newId, _obj); - }); - } else { - _obj = obj; - } + await this.socket.setObject(newId, _obj); + return; + } + _obj = obj; - // Name must always exist - _obj.common.name = newCommon.name; - _obj.common.expert = true; - _obj.type = 'script'; - _obj._id = newId; // prefix + newCommon.name.replace(/[\s"']/g, '_'); + // Name must always exist + _obj.common.name = newCommon.name; + _obj.common.expert = true; + _obj.type = 'script'; + _obj._id = newId; // prefix + newCommon.name.replace(/[\s"']/g, '_'); - return this.socket.setObject(newId, _obj); - } - }); + return this.socket.setObject(newId, _obj); } - onEnableDisable(id, enabled) { + onEnableDisable(id: string, enabled: boolean): void { if (this.scripts[id] && this.scripts[id].type === 'script') { - const common = this.scripts[id].common; + const common: ioBroker.ScriptCommon = this.scripts[id].common as ioBroker.ScriptCommon; common.enabled = enabled; common.expert = true; - this.updateScript(id, id, common) - .catch(err => err !== 'canceled' && this.showError(err)); + this.updateScript(id, id, common).catch(err => err !== 'canceled' && this.showJsError(err)); } } - getLiveHost(cb, _list) { - if (!_list) { - _list = this.hosts ? [...this.hosts] : []; - } - - if (_list.length) { - const id = _list.shift(); - this.socket.getState(`${id}.alive`) - .then(state => { - if (state && state.val) { - cb(id); - } else { - setTimeout(() => this.getLiveHost(cb, _list)); - } - }); - } else { - cb(); + async getLiveHost(): Promise { + for (let h = 0; h < this.hosts.length; h++) { + const id = this.hosts[h]; + const state = await this.socket.getState(`${id}.alive`); + if (state && state?.val) { + return id; + } } + return undefined; } - onExport() { - this.getLiveHost(host => { - if (!host) { - return this.showError(I18n.t('No active host found')); - } + async onExport(): Promise { + const host = await this.getLiveHost(); + if (!host) { + this.showJsError(I18n.t('No active host found')); + return; + } - const d = new Date(); - let date = d.getFullYear(); - let m = d.getMonth() + 1; - if (m < 10) { - m = `0${m}`; - } - date += `-${m}`; - m = d.getDate(); - if (m < 10) { - m = `0${m}`; - } - date += `-${m}-`; + const d = new Date(); + let date = d.getFullYear().toString(); + let m: number | string = d.getMonth() + 1; + if (m < 10) { + m = `0${m}`; + } + date += `-${m}`; + m = d.getDate(); + if (m < 10) { + m = `0${m}`; + } + date += `-${m}-`; - this.socket.getRawSocket().emit('sendToHost', host, 'readObjectsAsZip', { + this.socket.getRawSocket().emit( + 'sendToHost', + host, + 'readObjectsAsZip', + { adapter: 'javascript', id: 'script.js', link: `${date}scripts.zip`, // request link to file and not the data itself fileStorageNamespace: `admin.${this.instance}`, // new controller 5.x understands this and saves ZIP in the file store - }, data => { + }, + (data: string | { data?: string; error?: string }) => { if (typeof data === 'string') { // it is a link to the created file const a = document.createElement('a'); - if (data.startsWith('admin.')) { - // new controller - // actual position is http://IP:8081/adapter/javascript/index.html - // we need http://IP:8081/files/admin.0/zip/2023-06-20-scripts.zip - a.href = `../../files/${data}`; - } else { - // the data is "system.host.HOST.zip.2020-01-26-scripts.zip" - const parts = data.split('.zip.'); - a.href = `./zip/${parts[0]}/${parts[1]}`; - } + // actual position is http://IP:8081/adapter/javascript/index.html + // we need http://IP:8081/files/admin.0/zip/2023-06-20-scripts.zip + a.href = `../../files/${data}`; document.body.appendChild(a); a.click(); a.remove(); } else { - data.error && this.showError(data.error); + data.error && this.showJsError(data.error); if (data.data) { const a = document.createElement('a'); a.href = `data: application/zip;base64,${data.data}`; @@ -740,12 +818,12 @@ class App extends GenericApp { a.remove(); } } - }); - }); + }, + ); } - onImport(data) { - this.importFile = data; + onImport(data: string | undefined): void { + this.importFile = data || null; if (data) { this.confirmCallback = this.onImportConfirmed.bind(this); this.setState({ importFile: false, confirm: I18n.t('Existing scripts will be overwritten.') }); @@ -754,162 +832,202 @@ class App extends GenericApp { } } - onImportConfirmed(ok) { + async onImportConfirmed(ok: boolean): Promise { let data = this.importFile; this.importFile = null; if (ok && data) { data = data.split(',')[1]; - this.getLiveHost(host => { - if (!host) { - this.showError(I18n.t('No active host found')); - return; - } - this.socket.getRawSocket().emit('sendToHost', host, 'writeObjectsAsZip', { + const host = await this.getLiveHost(); + if (!host) { + this.showJsError(I18n.t('No active host found')); + return; + } + this.socket.getRawSocket().emit( + 'sendToHost', + host, + 'writeObjectsAsZip', + { data: data, adapter: 'javascript', - id: 'script.js' - }, data => { + id: 'script.js', + }, + (data: string | { error?: string }) => { if (data === 'permissionError') { - this.showError(I18n.t(data)); - } else if (!data || data.error) { - this.showError(data ? I18n.t(data.error) : I18n.t('Unknown error')); + this.showJsError(I18n.t(data)); + } else if (!data || (data as { error?: string }).error) { + this.showJsError( + data ? I18n.t((data as { error?: string }).error || '') : I18n.t('Unknown error'), + ); } else { this.showMessage(I18n.t('Done')); } - }); - }); + }, + ); } } - toggleLogLayout() { + toggleLogLayout(): void { window.localStorage.setItem('App.logHorzLayout', this.state.logHorzLayout ? 'false' : 'true'); this.setState({ logHorzLayout: !this.state.logHorzLayout }); } - renderEditor() { - const isAnyRulesExists = Object.keys(this.scripts).reduce((sum, id) => - sum + (this.scripts[id].common.engineType === 'Rules' ? 1 : 0), 0); - - return { - if (!value) { - this.setState({debugMode: false, debugInstance: null}); - } else { - this.setState({debugMode: true}); - } - }} - visible={!this.state.resizing} - socket={this.socket} - adapterName={this.adapterName} - onLocate={menuSelectId => this.setState({ menuSelectId })} - runningInstances={this.state.runningInstances} - menuOpened={this.state.menuOpened} - searchText={this.state.searchText} - themeType={this.state.themeType} - themeName={this.state.themeName} - theme={this.state.theme} - expertMode={this.state.expertMode} - onChange={(id, common) => this.onUpdateScript(id, common)} - isAnyRulesExists={isAnyRulesExists} - debugInstance={this.state.debugInstance} - onSelectedChange={(id, editing) => { - const newState = {}; - let changed = false; - if (id !== this.state.selected) { - changed = true; - newState.selected = id; - } - if (JSON.stringify(editing) !== JSON.stringify(this.state.editing)) { - changed = true; - newState.editing = JSON.parse(JSON.stringify(editing)); + renderEditor(): React.JSX.Element { + const isAnyRulesExists = Object.keys(this.scripts).reduce( + (sum, id) => sum + ((this.scripts[id].common as ioBroker.ScriptCommon).engineType === 'Rules' ? 1 : 0), + 0, + ); + + return ( + { + if (!value) { + this.setState({ debugMode: false, debugInstance: null }); + } else { + this.setState({ debugMode: true }); + } + }} + visible={!this.state.resizing} + socket={this.socket} + adapterName={this.adapterName} + onLocate={menuSelectId => this.setState({ menuSelectId })} + runningInstances={this.state.runningInstances} + menuOpened={this.state.menuOpened} + searchText={this.state.searchText} + themeType={this.state.themeType} + themeName={this.state.themeName} + theme={this.state.theme} + expertMode={this.state.expertMode} + onChange={(id, common) => this.onUpdateScript(id, common)} + isAnyRulesExists={isAnyRulesExists} + debugInstance={this.state.debugInstance} + onSelectedChange={(id, editing) => { + const newState: Partial = {}; + let changed = false; + if (id !== this.state.selected) { + changed = true; + newState.selected = id; + } + if (JSON.stringify(editing) !== JSON.stringify(this.state.editing)) { + changed = true; + newState.editing = JSON.parse(JSON.stringify(editing)); + } + changed && this.setState(newState as AppState); + }} + onRestart={id => this.socket.extendObject(id, { common: { enabled: true } })} + selected={ + this.state.selected && + this.scripts[this.state.selected] && + this.scripts[this.state.selected].type === 'script' + ? this.state.selected + : '' } - changed && this.setState(newState); - }} - onRestart={id => this.socket.extendObject(id, { common: { enabled: true } })} - selected={this.state.selected && this.scripts[this.state.selected] && this.scripts[this.state.selected].type === 'script' ? this.state.selected : ''} - objects={this.scripts} - instances={this.state.instances} - />; + objects={this.scripts} + resizing={this.state.resizing} + /> + ); } - showLogButton() { - return { - window.localStorage.setItem('App.hideLog', 'false'); - this.setState({ hideLog: false, resizing: true }); - setTimeout(() => this.setState({ resizing: false }), 300); - }} - > - - ; + showLogButton(): React.JSX.Element { + return ( + { + window.localStorage.setItem('App.hideLog', 'false'); + this.setState({ hideLog: false, resizing: true }); + setTimeout(() => this.setState({ resizing: false }), 300); + }} + > + + + ); } - renderErrorDialog() { - return this.state.errorText ? + renderErrorDialog(): React.JSX.Element | null { + return this.state.errorText ? ( this.setState({ errorText: '' })} text={this.state.errorText} - /> : - null; + /> + ) : null; } - renderMain() { + renderMain(): (React.JSX.Element | null)[] | null { let content; if (this.state.debugMode || this.state.hideLog) { - content = <> - {!this.state.debugMode && this.state.hideLog ? this.showLogButton() : undefined} - {this.renderEditor()} - ; + content = ( + <> + {!this.state.debugMode && this.state.hideLog ? this.showLogButton() : undefined} + {this.renderEditor()} + + ); } else { - content = this.setState({ resizing: true })} - onResizing - onResizeFinished={(_gutterIdx, logSizes) => { - this.setState({ logSizes, resizing: false }); - window.localStorage.setItem('JS.logSizes', JSON.stringify(logSizes)); - }} - gutterClassName={this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} - > - {this.renderEditor()} - this.toggleLogLayout()} - editing={this.state.editing} - socket={this.socket} - selected={this.state.selected} - onHideLog={() => { - window.localStorage.setItem('App.hideLog', 'true'); - this.setState({ hideLog: true, resizing: true }); - setTimeout(() => this.setState({ resizing: false }), 300); + content = ( + this.setState({ resizing: true })} + onResizeFinished={(_gutterIdx, logSizes) => { + this.setState({ logSizes: logSizes as [number, number], resizing: false }); + window.localStorage.setItem('JS.logSizes', JSON.stringify(logSizes)); }} - /> - ; + gutterClassName={this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} + > + {this.renderEditor()} + this.toggleLogLayout()} + editing={this.state.editing} + socket={this.socket} + selected={this.state.selected} + onHideLog={() => { + window.localStorage.setItem('App.hideLog', 'true'); + this.setState({ hideLog: true, resizing: true }); + setTimeout(() => this.setState({ resizing: false }), 300); + }} + /> + + ); } return [ - this.state.message ? this.setState({ message: '' })} text={this.state.message} /> : null, + this.state.message ? ( + this.setState({ message: '' })} + text={this.state.message} + /> + ) : null, this.renderErrorDialog(), - this.state.importFile ? this.onImport(data)} /> : null, - this.state.confirm ? { - this.state.confirm && this.setState({ confirm: '' }); - this.confirmCallback && this.confirmCallback(result); - this.confirmCallback = null; - }} - text={this.state.confirm} /> : null, - + this.state.importFile ? ( + this.onImport(data)} + /> + ) : null, + this.state.confirm ? ( + { + this.state.confirm && this.setState({ confirm: '' }); + this.confirmCallback && this.confirmCallback(result); + this.confirmCallback = null; + }} + text={this.state.confirm} + /> + ) : null, + ); - return - - - - ; + return ( + + + + + + ); } let context; if (this.state.menuOpened) { - context = { - this.setState({ splitSizes }); - window.localStorage.setItem('JS.splitSizes', JSON.stringify(splitSizes)); - }} - gutterClassName={this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} - > -
- - this.setState({ debugInstance: data, debugMode: !!data })} - key="sidemenu" - scripts={this.scripts} - scriptsHash={this.state.scriptsHash} - instances={this.state.instances} - update={this.state.updateScripts} - onRename={this.onRename.bind(this)} - onSelect={this.onSelect.bind(this)} - socket={this.socket} - selectId={this.state.menuSelectId} - onEdit={this.onEdit.bind(this)} - expertMode={this.state.expertMode} - themeType={this.state.themeType} - themeName={this.state.themeName} - onThemeChange={themeName => { - Utils.setThemeName(themeName); - const themeType = Utils.getThemeType(themeName); - this.setState({ themeName, themeType }, () => this.props.onThemeChange(themeName)); - }} - runningInstances={this.state.runningInstances} - onExpertModeChange={this.onExpertModeChange.bind(this)} - onDelete={this.onDelete.bind(this)} - onAddNew={this.onAddNew.bind(this)} - onEnableDisable={this.onEnableDisable.bind(this)} - onExport={this.onExport.bind(this)} - width={500} // TODO: https://github.com/ioBroker/ioBroker.javascript/issues/1643 - onImport={() => this.setState({ importFile: true })} - onSearch={searchText => this.setState({ searchText })} - version={this.props.version} - /> -
- {this.renderMain()} -
; + context = ( + { + this.setState({ splitSizes: splitSizes as [number, number] }); + window.localStorage.setItem('JS.splitSizes', JSON.stringify(splitSizes)); + }} + gutterClassName={this.state.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} + > +
+ this.setState({ debugInstance: data, debugMode: !!data })} + key="sidemenu" + scripts={this.scripts} + scriptsHash={this.state.scriptsHash} + instances={this.state.instances} + onRename={this.onRename.bind(this)} + socket={this.socket} + selectId={this.state.menuSelectId} + onEdit={this.onEdit.bind(this)} + expertMode={this.state.expertMode} + themeName={this.state.themeName} + onThemeChange={themeName => { + Utils.setThemeName(themeName); + const themeType = Utils.getThemeType(themeName); + this.setState({ themeName, themeType }, () => this.toggleTheme(themeName)); + }} + runningInstances={this.state.runningInstances} + onExpertModeChange={this.onExpertModeChange.bind(this)} + onDelete={this.onDelete.bind(this)} + onAddNew={this.onAddNew.bind(this)} + onEnableDisable={this.onEnableDisable.bind(this)} + onExport={this.onExport.bind(this)} + width={500} // TODO: https://github.com/ioBroker/ioBroker.javascript/issues/1643 + onImport={() => this.setState({ importFile: true })} + onSearch={searchText => this.setState({ searchText })} + version={this.props.version} + /> +
+ {this.renderMain()} +
+ ); } else { context = this.renderMain(); } - return - -
- - {context} - -
-
-
; + return ( + + +
+ {context} +
+
+
+ ); } } -App.propTypes = { - version: PropTypes.string, - onThemeChange: PropTypes.func, -}; - export default App; diff --git a/src-editor/src/Components/BlocklyEditor.tsx b/src-editor/src/Components/BlocklyEditor.tsx index 8d925f16..40d7ffa2 100644 --- a/src-editor/src/Components/BlocklyEditor.tsx +++ b/src-editor/src/Components/BlocklyEditor.tsx @@ -1,19 +1,23 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { I18n, Message as DialogMessage } from '@iobroker/adapter-react-v5'; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material'; +import { Cancel as IconCancel, Check as IconOk } from '@mui/icons-material'; + +import { I18n, Message as DialogMessage, type ThemeType } from '@iobroker/adapter-react-v5'; + import DialogError from '../Dialogs/Error'; import DialogExport from '../Dialogs/Export'; import DialogImport from '../Dialogs/Import'; +import { type BlocklyType, type BlockSvg, type WorkspaceSvg, type CustomBlock, initBlockly } from './blockly-plugins'; let languageBlocklyLoaded = false; let languageOwnLoaded = false; -let toolboxText = null; -let toolboxXml; -let scriptsLoaded = []; +let toolboxText: string | null = null; +let toolboxXml: Element | null = null; +const scriptsLoaded: string[] = []; -// BF (2020-10-31) I have no Idea, why it does not work as static in BlocklyEditor, but outside of BlocklyEditor it works -function searchXml(root, text, _id, _result) { +// BF (2020-10-31) I have no Idea, why it does not work as static in BlocklyEditor, but outside BlocklyEditor it works +function searchXml(root: Element, text: string, _id?: string, _result?: string[]): string[] { _result = _result || []; if (root.tagName === 'BLOCK' || root.tagName === 'block') { _id = root.id; @@ -22,26 +26,62 @@ function searchXml(root, text, _id, _result) { for (let a = 0; a < root.attributes.length; a++) { const val = (root.attributes[a].value || '').toLowerCase(); if (root.attributes[a].nodeName === 'name' && (val === 'oid' || val === 'text' || val === 'var')) { - if ((root.innerHTML || root.innerText || '').toLowerCase().includes(text)) { + if (_id && root.innerHTML?.toLowerCase().includes(text)) { _result.push(_id); } } } } - root.childNodes.forEach(node => - searchXml(node, text, _id, _result)); + root.childNodes.forEach(node => searchXml(node as HTMLElement, text, _id, _result)); return _result; } -class BlocklyEditor extends React.Component { - constructor(props) { - super(props); +interface BlocklyEditorProps { + command: '' | 'check' | 'export' | 'import'; + onChange: (code: string) => void; + searchText: string; + code: string; + scriptId: string; + themeType: ThemeType; +} - this.blockly = null; - this.blocklyWorkspace = null; - this.toolbox = null; - this.Blockly = window.Blockly; +interface BlocklyEditorState { + languageOwnLoaded: boolean; + languageBlocklyLoaded: boolean; + changed: boolean; + message: string | { text: string; title: string }; + error: string | { text: string; title: string }; + themeType: ThemeType; + exportText: string; + importText: boolean; + searchText: string; + showInputPrompt: null | { + promptText: string; + defaultText: string; + callback: (p1: string | null) => void; + value: string; + }; +} + +class BlocklyEditor extends React.Component { + private blockly: HTMLElement | null = null; + private blocklyWorkspace: WorkspaceSvg | null = null; + private originalCode: string; + private someSelected: string[] | null = null; + private changeTimer: ReturnType | null = null; + private someSelectedTime: number = 0; + private ignoreChanges: boolean = false; + private darkTheme: any; + private blinkBlock: any; + private readonly onResizeBind: () => void; + private didUpdate: ReturnType | null = null; + private lastCommand = ''; + private lastSearch: string; + public static Blockly: BlocklyType = window.Blockly; + + constructor(props: BlocklyEditorProps) { + super(props); this.state = { languageOwnLoaded, @@ -53,55 +93,67 @@ class BlocklyEditor extends React.Component { exportText: '', importText: false, searchText: this.props.searchText || '', + showInputPrompt: null, }; this.originalCode = props.code || ''; this.someSelected = null; - this.changeTimer = null; this.onResizeBind = this.onResize.bind(this); - this.lastCommand = ''; this.lastSearch = this.props.searchText || ''; this.blinkBlock = null; + + initBlockly(); + BlocklyEditor.Blockly.dialog.setPrompt(this.onShowNameDialog); + this.loadLanguages(); } - static loadJS(url, callback, location) { + onShowNameDialog = (promptText: string, defaultText: string, callback: (p1: string | null) => void): void => { + this.setState({ showInputPrompt: { promptText, defaultText, callback, value: defaultText } }); + }; + + static loadJS(url: string, callback: () => void, location?: HTMLElement): void { const scriptTag = document.createElement('script'); try { scriptTag.src = url; scriptTag.onload = callback; - scriptTag.onreadystatechange = callback; scriptTag.onerror = callback; (location || window.document.body).appendChild(scriptTag); } catch (e) { console.error(`Cannot load ${url}: ${e}`); - callback && callback(); + if (callback) { + callback(); + } } } - static loadScripts(scripts, callback) { - if (!scripts || !scripts.length) { - return callback && callback(); + static loadScripts(scripts: string[], callback: () => void): void { + if (!scripts?.length) { + if (callback) { + callback(); + } + return; } const adapter = scripts.pop(); - if (!scriptsLoaded.includes(adapter)) { + if (adapter && !scriptsLoaded.includes(adapter)) { scriptsLoaded.push(adapter); BlocklyEditor.loadJS(`../../adapter/${adapter}/blockly.js`, (/*data, textStatus, jqxhr*/) => - setTimeout(() => BlocklyEditor.loadScripts(scripts, callback), 0)); + setTimeout(() => BlocklyEditor.loadScripts(scripts, callback), 0),); } else { setTimeout(() => BlocklyEditor.loadScripts(scripts, callback), 0); } } - static loadCustomBlockly(adapters, callback) { + static loadCustomBlockly(adapters: Record, callback: () => void): void { // get all adapters, that can have blockly - const toLoad = []; + const toLoad: string[] = []; for (const id in adapters) { - if (!adapters.hasOwnProperty(id) || + if ( + !Object.prototype.hasOwnProperty.call(adapters, id) || !adapters[id] || !id.match(/^system\.adapter\./) || adapters[id].type !== 'adapter' @@ -109,7 +161,7 @@ class BlocklyEditor extends React.Component { continue; } - if (adapters[id].common && adapters[id].common.blockly) { + if (adapters[id].common?.blockly) { console.log(`Detected custom blockly: ${adapters[id].common.name}`); toLoad.push(adapters[id].common.name); } @@ -118,12 +170,12 @@ class BlocklyEditor extends React.Component { BlocklyEditor.loadScripts(toLoad, callback); } - static loadXMLDoc(text) { + static loadXMLDoc(text: string): Document | null { let parseXml; if (window.DOMParser) { - parseXml = xmlStr => (new window.DOMParser()).parseFromString(xmlStr, 'text/xml'); + parseXml = (xmlStr: string): Document => new window.DOMParser().parseFromString(xmlStr, 'text/xml'); } else if (typeof window.ActiveXObject !== 'undefined' && new window.ActiveXObject('Microsoft.XMLDOM')) { - parseXml = xmlStr => { + parseXml = (xmlStr: string): Document => { const xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); xmlDoc.async = 'false'; xmlDoc.loadXML(xmlStr); @@ -135,9 +187,9 @@ class BlocklyEditor extends React.Component { return parseXml(text); } - searchBlocks(text) { + searchBlocks(text: string): string[] { if (this.blocklyWorkspace) { - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); + const dom: Element = BlocklyEditor.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); const ids = searchXml(dom, text.toLowerCase()); console.log(`Search "${text}" found blocks: ${ids.length ? JSON.stringify(ids) : 'none'}`); @@ -148,29 +200,29 @@ class BlocklyEditor extends React.Component { return []; } - searchId() { - const ids = this.lastSearch && this.searchBlocks(this.lastSearch); - if (ids && ids.length) { + searchId(): void { + const ids = this.lastSearch ? this.searchBlocks(this.lastSearch) : null; + if (ids?.length) { this.someSelected = ids; - this.someSelected.forEach(id => this.blocklyWorkspace.highlightBlock(id, true)); + this.someSelected.forEach(id => this.blocklyWorkspace?.highlightBlock(id, true)); this.someSelectedTime = Date.now(); } else if (this.someSelected) { // remove selection - this.someSelected.forEach(id => this.blocklyWorkspace.highlightBlock(id, false)); + this.someSelected.forEach(id => this.blocklyWorkspace?.highlightBlock(id, false)); this.someSelected = null; } } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: BlocklyEditorProps): void { if (nextProps.command && this.lastCommand !== nextProps.command) { this.lastCommand = nextProps.command; - setTimeout(() => this.lastCommand = '', 300); + setTimeout(() => (this.lastCommand = ''), 300); if (this.lastCommand === 'check') { this.blocklyCheckBlocks((err, badBlock) => { if (!err) { this.setState({ message: I18n.t('Ok') }); } else { - badBlock && this.blocklyBlinkBlock(badBlock); + badBlock && BlocklyEditor.blocklyBlinkBlock(badBlock); this.setState({ error: { text: I18n.t(err), title: I18n.t('Error was found') } }); this.blinkBlock = badBlock; } @@ -198,7 +250,7 @@ class BlocklyEditor extends React.Component { } } - loadLanguages() { + loadLanguages(): void { // load blockly language if (!languageBlocklyLoaded) { const fileLang = window.document.createElement('script'); @@ -208,14 +260,7 @@ class BlocklyEditor extends React.Component { // most browsers fileLang.onload = () => { languageBlocklyLoaded = true; - this.setState({languageBlocklyLoaded}); - }; - // IE 6 & 7 - fileLang.onreadystatechange = () => { - if (this.readyState === 'complete') { - languageBlocklyLoaded = true; - this.setState({ languageBlocklyLoaded }); - } + this.setState({ languageBlocklyLoaded }); }; window.document.getElementsByTagName('head')[0].appendChild(fileLang); } @@ -226,24 +271,19 @@ class BlocklyEditor extends React.Component { // most browsers fileCustom.onload = () => { languageOwnLoaded = true; - this.setState({languageOwnLoaded}); - }; - // IE 6 & 7 - fileCustom.onreadystatechange = () => { - if (this.readyState === 'complete') { - languageOwnLoaded = true; - this.setState({ languageOwnLoaded }); - } + this.setState({ languageOwnLoaded }); }; window.document.getElementsByTagName('head')[0].appendChild(fileCustom); } } - onResize() { - this.Blockly.svgResize(this.blocklyWorkspace); + onResize(): void { + if (this.blocklyWorkspace) { + BlocklyEditor.Blockly.svgResize(this.blocklyWorkspace); + } } - jsCode2Blockly(text) { + static jsCode2Blockly(text: string | undefined): string | null { text = text || ''; const lines = text.split(/[\r\n]+|\r|\n/g); let xml = ''; @@ -268,14 +308,14 @@ class BlocklyEditor extends React.Component { return code; } - blocklyBlinkBlock(block) { + static blocklyBlinkBlock(block: BlockSvg): void { for (let i = 300; i < 3000; i += 300) { setTimeout(() => block.select(), i); setTimeout(() => block.unselect(), i + 150); } } - blocklyRemoveOrphanedShadows() { + blocklyRemoveOrphanedShadows(): void { if (this.blocklyWorkspace) { const blocks = this.blocklyWorkspace.getAllBlocks(); let block; @@ -295,11 +335,11 @@ class BlocklyEditor extends React.Component { } } - blocklyCheckBlocks(cb) { + blocklyCheckBlocks(cb: (warningText?: string, badBlock?: BlockSvg) => void): boolean { let warningText; if (!this.blocklyWorkspace || this.blocklyWorkspace.getAllBlocks().length === 0) { cb && cb('no blocks found'); - return; + return false; } let badBlock = this.blocklyGetUnconnectedBlock(); if (badBlock) { @@ -315,7 +355,7 @@ class BlocklyEditor extends React.Component { if (cb) { cb(warningText, badBlock); } else { - this.blocklyBlinkBlock(badBlock); + BlocklyEditor.blocklyBlinkBlock(badBlock); } return false; } @@ -326,15 +366,24 @@ class BlocklyEditor extends React.Component { } // get unconnected block - blocklyGetUnconnectedBlock() { - const blocks = this.blocklyWorkspace.getAllBlocks(); + blocklyGetUnconnectedBlock(): BlockSvg | null { + const blocks: BlockSvg[] | undefined = this.blocklyWorkspace?.getAllBlocks(); let block; - for (let i = 0; (block = blocks[i]); i++) { - const connections = block.getConnections_(true); - let conn; - for (let j = 0; (conn = connections[j]); j++) { - if (!conn.sourceBlock_ || ((conn.type === this.Blockly.INPUT_VALUE || conn.type === this.Blockly.OUTPUT_VALUE) && !conn.targetConnection && !conn._optional)) { - return block; + if (blocks) { + for (let i = 0; (block = blocks[i]); i++) { + const connections = block.getConnections_(true); + let conn; + for (let j = 0; (conn = connections[j]); j++) { + if ( + !conn.sourceBlock_ || + ((conn.type === BlocklyEditor.Blockly.INPUT_VALUE || + conn.type === BlocklyEditor.Blockly.OUTPUT_VALUE) && + !conn.targetConnection && + // @ts-expect-error Check it later + !conn._optional) + ) { + return block; + } } } } @@ -342,55 +391,68 @@ class BlocklyEditor extends React.Component { } // get block with warning - blocklyGetBlockWithWarning() { - const blocks = this.blocklyWorkspace.getAllBlocks(); + blocklyGetBlockWithWarning(): BlockSvg | null { + const blocks = this.blocklyWorkspace?.getAllBlocks(); let block; - for (let i = 0; (block = blocks[i]); i++) { - if (block.warning) { - return block; + if (blocks) { + for (let i = 0; (block = blocks[i]); i++) { + // @ts-expect-error fix later + if (block.warning) { + return block; + } } } return null; } - blocklyCode2JSCode(oneWay) { - let code = this.Blockly.JavaScript.workspaceToCode(this.blocklyWorkspace); + blocklyCode2JSCode(oneWay?: boolean): string { + if (!this.blocklyWorkspace) { + return ''; + } + let code = BlocklyEditor.Blockly.JavaScript.workspaceToCode(this.blocklyWorkspace); if (!oneWay) { code += '\n'; - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); - const text = this.Blockly.Xml.domToText(dom); + const dom = BlocklyEditor.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); + const text = BlocklyEditor.Blockly.Xml.domToText(dom); code += `//${btoa(encodeURIComponent(text))}`; } return code; } - exportBlocks() { - let exportText; - const selectedBlocks = this.Blockly.getSelected(); + exportBlocks(): void { + if (!this.blocklyWorkspace) { + return; + } + let exportText: string; + const selectedBlocks: BlockSvg | null = BlocklyEditor.Blockly.getSelected() as BlockSvg | null; if (selectedBlocks) { - const xmlBlock = this.Blockly.Xml.blockToDom(selectedBlocks); - if (this.Blockly.dragMode_ !== this.Blockly.DRAG_FREE) { - this.Blockly.Xml.deleteNext(xmlBlock); - } + const xmlBlock: Element = BlocklyEditor.Blockly.Xml.blockToDom(selectedBlocks) as Element; + // @1ts-expect-error fix later. TODO!!!! + // if (BlocklyEditor.Blockly.dragMode_ !== BlocklyEditor.Blockly.DRAG_FREE) { + // BlocklyEditor.Blockly.Xml.deleteNext(xmlBlock); + // } // Encode start position in XML. const xy = selectedBlocks.getRelativeToSurfaceXY(); - xmlBlock.setAttribute('x', selectedBlocks.RTL ? -xy.x : xy.x); - xmlBlock.setAttribute('y', xy.y); + xmlBlock.setAttribute('x', (selectedBlocks.RTL ? -xy.x : xy.x).toString()); + xmlBlock.setAttribute('y', xy.y.toString()); - exportText = this.Blockly.Xml.domToPrettyText(xmlBlock); + exportText = BlocklyEditor.Blockly.Xml.domToPrettyText(xmlBlock); } else { - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); - exportText = this.Blockly.Xml.domToPrettyText(dom); + const dom = BlocklyEditor.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); + exportText = BlocklyEditor.Blockly.Xml.domToPrettyText(dom); } this.setState({ exportText }); } - importBlocks() { + importBlocks(): void { this.setState({ importText: true }); } - onImportBlocks(xml) { + onImportBlocks(xml: string | undefined): void { + if (!this.blocklyWorkspace) { + return; + } xml = (xml || '').trim(); if (xml) { try { @@ -417,12 +479,14 @@ class BlocklyEditor extends React.Component { xml = xml.replace(/[\n\r]/g, '').replace(/.*<\/variables>/g, ''); window.scripts.loading = true; - const xmlBlocks = this.Blockly.utils.xml.textToDom(xml); + const xmlBlocks = BlocklyEditor.Blockly.utils.xml.textToDom(xml); if (xmlBlocks.nodeName === 'xml') { for (let b = 0; b < xmlBlocks.children.length; b++) { + // @ts-expect-error fix later this.blocklyWorkspace.paste(xmlBlocks.children[b]); } } else { + // @ts-expect-error fix later this.blocklyWorkspace.paste(xmlBlocks); } @@ -430,12 +494,12 @@ class BlocklyEditor extends React.Component { this.onBlocklyChanged(); } catch (e) { - this.setState({ error: { text: e, title: I18n.t('Import error') } }); + this.setState({ error: { text: (e as Error).toString(), title: I18n.t('Import error') } }); } } } - loadCode() { + loadCode(): void { if (!this.blocklyWorkspace) { return; } @@ -444,25 +508,27 @@ class BlocklyEditor extends React.Component { this.blocklyWorkspace.clear(); try { - const xml = this.jsCode2Blockly(this.originalCode) || ''; + const xml = + BlocklyEditor.jsCode2Blockly(this.originalCode) || + ''; window.scripts.loading = true; - const dom = this.Blockly.utils.xml.textToDom(xml); - this.Blockly.Xml.domToWorkspace(dom, this.blocklyWorkspace); + const dom = BlocklyEditor.Blockly.utils.xml.textToDom(xml); + BlocklyEditor.Blockly.Xml.domToWorkspace(dom, this.blocklyWorkspace); window.scripts.loading = false; } catch (e) { console.error(e); setTimeout(() => this.setState({ error: I18n.t('Cannot extract Blockly code!') })); } - setTimeout(() => this.ignoreChanges = false, 100); + setTimeout(() => (this.ignoreChanges = false), 100); } - onBlocklyChanged() { + onBlocklyChanged(): void { this.blocklyRemoveOrphanedShadows(); this.setState({ changed: true }); this.onChange(); } - async componentDidUpdate() { + async componentDidUpdate(): Promise { if (!this.blockly) { return; } @@ -477,10 +543,11 @@ class BlocklyEditor extends React.Component { window.addEventListener('resize', this.onResizeBind, false); toolboxText = toolboxText || (await this.getToolbox()); - toolboxXml = toolboxXml || this.Blockly.utils.xml.textToDom(toolboxText); + toolboxXml = toolboxXml || BlocklyEditor.Blockly.utils.xml.textToDom(toolboxText); - this.darkTheme = this.Blockly.Theme.defineTheme('dark', { - base: this.Blockly.Themes.Classic, + this.darkTheme = BlocklyEditor.Blockly.Theme.defineTheme('dark', { + name: 'dark', + base: BlocklyEditor.Blockly.Themes.Classic, componentStyles: { workspaceBackgroundColour: '#1e1e1e', toolboxBackgroundColour: 'blackBackground', @@ -493,62 +560,64 @@ class BlocklyEditor extends React.Component { insertionMarkerOpacity: 0.3, scrollbarOpacity: 0.4, cursorColour: '#d0d0d0', - blackBackground: '#333', }, }); // https://developers.google.com/blockly/reference/js/blockly.blocklyoptions_interface.md - this.blocklyWorkspace = this.Blockly.inject( - this.blockly, - { - renderer: 'thrasos', - theme: 'classic', - media: 'google-blockly/media/', - toolbox: toolboxXml, - zoom: { - controls: true, - wheel: false, - startScale: 1.0, - maxScale: 3, - minScale: 0.3, - scaleSpeed: 1.2, - pinch: true, - }, - move: { - scrollbars: { - horizontal: true, - vertical: true, - }, - drag: true, - wheel: true, - }, - trashcan: true, - grid: { - spacing: 25, - length: 1, - snap: true, + this.blocklyWorkspace = BlocklyEditor.Blockly.inject(this.blockly, { + renderer: 'thrasos', + theme: 'classic', + media: 'google-blockly/media/', + toolbox: toolboxXml, + zoom: { + controls: true, + wheel: false, + startScale: 1.0, + maxScale: 3, + minScale: 0.3, + scaleSpeed: 1.2, + pinch: true, + }, + move: { + scrollbars: { + horizontal: true, + vertical: true, }, - sounds: false, // disable sounds + drag: true, + wheel: true, }, - ); + trashcan: true, + grid: { + spacing: 25, + length: 1, + snap: true, + }, + sounds: false, // disable sounds + }); // for blockly itself window.scripts = { blocklyWorkspace: this.blocklyWorkspace, }; // Workaround: Replace procedure category flyout - this.blocklyWorkspace.registerToolboxCategoryCallback('PROCEDURE', this.Blockly.Procedures.flyoutCategoryNew); + this.blocklyWorkspace.registerToolboxCategoryCallback('PROCEDURE', window.Blockly.Procedures.flyoutCategoryNew); // Listen to events on master workspace. this.blocklyWorkspace.addChangeListener(masterEvent => { if (this.someSelected && Date.now() - this.someSelectedTime > 500) { - const allBlocks = this.blocklyWorkspace.getAllBlocks(); + const allBlocks = this.blocklyWorkspace?.getAllBlocks(); this.someSelected = null; - allBlocks.forEach(b => b.removeSelect()); + allBlocks?.forEach(b => b.removeSelect()); } - if ([this.Blockly.Events.UI, this.Blockly.Events.CREATE, this.Blockly.Events.VIEWPORT_CHANGE].includes(masterEvent.type)) { - return; // Don't mirror UI events. + if ( + [ + BlocklyEditor.Blockly.Events.UI, + BlocklyEditor.Blockly.Events.CREATE, + BlocklyEditor.Blockly.Events.VIEWPORT_CHANGE, + ].includes(masterEvent.type as 'ui' | 'create' | 'viewport_change') + ) { + return; // Don't mirror UI events. } if (this.ignoreChanges) { return; @@ -570,19 +639,20 @@ class BlocklyEditor extends React.Component { setTimeout(() => this.searchId(), 200); // select found blocks } - updateBackground() { - if (this.state.themeType === 'dark' || this.state.themeType === 'blue') { - this.blocklyWorkspace.setTheme(this.darkTheme); - } else { + updateBackground(): void { + if (this.state.themeType === 'dark') { + this.blocklyWorkspace?.setTheme(this.darkTheme); + } else if (this.blocklyWorkspace) { this.blocklyWorkspace.getThemeManager(); - this.blocklyWorkspace.setTheme(this.Blockly.Themes.Classic); + this.blocklyWorkspace.setTheme(BlocklyEditor.Blockly.Themes.Classic); } } - componentWillUnmount() { + componentWillUnmount(): void { if (!this.blocklyWorkspace) { return; } + this.blocklyWorkspace.dispose(); this.blocklyWorkspace = null; this.changeTimer && clearTimeout(this.changeTimer); @@ -590,18 +660,20 @@ class BlocklyEditor extends React.Component { window.removeEventListener('resize', this.onResizeBind); } - onChange() { + onChange(): void { this.originalCode = this.blocklyCode2JSCode(); this.props.onChange && this.props.onChange(this.originalCode); } - async getToolbox(retry) { + async getToolbox(retry?: boolean): Promise { // Interpolate translated messages into toolbox. - const el = window.document.getElementById('toolbox'); - let toolboxText = el && el.outerHTML; + const el = window.document.getElementById('toolbox'); + let toolboxText = el?.outerHTML; if (!toolboxText) { if (!retry) { - return new Promise(resolve => { setTimeout(() => resolve(this.getToolbox(true)), 500); }); + return new Promise(resolve => { + setTimeout(() => resolve(this.getToolbox(true)), 500); + }); } console.error('Cannot load blocks!'); @@ -609,16 +681,17 @@ class BlocklyEditor extends React.Component { } toolboxText = toolboxText.replace(/{(\w+)}/g, (m, p1) => window.MSG[p1]); - if (this.Blockly.CustomBlocks) { + if (window.Blockly.CustomBlocks) { let blocks = ''; const lang = I18n.getLanguage(); - for (let cb = 0; cb < this.Blockly.CustomBlocks.length; cb++) { - const name = this.Blockly.CustomBlocks[cb]; + for (let cb = 0; cb < window.Blockly.CustomBlocks.length; cb++) { + const name = window.Blockly.CustomBlocks[cb]; // add blocks - blocks += ``; - for (const _b in this.Blockly[name].blocks) { - if (Object.prototype.hasOwnProperty.call(this.Blockly[name].blocks, _b)) { - blocks += this.Blockly[name].blocks[_b]; + const _block: CustomBlock = (window.Blockly as unknown as Record)[name]; + blocks += ``; + for (const _b in _block.blocks) { + if (Object.prototype.hasOwnProperty.call(_block.blocks, _b)) { + blocks += _block.blocks[_b]; } } blocks += ''; @@ -629,58 +702,159 @@ class BlocklyEditor extends React.Component { return toolboxText; } - renderMessageDialog() { - return this.state.message ? + renderMessageDialog(): React.JSX.Element | null { + return this.state.message ? ( this.setState({ message: '' })} - /> : - null; + /> + ) : null; } - renderErrorDialog() { - return this.state.error ? + renderErrorDialog(): React.JSX.Element | null { + return this.state.error ? ( { if (this.blinkBlock) { - this.blocklyBlinkBlock(this.blinkBlock); + BlocklyEditor.blocklyBlinkBlock(this.blinkBlock); this.blinkBlock = null; } this.setState({ error: '' }); - }}/> : - null; - } - - renderExportDialog() { - return this.state.exportText ? this.setState({ exportText: '' })} text={this.state.exportText} scriptId={this.props.scriptId} /> : null; - } - - renderImportDialog() { - return this.state.importText ? { - this.setState({ importText: false }); - this.onImportBlocks(text); - }} - /> : null; + }} + /> + ) : null; + } + + renderExportDialog(): React.JSX.Element | null { + return this.state.exportText ? ( + this.setState({ exportText: '' })} + text={this.state.exportText} + scriptId={this.props.scriptId} + /> + ) : null; + } + + renderImportDialog(): React.JSX.Element | null { + return this.state.importText ? ( + { + this.setState({ importText: false }); + this.onImportBlocks(text); + }} + /> + ) : null; + } + + renderDialogPrompt(): React.JSX.Element | null { + if (!this.state.showInputPrompt) { + return null; + } + return ( + { + const cb = this.state.showInputPrompt?.callback; + if (cb) { + cb(null); + } + this.setState({ showInputPrompt: null }); + }} + maxWidth="sm" + fullWidth + open={!0} + > + {this.state.showInputPrompt.promptText} + + { + if (e.key === 'Enter') { + const cb = this.state.showInputPrompt?.callback; + const value = this.state.showInputPrompt?.value; + if (cb) { + cb(value === undefined ? null : value); + } + this.setState({ showInputPrompt: null }); + } + }} + onChange={e => { + const showInputPrompt: { + promptText: string; + defaultText: string; + callback: (p1: string | null) => void; + value: string; + } = { ...this.state.showInputPrompt } as { + promptText: string; + defaultText: string; + callback: (p1: string | null) => void; + value: string; + }; + if (this.state.showInputPrompt?.callback) { + showInputPrompt.callback = this.state.showInputPrompt?.callback; + } + showInputPrompt.value = e.target.value; + this.setState({ showInputPrompt }); + }} + /> + + + + + + + ); } - render() { + render(): (React.JSX.Element | null)[] | null { if (this.state.languageBlocklyLoaded && this.state.languageOwnLoaded) { this.didUpdate = setTimeout(() => { this.didUpdate = null; - this.componentDidUpdate(); + void this.componentDidUpdate(); }, 100); return [
this.blockly = el} + ref={el => (this.blockly = el)} style={{ // marginLeft: 180, width: '100%', // 'calc(100% - 180px)', @@ -690,6 +864,7 @@ class BlocklyEditor extends React.Component { }} />, + this.renderDialogPrompt(), this.renderMessageDialog(), this.renderErrorDialog(), this.renderExportDialog(), @@ -701,12 +876,4 @@ class BlocklyEditor extends React.Component { } } -BlocklyEditor.propTypes = { - command: PropTypes.string, - onChange: PropTypes.func, - searchText: PropTypes.string, - scriptId: PropTypes.string, - themeType: PropTypes.string, -}; - export default BlocklyEditor; diff --git a/src-editor/src/Components/Debugger/Console.tsx b/src-editor/src/Components/Debugger/Console.tsx index 558b6cbc..81675cf6 100644 --- a/src-editor/src/Components/Debugger/Console.tsx +++ b/src-editor/src/Components/Debugger/Console.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Box, IconButton } from '@mui/material'; @@ -9,18 +8,18 @@ import { MdVerticalAlignBottom as IconBottom, } from 'react-icons/md'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; +import { I18n, type IobTheme, Utils } from '@iobroker/adapter-react-v5'; const TOOLBOX_WIDTH = 34; -const styles = { +const styles: Record = { logBox: { width: '100%', height: '100%', position: 'relative', overflow: 'hidden', }, - logBoxInner: theme => ({ + logBoxInner: (theme: IobTheme): React.CSSProperties => ({ display: 'inline-block', color: theme.palette.mode === 'dark' ? 'white' : 'black', width: `calc(100% - ${TOOLBOX_WIDTH}px)`, @@ -30,24 +29,24 @@ const styles = { position: 'relative', verticalAlign: 'top', }), - info: theme => ({ + info: (theme: IobTheme): React.CSSProperties => ({ background: theme.palette.mode === 'dark' ? 'darkgrey' : 'lightgrey', - color: theme.palette.mode === 'dark' ? 'black' : 'black', + color: theme.palette.mode === 'dark' ? 'black' : 'black', }), - error: theme => ({ + error: (theme: IobTheme): React.CSSProperties => ({ background: '#FF0000', - color: theme.palette.mode === 'dark' ? 'black' : 'white', + color: theme.palette.mode === 'dark' ? 'black' : 'white', }), - warn: theme => ({ + warn: (theme: IobTheme): React.CSSProperties => ({ background: '#FF8000', - color: theme.palette.mode === 'dark' ? 'black' : 'white', + color: theme.palette.mode === 'dark' ? 'black' : 'white', }), - debug: theme => ({ + debug: (theme: IobTheme): React.CSSProperties => ({ background: 'gray', opacity: 0.8, - color: theme.palette.mode === 'dark' ? 'black' : 'white', + color: theme.palette.mode === 'dark' ? 'black' : 'white', }), - silly: theme => ({ + silly: (theme: IobTheme): React.CSSProperties => ({ background: 'gray', opacity: 0.6, color: theme.palette.mode === 'dark' ? 'black' : 'white', @@ -63,7 +62,8 @@ const styles = { //marginLeft: 2, width: TOOLBOX_WIDTH, height: '100%', - boxShadow: '2px 0px 4px -1px rgba(0, 0, 0, 0.2), 4px 0px 5px 0px rgba(0, 0, 0, 0.14), 1px 0px 10px 0px rgba(0, 0, 0, 0.12)', + boxShadow: + '2px 0px 4px -1px rgba(0, 0, 0, 0.2), 4px 0px 5px 0px rgba(0, 0, 0, 0.14), 1px 0px 10px 0px rgba(0, 0, 0, 0.12)', display: 'inline-block', verticalAlign: 'top', overflow: 'hidden', @@ -82,9 +82,9 @@ const styles = { }, }; -function getTimeString(d) { +function getTimeString(d: Date): string { let text; - let i = d.getHours(); + let i: string | number = d.getHours(); if (i < 10) { i = `0${i.toString()}`; } @@ -94,7 +94,7 @@ function getTimeString(d) { if (i < 10) { i = `0${i.toString()}`; } - text += i + ':'; + text += `${i}:`; i = d.getSeconds(); if (i < 10) { i = `0${i.toString()}`; @@ -110,88 +110,123 @@ function getTimeString(d) { return text; } -class Console extends React.Component { - constructor(props) { +interface ConsoleProps { + onClearAllLogs: () => void; + console: { ts: number; text: string; severity: ioBroker.LogLevel }[]; +} + +interface ConsoleState { + goBottom: boolean; +} + +class Console extends React.Component { + private readonly messagesEnd: React.RefObject; + + constructor(props: ConsoleProps) { super(props); this.state = { - lines: {}, goBottom: true, }; this.messagesEnd = React.createRef(); } - generateLine(message) { - return - {getTimeString(new Date(message.ts))} - {message.severity} - {message.text} - ; + + static generateLine(message: { ts: number; text: string; severity: ioBroker.LogLevel }): React.JSX.Element { + return ( + + {getTimeString(new Date(message.ts))} + {message.severity} + {message.text} + + ); } - renderLogList(lines) { - if (lines && lines.length) { - return - - - {lines.map(line => this.generateLine(line))} - -
-
- ; + + renderLogList(lines: { ts: number; text: string; severity: ioBroker.LogLevel }[]): React.JSX.Element { + if (lines?.length) { + return ( + + + {lines.map(line => Console.generateLine(line))} +
+
+ + ); } - return {I18n.t('Log outputs')}; + return ( + + {I18n.t('Log outputs')} + + ); } - onCopy() { + onCopy(): void { Utils.copyToClipboard(this.props.console.join('\n')); } - scrollToBottom() { - this.messagesEnd && this.messagesEnd.current && this.messagesEnd.current.scrollIntoView({behavior: 'smooth'}); + scrollToBottom(): void { + this.messagesEnd?.current?.scrollIntoView({ behavior: 'smooth' }); } - componentDidUpdate() { + componentDidUpdate(): void { this.state.goBottom && this.scrollToBottom(); } - render() { + render(): React.JSX.Element { const lines = this.props.console; - return
-
- this.setState({ goBottom: !this.state.goBottom })} - color={this.state.goBottom ? 'secondary' : ''} - size="medium" - > - - - {lines && lines.length ? this.props.onClearAllLogs()} - size="medium" + return ( +
+
- - : null} - {lines && lines.length ? this.onCopy()} - size="medium" - > - - : null} + this.setState({ goBottom: !this.state.goBottom })} + color={this.state.goBottom ? 'secondary' : undefined} + size="medium" + > + + + {lines?.length ? ( + this.props.onClearAllLogs()} + size="medium" + > + + + ) : null} + {lines?.length ? ( + this.onCopy()} + size="medium" + > + + + ) : null} +
+ {this.renderLogList(lines)}
- {this.renderLogList(lines)} -
; + ); } } -Console.propTypes = { - theme: PropTypes.object, - onClearAllLogs: PropTypes.func, - console: PropTypes.array, -}; - export default Console; diff --git a/src-editor/src/Components/Debugger/Editor.tsx b/src-editor/src/Components/Debugger/Editor.tsx index 548ea271..f792fde3 100644 --- a/src-editor/src/Components/Debugger/Editor.tsx +++ b/src-editor/src/Components/Debugger/Editor.tsx @@ -1,65 +1,69 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ScriptEditorComponent from '../ScriptEditorVanilaMonaco'; +import type { AdminConnection, ThemeName, ThemeType } from '@iobroker/adapter-react-v5'; -const styles = { +import type { DebuggerLocation, SetBreakpointParameterType } from './types'; + +const styles: Record = { editorDiv: { height: '100%', width: '100%', overflow: 'hidden', - position: 'relative' + position: 'relative', }, }; -class Editor extends React.Component { - constructor(props) { +interface EditorProps { + runningInstances: Record; + socket: AdminConnection; + sourceId: string | null; + script: string; + scriptName: string; + adapterName: string; + paused: boolean; + breakpoints: SetBreakpointParameterType[]; + location: DebuggerLocation | null; + themeType: ThemeType; + themeName: ThemeName; + onToggleBreakpoint: (i: number) => void; +} + +interface EditorState { + lines: string[]; +} + +class Editor extends React.Component { + constructor(props: EditorProps) { super(props); this.state = { - lines: (this.props.script || '').split(/\r\n|\n/) + lines: (this.props.script || '').split(/\r\n|\n/), }; } - editorDidMount(editor, monaco) { - this.monaco = monaco; - this.editor = editor; - editor.focus(); - } - - render() { - return
- this.props.onToggleBreakpoint(i)} - /> -
; + render(): React.JSX.Element { + return ( +
+ this.props.onToggleBreakpoint(i)} + /> +
+ ); } } -Editor.propTypes = { - runningInstances: PropTypes.object, - socket: PropTypes.object, - sourceId: PropTypes.string, - script: PropTypes.string, - scriptName: PropTypes.string, - adapterName: PropTypes.string, - paused: PropTypes.bool, - breakpoints: PropTypes.array, - location: PropTypes.object, - themeType: PropTypes.string, - themeName: PropTypes.string, - onToggleBreakpoint: PropTypes.func, -}; - export default Editor; diff --git a/src-editor/src/Components/Debugger/Stack.tsx b/src-editor/src/Components/Debugger/Stack.tsx index 7e34a46d..583fc881 100644 --- a/src-editor/src/Components/Debugger/Stack.tsx +++ b/src-editor/src/Components/Debugger/Stack.tsx @@ -1,27 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ReactSplit, { SplitDirection } from '@devbookhq/splitter'; -import ReactJson from 'react-json-view'; - -import { - ListItemButton, - ListItemText, - Input, - InputAdornment, - IconButton, - List, Box, -} from '@mui/material'; - -import { - MdCheck as CheckIcon, - MdAdd as IconAdd, - MdDelete as IconDelete, -} from 'react-icons/md'; - -import { I18n } from '@iobroker/adapter-react-v5'; - -const styles = { +import ReactJson from 'react-json-view'; + +import { ListItemButton, ListItemText, Input, InputAdornment, IconButton, List, Box } from '@mui/material'; + +import { MdCheck as CheckIcon, MdAdd as IconAdd, MdDelete as IconDelete } from 'react-icons/md'; + +import { I18n, type IobTheme, type ThemeType } from '@iobroker/adapter-react-v5'; +import type { DebugScopes, CallFrame, DebugValue, DebugVariable, DebugObject } from './types'; + +const styles: Record = { frameRoot: { paddingTop: 0, paddingBottom: 0, @@ -29,7 +18,7 @@ const styles = { frameTextRoot: { m: 0, }, - frameTextPrimary: theme => ({ + frameTextPrimary: (theme: IobTheme): React.CSSProperties => ({ color: theme.palette.mode === 'dark' ? '#CCC' : '#333', }), frameTextSecondary: { @@ -50,13 +39,13 @@ const styles = { width: 50, }, scopeType_local: { - color: '#53a944' + color: '#53a944', }, scopeType_closure: { - color: '#365b80' + color: '#365b80', }, scopeType_user: { - color: '#a48a15' + color: '#a48a15', }, scopeName: { color: '#bc5b5b', @@ -65,23 +54,23 @@ const styles = { textOverflow: 'ellipsis', }, scopeButton: { - width: 32 + width: 32, }, scopeValueEditable: { - cursor: 'pointer' + cursor: 'pointer', }, selectedFrame: { backgroundColor: '#777', - color: 'white' + color: 'white', }, splitter: { width: '100%', height: 'calc(100% - 36px)', overflow: 'hidden', - fontSize: 12 + fontSize: 12, }, - toolbarScopes: theme => ({ + toolbarScopes: (theme: IobTheme): React.CSSProperties => ({ width: 24, display: 'inline-block', height: '100%', @@ -100,7 +89,7 @@ const styles = { display: 'inline-block', verticalAlign: 'top', }, - scopeNameEqual: theme => ({ + scopeNameEqual: (theme: IobTheme): React.CSSProperties => ({ display: 'inline-block', color: theme.palette.mode === 'dark' ? '#EEE' : '#222', verticalAlign: 'top', @@ -117,33 +106,116 @@ const styles = { }, valueNull: { - color: '#a44a24' + color: '#a44a24', }, valueUndefined: { - color: '#a44a24' + color: '#a44a24', }, valueString: { - color: '#1e8816' + color: '#1e8816', }, valueNumber: { - color: '#163c88' + color: '#163c88', }, valueBoolean: { - color: '#a44a24' + color: '#a44a24', }, valueObject: { - color: '#721b70' + color: '#721b70', }, valueNone: { - color: '#8a8a8a' + color: '#8a8a8a', }, valueFunc: { - color: '#ac4343' + color: '#ac4343', }, + error: (theme: IobTheme): React.CSSProperties => ({ + color: theme.palette.mode === 'dark' ? '#FF8080' : '#FF0000', + fontStyle: 'italic', + }), }; -class Stack extends React.Component { - constructor(props) { +interface StackProps { + currentScriptId: string | null; + mainScriptId?: string; + scopes: DebugScopes | null; + expressions: DebugVariable[]; + callFrames: CallFrame[] | undefined; + currentFrame: number; + onExpressionDelete: (index: number) => void; + onChangeCurrentFrame: (index: number) => void; + onWriteScopeValue: (options: { + variableName: string; + scopeNumber: 0; + newValue: { + value: any; + valueType: + | 'string' + | 'number' + | 'object' + | 'boolean' + | 'undefined' + | 'null' + | 'bigint' + | 'symbol' + | 'function'; + }; + callFrameId: string | undefined; + }) => void; + onExpressionAdd: (cb: (index: number, item: DebugVariable) => void) => void; + onExpressionNameUpdate: (index: number, scopeValue: string, cb: () => void) => void; + themeType: ThemeType; +} + +interface StackState { + editValue: { + type: 'expression' | 'local' | 'closure' | 'global'; + valueType: 'function' | 'string' | 'boolean' | 'number' | 'object' | 'undefined' | 'null' | 'bigint' | 'symbol'; + index: number; + name: string; + value: string; + scopeId?: string; + } | null; + callFrames: CallFrame[] | undefined; + framesSizes: number[]; +} + +function previewToObject(obj: DebugObject): Record | string { + const result: Record = {}; + if (obj.className === 'ReferenceError') { + return obj.description; + } + obj.preview?.properties?.forEach(item => { + if (item?.type === 'object') { + if (item.subtype === 'null') { + result[item.name] = null; + } else { + result[item.name] = `{ ${item.value === 'Object' ? '...' : item.value} }`; + } + } else { + if (item.type === 'boolean') { + result[item.name || item.description] = item.value === 'true'; + } else if (item.type === 'number') { + result[item.name || item.description] = parseFloat(item.value); + } else if (item.type === 'function') { + result[item.name || item.description] = 'function(){}'; + } else if (item.type === 'undefined') { + result[item.name || item.description] = undefined; + } else { + result[item.name || item.description] = item.value; + } + } + }); + + return result; +} + +class Stack extends React.Component { + private readonly editRef: React.RefObject; + + private scopeValue: boolean | undefined | number | string | null = null; + + constructor(props: StackProps) { super(props); const framesSizesStr = window.localStorage.getItem('JS.framesSizes'); @@ -151,7 +223,7 @@ class Stack extends React.Component { if (framesSizesStr) { try { framesSizes = JSON.parse(framesSizesStr); - } catch (e) { + } catch { // ignore } } @@ -165,154 +237,230 @@ class Stack extends React.Component { this.editRef = React.createRef(); } - onExpressionNameUpdate() { - this.props.onExpressionNameUpdate(this.state.editValue.index, this.scopeValue, () => { - this.setState({ editValue: null }); - this.scopeValue = null; - }); + onExpressionNameUpdate(): void { + if (this.state.editValue) { + this.props.onExpressionNameUpdate(this.state.editValue.index, this.scopeValue as string, () => { + this.setState({ editValue: null }); + this.scopeValue = null; + }); + } } - renderExpression(item, i) { - const name = this.state.editValue && this.state.editValue.type === 'expression' && this.state.editValue.index === i ? - this.state.editValue && this.setState({editValue: null})} - defaultValue={item.name} - onKeyUp={e => { - if (e.keyCode === 13) { - this.onExpressionNameUpdate(); - } else if (e.keyCode === 27) { - this.setState({editValue: null}); - } - }} - - onChange={e => - this.scopeValue = e.target.value} - - endAdornment={ - - this.onExpressionNameUpdate()} size="medium"> - - - - } - /> - : - [ -
{item.name}
, - = , -
{this.formatValue(item.value)}
- ]; - - return - user - { - this.scopeValue = item.name || ''; - this.setState({ - editValue: { - type: 'expression', - valueType: 'string', - index: i, - name: item.name, - value: item.name || '' + renderExpression(item: DebugVariable, i: number): React.JSX.Element { + const name = + this.state.editValue && this.state.editValue.type === 'expression' && this.state.editValue.index === i ? ( + this.state.editValue && this.setState({ editValue: null })} + defaultValue={item.name} + onKeyUp={e => { + if (e.key === 'Enter') { + this.onExpressionNameUpdate(); + } else if (e.key === 'Escape') { + this.setState({ editValue: null }); } - }); - }} - >{name} - this.props.onExpressionDelete(i)} - > - - - + }} + onChange={e => (this.scopeValue = e.target.value)} + endAdornment={ + + this.onExpressionNameUpdate()} + size="medium" + > + + + + } + /> + ) : ( + [ +
+ {item.name} +
, + + {' '} + ={' '} + , +
+ {this.formatValue(item.value)} +
, + ] + ); + + return ( + + user + { + this.scopeValue = item.name || ''; + this.setState({ + editValue: { + type: 'expression', + valueType: 'string', + index: i, + name: item.name, + value: item.name || '', + }, + }); + }} + > + {name} + + this.props.onExpressionDelete(i)} + > + + + + ); } - renderExpressions() { + renderExpressions(): React.JSX.Element[] { return this.props.expressions.map((item, i) => this.renderExpression(item, i)); } - renderOneFrameTitle(frame, i) { - if (this.props.mainScriptId === this.props.currentScriptId && frame.location.scriptId !== this.props.mainScriptId) { + renderOneFrameTitle(frame: CallFrame, i: number): React.JSX.Element | null { + if ( + this.props.mainScriptId === this.props.currentScriptId && + frame.location.scriptId !== this.props.mainScriptId + ) { return null; } - const fileName = frame.url.split('/').pop().replace(/^script\.js\./, ''); - return this.props.onChangeCurrentFrame(i)} - dense - selected={this.props.currentFrame === i} - style={styles.frameRoot} - > - - ; + const fileName = (frame.url.split('/').pop() || '').replace(/^script\.js\./, ''); + return ( + this.props.onChangeCurrentFrame(i)} + dense + selected={this.props.currentFrame === i} + style={styles.frameRoot} + > + + + ); } - formatValue(value, forEdit) { + formatValue(value: DebugValue | DebugObject | null, forEdit?: boolean): React.JSX.Element | string { if (!value) { if (forEdit) { return 'none'; } return none; - } else if (value.type === 'function') { - const text = value.description ? (value.description.length > 100 ? value.description.substring(0, 100) + '...' : value.description) : 'function'; + } + if (value.type === 'undefined') { + if (forEdit) { + return 'undefined'; + } + return undefined; + } + if (value.type === 'null') { + if (forEdit) { + return 'null'; + } + return null; + } + if (value.type === 'function') { + const text = value.description + ? value.description.length > 100 + ? `${value.description.substring(0, 100)}...` + : value.description + : 'function'; if (forEdit) { return text; } - return {text}; - } else if (value.value === undefined) { + return ( + + {text} + + ); + } + if (value.type === 'object') { + const val = previewToObject(value as DebugObject); + if (forEdit) { + return JSON.stringify(val); + } + if (typeof val === 'string') { + return {val}; + } + return ( + + ); + } + if (value.value === undefined) { if (forEdit) { return 'undefined'; } return undefined; - } else if (value.value === null) { + } + if (value.value === null) { if (forEdit) { return 'null'; } return null; - } else if (value.type === 'string') { + } + if (value.type === 'string') { if (forEdit) { return value.value; } - const text = value.value ? (value.value.length > 100 ? value.value.substring(0, 100) + '...' : value.value) : ''; - return "{text}"; - } else if (value.type === 'boolean') { + const text = `"${ + value.value ? (value.value.length > 100 ? `${value.value.substring(0, 100)}...` : value.value) : '' + }"`; + return ( + + {text} + + ); + } + if (value.type === 'boolean') { if (forEdit) { return value.value.toString(); } return {value.value.toString()}; - } else if (value.type === 'object') { - if (forEdit) { - return JSON.stringify(value.value); - } - return ; } return value.value.toString(); } - onWriteScopeValue() { + onWriteScopeValue(): void { if (this.scopeValue === 'true') { this.scopeValue = true; } else if (this.scopeValue === 'false') { @@ -321,170 +469,206 @@ class Stack extends React.Component { this.scopeValue = null; } else if (this.scopeValue === 'undefined') { this.scopeValue = undefined; - } else if (parseFloat(this.scopeValue).toString() === this.scopeValue) { + } else if (parseFloat(this.scopeValue as string).toString() === this.scopeValue) { this.scopeValue = parseFloat(this.scopeValue); } this.props.onWriteScopeValue({ - variableName: this.state.editValue.name, + variableName: this.state.editValue?.name || '', scopeNumber: 0, newValue: { value: this.scopeValue, valueType: typeof this.scopeValue, }, - callFrameId: this.props.callFrames[this.props.currentFrame].callFrameId + callFrameId: this.props.callFrames?.[this.props.currentFrame].callFrameId, }); this.setState({ editValue: null }); this.scopeValue = null; } - componentDidUpdate() { + componentDidUpdate(): void { //this.editRef.current?.select(); this.editRef.current?.focus(); } - renderScope(scopeId, item, type) { - const editable = !this.props.currentFrame && item.value && (item.value.type === 'undefined' || item.value.type === 'string' || item.value.type === 'number' || item.value.type === 'boolean' || item.value?.value === null || item.value?.value === undefined); - - const el = this.state.editValue && this.state.editValue.type === type && this.state.editValue.name === item.name ? - [ -
{item.name}
, - = , - this.state.editValue && this.setState({editValue: null})} - defaultValue={this.formatValue(item.value, true)} - onKeyUp={e => { - if (e.keyCode === 13) { - this.onWriteScopeValue() - } else if (e.keyCode === 27) { - this.setState({editValue: null}) + renderScope(scopeId: string, item: DebugVariable, type: 'global' | 'local' | 'closure'): React.JSX.Element { + const editable = + !this.props.currentFrame && + item.value && + (item.value.type === 'undefined' || + item.value.type === 'string' || + item.value.type === 'number' || + item.value.type === 'boolean' || + (item.value as DebugValue)?.value === null || + ((item.value as DebugValue)?.value === undefined && item.value.type !== 'object')); + + const el = + this.state.editValue?.type === type && this.state.editValue?.name === item.name + ? [ +
+ {item.name} +
, + + {' '} + ={' '} + , + this.state.editValue && this.setState({ editValue: null })} + defaultValue={this.formatValue(item.value, true)} + onKeyUp={e => { + if (e.key === 'Enter') { + this.onWriteScopeValue(); + } else if (e.key === 'Escape') { + this.setState({ editValue: null }); + } + }} + onChange={e => (this.scopeValue = e.target.value)} + endAdornment={ + + this.onWriteScopeValue()} + size="medium" + > + + + + } + />, + ] + : [ +
+ {item.name} +
, + + {' '} + ={' '} + , +
+ {this.formatValue(item.value)} ({item.value.type}) +
, + ]; + + return ( + + {type} + { + if (editable) { + this.scopeValue = (item.value as DebugValue).value; + this.setState({ + editValue: { + scopeId, + type, + index: 0, + valueType: item.value.type, + name: item.name, + value: (item.value as DebugValue).value, + }, + }); } }} - onChange={e => - this.scopeValue = e.target.value} - endAdornment={ - - this.onWriteScopeValue()} size="medium"> - - - - } - /> - ] - : - [ -
{item.name}
, - = , -
{this.formatValue(item.value)} ({item.value.type})
- ]; - - - return - {type} - { - if (editable) { - this.scopeValue = item.value.value; - this.setState({ - editValue: { - scopeId, - type, - valueType: item.value.type, - name: item.name, - value: item.value.value - } - }); - } - }} - >{el} - ; + > + {el} + + + ); } - renderScopes(frame) { + renderScopes(frame: CallFrame): React.JSX.Element | null { if (!frame) { return null; } // first local - let result = this.renderExpressions(); - - let items = this.props.scopes?.local?.properties?.result.map(item => this.renderScope(this.props.scopes.id, item, 'local')); - items && items.forEach(item => result.push(item)); - - items = this.props.scopes?.closure?.properties?.result.map(item => this.renderScope(this.props.scopes.id, item, 'closure')); - items && items.forEach(item => result.push(item)); - - return - - {result} - -
; + const result: React.JSX.Element[] = this.renderExpressions(); + + let items = this.props.scopes?.local?.properties?.result.map( + item => this.props.scopes && this.renderScope('', item, 'local'), + ); + items?.forEach(item => item && result.push(item)); + + items = this.props.scopes?.closure?.properties?.result.map( + item => this.props.scopes && this.renderScope('', item, 'closure'), + ); + items?.forEach(item => item && result.push(item)); + + return ( + + {result} +
+ ); } - render() { - return { - this.setState({ framesSizes }); - window.localStorage.setItem('JS.framesSizes', JSON.stringify(framesSizes)); - }} - gutterClassName={this.props.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} - > -
- - {this.props.callFrames ? this.props.callFrames.map((frame, i) => - this.renderOneFrameTitle(frame, i)) : null} - -
-
- - this.props.onExpressionAdd((i, item) => { - this.scopeValue = item.name || ''; - this.setState({ - editValue: { - type: 'expression', - valueType: 'string', - index: i, - name: item.name, - value: item.name || '', - } - }); - })} - > - - - -
- {this.props.callFrames && this.props.callFrames.length && this.renderScopes(this.props.callFrames[this.props.currentFrame])} + render(): React.JSX.Element { + return ( + { + this.setState({ framesSizes }); + window.localStorage.setItem('JS.framesSizes', JSON.stringify(framesSizes)); + }} + gutterClassName={this.props.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} + > +
+ + {this.props.callFrames?.map((frame, i) => this.renderOneFrameTitle(frame, i)) || null} +
-
- ; +
+ + + this.props.onExpressionAdd((i: number, item: DebugVariable): void => { + this.scopeValue = item.name || ''; + this.setState({ + editValue: { + type: 'expression', + valueType: 'string', + index: i, + name: item.name, + value: item.name || '', + }, + }); + }) + } + > + + + +
+ {this.props.callFrames?.length && + this.renderScopes(this.props.callFrames[this.props.currentFrame])} +
+
+ + ); } } -Stack.propTypes = { - currentScriptId: PropTypes.string, - mainScriptId: PropTypes.string, - scopes: PropTypes.object, - expressions: PropTypes.array, - callFrames: PropTypes.array, - currentFrame: PropTypes.number, - onChangeCurrentFrame: PropTypes.func, - onWriteScopeValue: PropTypes.func, - onExpressionDelete: PropTypes.func, - onExpressionAdd: PropTypes.func, - onExpressionNameUpdate: PropTypes.func, - themeType: PropTypes.string, -}; - export default Stack; diff --git a/src-editor/src/Components/Debugger/index.tsx b/src-editor/src/Components/Debugger/index.tsx index 4eff80d8..3782fb71 100644 --- a/src-editor/src/Components/Debugger/index.tsx +++ b/src-editor/src/Components/Debugger/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ReactSplit, { SplitDirection } from '@devbookhq/splitter'; import { @@ -13,7 +12,8 @@ import { ListItemText, DialogTitle, Dialog, - Badge, Box, + Badge, + Box, } from '@mui/material'; import { @@ -27,52 +27,62 @@ import { MdWarning as IconException, } from 'react-icons/md'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n, type IobTheme, type ThemeName, type ThemeType } from '@iobroker/adapter-react-v5'; import DialogError from '../../Dialogs/Error'; import Editor from './Editor'; import Console from './Console'; import Stack from './Stack'; - -const styles = { - root: theme => ({ +import { + CallFrame, + DebugVariable, + DebuggerLocation, + SetBreakpointParameterType, + DebugCommandToBackEnd, + DebugCommandToBackEndScope, + DebugCommandFromBackEnd, + DebugScopes, + DebugCommandToBackEndSetBreakpoint, +} from './types'; + +const styles: Record = { + root: (theme: IobTheme): React.CSSProperties => ({ width: '100%', - height: `calc(100% - ${theme.toolbar.height + 38/*Theme.toolbar.height */ + 5}px)`, + height: `calc(100% - ${parseInt(theme.toolbar.height as string, 10) + 38 /*Theme.toolbar.height */ + 5}px)`, overflow: 'hidden', - position: 'relative' + position: 'relative', }), toolbar: { - minHeight: 38,//Theme.toolbar.height, - boxShadow: '0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12)' + minHeight: 38, //Theme.toolbar.height, + boxShadow: + '0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12)', }, buttonRun: { - color: 'green' + color: 'green', }, buttonPause: { - color: 'orange' + color: 'orange', }, buttonRestart: { - color: 'darkgreen' + color: 'darkgreen', }, buttonStop: { - color: 'red' + color: 'red', }, buttonNext: { - color: 'blue' + color: 'blue', }, buttonStep: { - color: 'blue' + color: 'blue', }, buttonOut: { - color: 'blue' - }, - buttonException: { - + color: 'blue', }, + buttonException: {}, - tabFile: theme => ({ + tabFile: (theme: IobTheme): React.CSSProperties => ({ textTransform: 'inherit', - color: theme.palette.mode === 'dark' ? '#DDD' : 'inherit' + color: theme.palette.mode === 'dark' ? '#DDD' : 'inherit', }), tabText: { maxWidth: 130, @@ -88,13 +98,13 @@ const styles = { right: 0, zIndex: 10, padding: 8, - cursor: 'pointer' + cursor: 'pointer', }, - tabsRoot: theme => ({ + tabsRoot: (theme: IobTheme): React.CSSProperties => ({ minHeight: 24, background: theme.palette.mode === 'dark' ? '#333' : '#e6e6e6', - color: theme.palette.mode === 'dark' ? 'white' : 'inherit' + color: theme.palette.mode === 'dark' ? 'white' : 'inherit', }), tabRoot: { minHeight: 24, @@ -115,26 +125,74 @@ const styles = { height: 'calc(100% - 52px)', '& .layout-pane': { overflow: 'hidden', - height: '100%' - } - } + height: '100%', + }, + }, }; -class Debugger extends React.Component { - constructor(props) { +interface DebuggerProps { + src: string; + themeName: ThemeName; + themeType: ThemeType; + adapterName: string; + socket: AdminConnection; + debugInstance?: { adapter?: string; instance?: string } | null; + runningInstances: Record; +} + +interface DebuggerState { + breakpoints: SetBreakpointParameterType[]; + console: { text: string; severity: ioBroker.LogLevel; ts: number }[]; + context: { callFrames: CallFrame[] } | null; + currentFrame: number; + error: string; + expressions: DebugVariable[]; + finished: boolean; + instance: string | undefined; + location: DebuggerLocation | null; + logErrors: number; + logWarnings: number; + logs: number; + paused: boolean; + queryBreakpoints: DebuggerLocation[] | null; + running: boolean; + scopes: DebugScopes; + script: string; + selected: string | null; + started: boolean; + starting: boolean; + stopOnException: boolean; + tabs: Record; + toolSizes: number[]; + toolsTab: string; +} + +class Debugger extends React.Component { + private console: { text: string; severity: ioBroker.LogLevel; ts: number }[] | null = null; + + private scripts: Record = {}; + + private mainScriptId: string | null = null; + + constructor(props: DebuggerProps) { super(props); - let breakpoints = window.localStorage.getItem(`javascript.tools.bp.${this.props.src}`); + const breakpointsStr = window.localStorage.getItem(`javascript.tools.bp.${this.props.src}`); + let breakpoints; try { - breakpoints = breakpoints ? JSON.parse(breakpoints) : []; - } catch (e) { + breakpoints = breakpointsStr ? JSON.parse(breakpointsStr) : []; + } catch { breakpoints = []; } - let expressions = window.localStorage.getItem(`javascript.tools.exps.${this.props.src}`); + const expressionsStr = window.localStorage.getItem(`javascript.tools.exps.${this.props.src}`); + let expressions: DebugVariable[]; try { - expressions = expressions ? JSON.parse(expressions) : []; - expressions = expressions.map(name => ({name})); - } catch (e) { + const names = expressionsStr ? (JSON.parse(expressionsStr) as string[]) : []; + expressions = names.map(name => ({ + name, + value: { type: 'undefined', description: '', value: 'undefined', name: 'name' }, + })); + } catch { expressions = []; } @@ -143,7 +201,7 @@ class Debugger extends React.Component { if (toolSizesStr) { try { toolSizes = JSON.parse(toolSizesStr); - } catch (e) { + } catch { // ignore } } @@ -171,674 +229,841 @@ class Debugger extends React.Component { logWarnings: 0, logs: 0, toolSizes, + instance: undefined, + context: null, }; - - this.scripts = {}; - this.mainScriptId = null; } - componentDidMount() { - new Promise(resolve => { - if (this.props.debugInstance) { - resolve(this.props.debugInstance.instance); + async componentDidMount(): Promise { + let instance: string | undefined; + if (this.props.debugInstance) { + instance = this.props.debugInstance.instance; + } else { + const obj = await this.props.socket.getObject(this.props.src); + instance = obj?.common?.engine?.replace('system.adapter.', '') || ''; + } + this.setState({ instance }, () => { + if (this.state.instance) { + void this.props.socket.setState(`${this.state.instance}.debug.from`, { + val: '{"cmd": "subscribed"}', + ack: true, + }); + //.then(() => ); + setTimeout( + () => this.props.socket.subscribeState(`${this.state.instance}.debug.from`, this.fromInstance), + 200, + ); } else { - this.props.socket.getObject(this.props.src) - .then(obj => resolve(obj?.common?.engine?.replace('system.adapter.', ''))); + this.setState({ error: 'Unknown instance' }); } - }) - .then(instance => - this.setState({ instance }, () => { - if (this.state.instance) { - this.props.socket.setState(`${this.state.instance}.debug.from`, { val: '{"cmd": "subscribed"}', ack: true }); - //.then(() => ); - setTimeout(() => - this.props.socket.subscribeState(`${this.state.instance}.debug.from`, this.fromInstance), 200); - } else { - this.setState({ error: 'Unknown instance' }); - } - })); + }); } - componentWillUnmount() { + componentWillUnmount(): void { if (this.state.instance) { this.props.socket.unsubscribeState(`${this.state.instance}.debug.from`, this.fromInstance); - this.props.socket.sendTo(this.state.instance, 'debugStop'); + void this.props.socket.sendTo(this.state.instance, 'debugStop'); } } - sendToInstance(cmd) { - this.props.socket.setState(`${this.state.instance}.debug.to`, { val: JSON.stringify(cmd), ack: false }); + sendToInstance(cmd: DebugCommandToBackEnd): void { + void this.props.socket.setState(`${this.state.instance}.debug.to`, { val: JSON.stringify(cmd), ack: false }); } - reinitBreakpoints(cb) { + reinitBreakpoints(cb: null | (() => void)): void { if (this.state.breakpoints.length) { - let breakpoints = JSON.parse(JSON.stringify(this.state.breakpoints)); - breakpoints = breakpoints.map(item => item.location); - this.setState({breakpoints: []}, () => { - this.sendToInstance({breakpoints, cmd: 'sb'}); + const breakpointsObj: SetBreakpointParameterType[] = JSON.parse(JSON.stringify(this.state.breakpoints)); + const breakpoints: DebuggerLocation[] = breakpointsObj.map(item => item.location); + this.setState({ breakpoints: [] }, () => { + this.sendToInstance({ breakpoints, cmd: 'sb' }); if (this.state.stopOnException) { - this.sendToInstance({cmd: 'stopOnException', state: true}); + this.sendToInstance({ cmd: 'stopOnException', state: true }); } cb && cb(); }); } else if (this.state.stopOnException) { - this.sendToInstance({cmd: 'stopOnException', state: true}); - cb && cb(); - } else { + this.sendToInstance({ cmd: 'stopOnException', state: true }); cb && cb(); + } else if (cb) { + cb(); } } - getLocation(context) { + static getLocation(context: { callFrames: CallFrame[] }): DebuggerLocation | null { if (context.callFrames) { const frame = context.callFrames[0]; return frame.location; } + return null; } - readCurrentScope() { + readCurrentScope(): void { const frame = this.state.context?.callFrames && this.state.context.callFrames[this.state.currentFrame]; if (frame) { const scopes = frame.scopeChain.filter(scope => scope.type !== 'global'); if (scopes.length) { - this.sendToInstance({cmd: 'scope', scopes}); + this.sendToInstance({ cmd: 'scope', scopes } as DebugCommandToBackEndScope); } else if (this.state.scopes.global || this.state.scopes.local || this.state.scopes.closure) { - this.setState({scopes: {}}); + this.setState({ scopes: {} }); } } } - readExpressions(i) { - if (this.state.expressions.length && this.state.context?.callFrames && this.state.context.callFrames[this.state.currentFrame]) { + readExpressions(i?: number): void { + if ( + this.state.expressions.length && + this.state.context?.callFrames && + this.state.context.callFrames[this.state.currentFrame] + ) { if (i !== undefined) { this.sendToInstance({ cmd: 'expressions', expressions: [this.state.expressions[i]], - callFrameId: this.state.context.callFrames[this.state.currentFrame].callFrameId + callFrameId: this.state.context.callFrames[this.state.currentFrame].callFrameId, }); } else { this.sendToInstance({ cmd: 'expressions', expressions: this.state.expressions, - callFrameId: this.state.context.callFrames[this.state.currentFrame].callFrameId + callFrameId: this.state.context.callFrames[this.state.currentFrame].callFrameId, }); } } } - fromInstance = (id, state) => { - try { - const data = JSON.parse(state.val); - if (data.cmd === 'subscribed') { - this.props.socket.sendTo(this.state.instance, 'debug', this.props.debugInstance || {scriptName: this.props.src}); - } else - if (data.cmd === 'readyToDebug') { - this.mainScriptId = data.scriptId; - this.scripts[data.scriptId] = data.script; - if (data.script.startsWith('(async () => {debugger;\n')) { - this.scripts[data.scriptId] = `(async () => {\n${data.script.substring('(async () => {debugger;\n'.length)}`; - } else if (data.script.startsWith('debugger;')) { - this.scripts[data.scriptId] = data.script.substring('debugger;'.length); - } + fromInstance = (_id: string, state: ioBroker.State | null | undefined): void => { + if (state?.val && this.state.instance !== undefined) { + try { + const data: DebugCommandFromBackEnd = JSON.parse(state.val as string) as DebugCommandFromBackEnd; + + if (data.cmd === 'subscribed') { + void this.props.socket.sendTo( + this.state.instance, + 'debug', + this.props.debugInstance || { scriptName: this.props.src }, + ); + } else if (data.cmd === 'readyToDebug') { + this.mainScriptId = data.scriptId; + this.scripts[data.scriptId] = data.script; + if (data.script.startsWith('(async () => {debugger;\n')) { + this.scripts[data.scriptId] = + `(async () => {\n${data.script.substring('(async () => {debugger;\n'.length)}`; + } else if (data.script.startsWith('debugger;')) { + this.scripts[data.scriptId] = data.script.substring('debugger;'.length); + } - const tabs = JSON.parse(JSON.stringify(this.state.tabs)); - tabs[data.scriptId] = this.props.debugInstance ? data.url: this.props.src.replace('script.js.', ''); - - const ts = `${Date.now()}.${Math.random() * 10000}`; - data.context?.callFrames && data.context.callFrames.forEach((item, i) => item.id = ts + i); - - this.setState({ - starting: false, - finished: false, - selected: this.mainScriptId, - script: this.scripts[data.scriptId], - tabs, - currentFrame: 0, - started: true, - paused: true, - location: this.getLocation(data.context), - context: data.context, - }, () => - this.reinitBreakpoints(() => { + const tabs = JSON.parse(JSON.stringify(this.state.tabs)); + tabs[data.scriptId] = this.props.debugInstance + ? data.url + : this.props.src.replace('script.js.', ''); + + const ts = `${Date.now()}.${Math.random() * 10000}`; + data.context?.callFrames?.forEach((item, i) => (item.id = ts + i)); + + this.setState( + { + starting: false, + finished: false, + selected: this.mainScriptId, + script: this.scripts[data.scriptId], + tabs, + currentFrame: 0, + started: true, + paused: true, + location: Debugger.getLocation(data.context), + context: data.context, + }, + () => + this.reinitBreakpoints(() => { + this.readCurrentScope(); + this.readExpressions(); + }), + ); + } else if (data.cmd === 'paused') { + const ts = `${Date.now()}.${Math.random() * 10000}`; + data.context?.callFrames?.forEach((item, i) => (item.id = ts + i)); + const location = Debugger.getLocation(data.context); + const tabs = JSON.parse(JSON.stringify(this.state.tabs)); + const parts = data.context.callFrames[0].url.split('iobroker.javascript'); + if (location) { + tabs[location.scriptId] = (parts[1] || parts[0]).replace('script.js.', ''); + } + + const newState: Partial = { + tabs, + paused: true, + location, + currentFrame: 0, + context: data.context, + }; + + newState.script = + !location?.scriptId || this.scripts[location.scriptId] === undefined + ? I18n.t('loading...') + : this.scripts[location.scriptId]; + newState.selected = location?.scriptId; + + this.setState(newState as DebuggerState, () => { this.readCurrentScope(); this.readExpressions(); - })); - } else if (data.cmd === 'paused') { - const ts = `${Date.now()}.${Math.random() * 10000}`; - data.context?.callFrames && data.context.callFrames.forEach((item, i) => item.id = ts + i); - const location = this.getLocation(data.context); - const tabs = JSON.parse(JSON.stringify(this.state.tabs)); - const parts = data.context.callFrames[0].url.split('iobroker.javascript'); - tabs[location.scriptId] = (parts[1] || parts[0]).replace('script.js.', ''); - - const newState = { - tabs, - paused: true, - location, - currentFrame: 0, - context: data.context, - scope: {id: (data.context?.callFrames && data.context.callFrames[0] && data.context.callFrames[0].id) || 0} - }; - - newState.script = this.scripts[location.scriptId] === undefined ? I18n.t('loading...') : this.scripts[location.scriptId]; - newState.selected = location.scriptId; - - this.setState(newState, () => { - this.readCurrentScope(); - this.readExpressions(); - if (!this.scripts[location.scriptId]) { - this.sendToInstance({cmd: 'source', scriptId: location.scriptId}); + if (location?.scriptId) { + if (!this.scripts[location.scriptId]) { + this.sendToInstance({ cmd: 'source', scriptId: location.scriptId }); + } + } + }); + } else if (data.cmd === 'script') { + this.scripts[data.scriptId] = data.text; + if (this.state.selected === data.scriptId) { + this.setState({ script: this.scripts[data.scriptId] }); } - }); - } else if (data.cmd === 'script') { - this.scripts[data.scriptId] = data.text; - if (this.state.selected === data.scriptId) { - this.setState({script: this.scripts[data.scriptId]}); - } - } else if (data.cmd === 'resumed') { - this.setState({paused: false}); - } else if (data.cmd === 'log') { - if (this.state.toolsTab === 'console') { - this.console = null; - const console = [...this.state.console]; - console.push({text: data.text, severity: data.severity, ts: data.ts}); - this.setState({console}); - } else { - if (data.severity === 'error') { - this.setState({logErrors: this.state.logErrors + 1}); - } else if (data.severity === 'warn') { - this.setState({logWarnings: this.state.logWarnings + 1}); + } else if (data.cmd === 'resumed') { + this.setState({ paused: false }); + } else if (data.cmd === 'log') { + if (this.state.toolsTab === 'console') { + this.console = null; + const console = [...this.state.console]; + console.push({ text: data.text, severity: data.severity, ts: data.ts }); + this.setState({ console }); } else { - this.setState({logs: this.state.logs + 1}); + if (data.severity === 'error') { + this.setState({ logErrors: this.state.logErrors + 1 }); + } else if (data.severity === 'warn') { + this.setState({ logWarnings: this.state.logWarnings + 1 }); + } else { + this.setState({ logs: this.state.logs + 1 }); + } + this.console = this.console || [...this.state.console]; + this.console.push({ text: data.text, severity: data.severity, ts: data.ts }); } - this.console = this.console || [...this.state.console]; - this.console.push({text: data.text, severity: data.severity, ts: data.ts}); - } - } else if (data.cmd === 'error') { - this.setState({error: data.error}); - } else if (data.cmd === 'finished' || data.cmd === 'debugStopped') { - this.setState({ - finished: true, - starting: false, - started: true, - }); - } else if (data.cmd === 'sb') { - const breakpoints = JSON.parse(JSON.stringify(this.state.breakpoints)); - let changed = false; - data.breakpoints.filter(bp => bp).forEach(bp => { - const found = breakpoints.find(item => - item.location.scriptId === bp.location.scriptId && item.location.lineNumber === bp.location.lineNumber); - if (!found) { - changed = true; - breakpoints.push(bp); + } else if (data.cmd === 'error') { + this.setState({ error: data.error }); + } else if (data.cmd === 'finished' || data.cmd === 'debugStopped') { + this.setState({ + finished: true, + starting: false, + started: true, + }); + } else if (data.cmd === 'sb') { + const breakpoints: SetBreakpointParameterType[] = JSON.parse( + JSON.stringify(this.state.breakpoints), + ); + let changed = false; + data.breakpoints + .filter(bp => bp) + .forEach(bp => { + const found = breakpoints.find( + item => + item.location.scriptId === bp.location.scriptId && + item.location.lineNumber === bp.location.lineNumber, + ); + if (!found) { + changed = true; + breakpoints.push(bp); + } + }); + changed && + window.localStorage.setItem( + `javascript.tools.bp.${this.props.src}`, + JSON.stringify(breakpoints), + ); + changed && this.setState({ breakpoints }); + } else if (data.cmd === 'cb') { + const breakpoints: SetBreakpointParameterType[] = JSON.parse( + JSON.stringify(this.state.breakpoints), + ); + let changed = false; + + data.breakpoints + .filter(id => id !== undefined && id !== null) + .forEach(bp => { + const found = breakpoints.find(item => item.id === bp); + if (found) { + const pos = breakpoints.indexOf(found); + breakpoints.splice(pos, 1); + changed = true; + } + }); + changed && + window.localStorage.setItem( + `javascript.tools.bp.${this.props.src}`, + JSON.stringify(breakpoints), + ); + changed && this.setState({ breakpoints }); + } else if (data.cmd === 'scope') { + // const global = data.scopes.find(scope => scope.type === 'global') || null; + const local = data.scopes.find(scope => scope.type === 'local') || undefined; + const closure = data.scopes.find(scope => scope.type === 'closure') || undefined; + + console.log(JSON.stringify(closure)); + + this.setState({ + scopes: { local, closure }, + }); + } else if (data.cmd === 'setValue') { + const scopes: DebugScopes = JSON.parse(JSON.stringify(this.state.scopes)); + let item; + if (data.scopeNumber === 0) { + item = scopes?.local?.properties?.result.find(item => item.name === data.variableName); + } else { + item = scopes?.closure?.properties?.result.find(item => item.name === data.variableName); } - }); - changed && window.localStorage.setItem('javascript.tools.bp.' + this.props.src, JSON.stringify(breakpoints)); - changed && this.setState({breakpoints}); - } else if (data.cmd === 'cb') { - const breakpoints = JSON.parse(JSON.stringify(this.state.breakpoints)); - let changed = false; - - data.breakpoints.filter(id => id !== undefined && id !== null).forEach(id => { - const found = breakpoints.find(item => item.id === id); - if (found) { - const pos = breakpoints.indexOf(found); - breakpoints.splice(pos, 1); - changed = true; + if (item) { + // @ts-expect-error fix later + item.value.value = data.newValue.value; + this.setState({ scopes }); } - }); - changed && window.localStorage.setItem('javascript.tools.bp.' + this.props.src, JSON.stringify(breakpoints)); - changed && this.setState({breakpoints}); - } else if (data.cmd === 'scope') { - //const global = data.scopes.find(scope => scope.type === 'global') || null; - const local = data.scopes.find(scope => scope.type === 'local') || null; - const closure = data.scopes.find(scope => scope.type === 'closure') || null; - - console.log(JSON.stringify(closure)); - - this.setState({scopes: {local, closure, id: `${this.state.scope.id}_${this.state.currentFrame}`}}); - } else if (data.cmd === 'setValue') { - const scopes = JSON.parse(JSON.stringify(this.state.scopes)); - let item; - if (data.scopeNumber === 0) { - item = scopes.local && scopes.local.properties && scopes.local.properties.result && scopes.local.properties.result.find(item => item.name === data.variableName); - } else { - item = scopes.closure && scopes.closure.properties && scopes.closure.properties.result && scopes.closure.properties.result && scopes.closure.properties.result.find(item => item.name === data.variableName); - } - if (item) { - item.value.value = data.newValue.value; - this.setState({scopes}); - } - } else if (data.cmd === 'expressions') { - // update values - let expressions = JSON.parse(JSON.stringify(this.state.expressions)); - let changed = false; - data.expressions.forEach(item => { - const expression = expressions.find(it => it.name === item.name); - if (expression) { - changed = true; - expression.value = item.result; + } else if (data.cmd === 'expressions') { + // update values + const expressions: DebugVariable[] = JSON.parse(JSON.stringify(this.state.expressions)); + let changed = false; + data.expressions.forEach(item => { + const expression = expressions.find(it => it.name === item.name); + if (expression) { + changed = true; + expression.value = item.result; + } + }); + changed && this.setState({ expressions }); + + console.log(`expressions: ${JSON.stringify(data)}`); + } else if (data.cmd === 'getPossibleBreakpoints') { + if (data.breakpoints?.length === 1) { + this.sendToInstance({ breakpoints: data.breakpoints, cmd: 'sb' }); + } else if (!data.breakpoints?.length) { + window.alert('cannot set'); + } else { + this.setState({ queryBreakpoints: data.breakpoints }); } - }); - changed && this.setState({expressions}); - - console.log('expressions: ' + JSON.stringify(data)); - } else if (data.cmd === 'getPossibleBreakpoints') { - if (data.breakpoints?.locations?.length === 1) { - this.sendToInstance({breakpoints: data.breakpoints.locations, cmd: 'sb'}); - } else if (!data.breakpoints?.locations?.length) { - window.alert('cannot set'); } else { - this.setState({queryBreakpoints: data.breakpoints.locations}); + console.error(`Unknown command: ${JSON.stringify(data)}`); } - } else { - console.error(`Unknown command: ${JSON.stringify(data)}`); + } catch { + // ignore } - } catch (e) { - } - } + }; - getTextAtLocation(location) { + getTextAtLocation(location: DebuggerLocation): React.JSX.Element[] { let line = this.state.script.split(/\r\n|\n/)[location.lineNumber]; let arrow; - if (location.columnNumber >= 10) { + if (location.columnNumber !== undefined && location.columnNumber >= 10) { line = line.substring(location.columnNumber - 10, location.columnNumber + 20); arrow = `${''.padStart(10, ' ')}↑`; - } else { + } else if (location.columnNumber !== undefined) { line = line.substring(0, 30 - location.columnNumber); arrow = `${''.padStart(location.columnNumber, ' ')}↑`; } return [ -
{line}
, -
{arrow}
+
+ {line} +
, +
+ {arrow} +
, ]; } - renderQueryBreakpoints() { + renderQueryBreakpoints(): React.JSX.Element | null { if (this.state.queryBreakpoints) { - return this.setState({ queryBreakpoints: null })} aria-labelledby="bp-dialog-title" open={!0}> - {I18n.t('Select breakpoint')} - - {this.state.queryBreakpoints.map((bp, i) => { - this.sendToInstance({ breakpoints: [bp], cmd: 'sb' }); - this.setState({ queryBreakpoints: null }); - }} - key={i}> - - )} - - ; + return ( + this.setState({ queryBreakpoints: null })} + aria-labelledby="bp-dialog-title" + open={!0} + > + {I18n.t('Select breakpoint')} + + {this.state.queryBreakpoints.map((bp, i) => ( + { + this.sendToInstance({ + breakpoints: [bp], + cmd: 'sb', + } as DebugCommandToBackEndSetBreakpoint); + this.setState({ queryBreakpoints: null }); + }} + key={i} + > + + + ))} + + + ); } return null; } - renderError() { + renderError(): React.JSX.Element | null { if (this.state.error) { - return this.setState({ error: '' })} text={this.state.error} />; + return ( + this.setState({ error: '' })} + text={this.state.error} + /> + ); } return null; } - closeTab(id, e) { - e && e.stopPropagation(); + closeTab(id: string, e?: React.MouseEvent): void { + e?.stopPropagation(); const tabs = JSON.parse(JSON.stringify(this.state.tabs)); delete tabs[id]; - const newState = {tabs, script: this.scripts[this.mainScriptId], selected: this.mainScriptId}; + const newState: Partial = { + tabs, + script: this.mainScriptId ? this.scripts[this.mainScriptId] : '...', + selected: this.mainScriptId, + }; if (this.state.location && this.state.location.scriptId !== this.mainScriptId) { newState.location = null; } - this.setState(newState); + this.setState(newState as DebuggerState); } - renderTabs() { + renderTabs(): React.JSX.Element { const disabled = !this.state.tabs || !this.state.started; - return { - if (this.scripts[value]) { - this.setState({selected: value, script: this.scripts[value]}); - } else { - this.setState({selected: value, script: 'loading...'}, () => - this.sendToInstance({cmd: 'source', scriptId: value})); - } - }} - scrollButtons="auto" - > - {Object.keys(this.state.tabs || []) - .map(id => { + return ( + { + if (this.scripts[value]) { + this.setState({ selected: value, script: this.scripts[value] }); + } else { + this.setState({ selected: value, script: 'loading...' }, () => + this.sendToInstance({ cmd: 'source', scriptId: value }), + ); + } + }} + scrollButtons="auto" + > + {Object.keys(this.state.tabs || []).map(id => { let label = id; - let title = this.state.tabs[id] || ''; + const title = this.state.tabs[id] || ''; if (this.state.tabs[id]) { - label = this.state.tabs[id].split('/').pop(); + label = this.state.tabs[id].split('/').pop() || ''; } - label = [ -
{label}
, - id !== this.mainScriptId && - this.closeTab(id, e)} fontSize="small" />]; - - return ; + const labelEl = [ +
+ {label} +
, + id !== this.mainScriptId ? ( + + this.closeTab(id, e)} + fontSize="small" + /> + + ) : null, + ]; + + return ( + + ); })} -
; +
+ ); } - onResume() { + onResume(): void { this.sendToInstance({ cmd: 'cont' }); } - onPause() { + onPause(): void { this.sendToInstance({ cmd: 'pause' }); } - onNext() { + onNext(): void { this.sendToInstance({ cmd: 'next' }); } - onStepIn() { + onStepIn(): void { this.sendToInstance({ cmd: 'step' }); } - onStepOut() { + onStepOut(): void { this.sendToInstance({ cmd: 'out' }); } - onRestart() { - this.setState({ started: false, starting: true }, () => - this.props.socket.sendTo(this.state.instance, 'debug', this.props.debugInstance || {scriptName: this.props.src})); + onRestart(): void { + this.setState( + { started: false, starting: true }, + () => + this.state.instance !== undefined && + this.props.socket.sendTo( + this.state.instance, + 'debug', + this.props.debugInstance || { scriptName: this.props.src }, + ), + ); } - onToggleException() { + onToggleException(): void { const stopOnException = !this.state.stopOnException; window.localStorage.setItem('javascript.tools.stopOnException', stopOnException ? 'true' : 'false'); this.setState({ stopOnException }, () => - this.sendToInstance({ cmd: 'stopOnException', state: stopOnException })); + this.sendToInstance({ cmd: 'stopOnException', state: stopOnException }), + ); } - renderToolbar() { + renderToolbar(): React.JSX.Element { const disabled = !this.state.started; - return - this.onRestart()} - title={I18n.t('Restart')} - size="medium"> - { - !this.state.finished && this.state.paused ? + return ( + + this.onRestart()} + title={I18n.t('Restart')} + size="medium" + > + + + {!this.state.finished && this.state.paused ? ( this.onResume()} title={I18n.t('Resume execution')} - size="medium"> - : - !this.state.finished && this.onPause()} - title={I18n.t('Pause execution')} - size="medium"> - } - {!this.state.finished && this.onNext()} - title={I18n.t('Go to next line')} - size="medium">} - {!this.state.finished && this.onStepIn()} - title={I18n.t('Step into function')} - size="medium">} - {!this.state.finished && this.onStepOut()} - title={I18n.t('Step out from function')} - size="medium">} - {!this.state.finished && this.onToggleException()} - title={I18n.t('Stop on exception')} - size="medium">} - {this.renderTabs()} - ; + size="medium" + > + + + ) : ( + !this.state.finished && ( + this.onPause()} + title={I18n.t('Pause execution')} + size="medium" + > + + + ) + )} + {!this.state.finished && ( + this.onNext()} + title={I18n.t('Go to next line')} + size="medium" + > + + + )} + {!this.state.finished && ( + this.onStepIn()} + title={I18n.t('Step into function')} + size="medium" + > + + + )} + {!this.state.finished && ( + this.onStepOut()} + title={I18n.t('Step out from function')} + size="medium" + > + + + )} + {!this.state.finished && ( + this.onToggleException()} + title={I18n.t('Stop on exception')} + size="medium" + > + + + )} + {this.renderTabs()} + + ); } - getPossibleBreakpoints(bp) { - const end = {...bp, columnNumber: 1000}; + getPossibleBreakpoints(bp: DebuggerLocation): void { + const end = { ...bp, columnNumber: 1000 }; this.sendToInstance({ cmd: 'getPossibleBreakpoints', start: bp, end }); } - toggleBreakpoint(lineNumber) { - let bp = this.state.breakpoints.find(item => item.location.scriptId === this.state.selected && item.location.lineNumber === lineNumber); + toggleBreakpoint(lineNumber: number): void { + const bp: SetBreakpointParameterType | undefined = this.state.breakpoints.find( + item => item.location.scriptId === this.state.selected && item.location.lineNumber === lineNumber, + ); if (bp) { const breakpoints = JSON.parse(JSON.stringify(this.state.breakpoints)); - this.setState({breakpoints}, () => - this.sendToInstance({ breakpoints: [bp.id], cmd: 'cb' })); + this.setState({ breakpoints }, () => this.sendToInstance({ breakpoints: [bp.id], cmd: 'cb' })); } else { - bp = { scriptId: this.state.selected, lineNumber, columnNumber: 0 }; - this.getPossibleBreakpoints(bp); + this.getPossibleBreakpoints({ + scriptId: this.state.selected, + lineNumber, + columnNumber: 0, + } as DebuggerLocation); } } - renderCode() { + renderCode(): React.JSX.Element | null { if (this.state.script && this.state.started) { const breakpoints = this.state.breakpoints.filter(bp => bp.location.scriptId === this.state.selected); - return this.toggleBreakpoint(i)} - /> + return ( + this.toggleBreakpoint(i)} + /> + ); } + return null; } - renderFrames() { + renderFrames(): React.JSX.Element | null { if (!this.state.paused) { return null; } - return { - this.setState({ currentFrame: i, scopes: {} }, () => { - this.readCurrentScope(); - this.readExpressions(); - }) - }} - onWriteScopeValue={obj => { - this.sendToInstance({ - cmd: 'setValue', - variableName: obj.variableName, - scopeNumber: obj.scopeNumber, - newValue: obj.newValue, - callFrameId: obj.callFrameId, - }); - }} - onExpressionDelete={i => { - const expressions = JSON.parse(JSON.stringify(this.state.expressions)); - expressions.splice(i, 1); - this.setState({expressions}); - window.localStorage.setItem(`javascript.tools.exps.${this.props.src}`, JSON.stringify(expressions.map(item => item.name))); - }} - onExpressionAdd={cb => { - const expressions = JSON.parse(JSON.stringify(this.state.expressions)); - expressions.push({ name: '', value: { value: '' } }); - this.setState({ expressions }, () => cb && cb(expressions.length - 1, this.state.expressions[expressions.length - 1])); - }} - onExpressionNameUpdate={(i, name, cb) => { - const expressions = JSON.parse(JSON.stringify(this.state.expressions)); - if (!name) { + return ( + { + this.setState({ currentFrame: i, scopes: {} }, () => { + this.readCurrentScope(); + this.readExpressions(); + }); + }} + onWriteScopeValue={obj => { + this.sendToInstance({ + cmd: 'setValue', + variableName: obj.variableName, + scopeNumber: obj.scopeNumber, + newValue: obj.newValue, + callFrameId: obj.callFrameId, + }); + }} + onExpressionDelete={i => { + const expressions: DebugVariable[] = JSON.parse(JSON.stringify(this.state.expressions)); expressions.splice(i, 1); - } else if (expressions.find(item => item.name === name)) { - return cb && cb(false); - } else { - expressions[i].name = name; - } + this.setState({ expressions }); + window.localStorage.setItem( + `javascript.tools.exps.${this.props.src}`, + JSON.stringify(expressions.map(item => item.name)), + ); + }} + onExpressionAdd={cb => { + const expressions = JSON.parse(JSON.stringify(this.state.expressions)); + expressions.push({ name: '', value: { value: '' } }); + this.setState( + { expressions }, + () => cb && cb(expressions.length - 1, this.state.expressions[expressions.length - 1]), + ); + }} + onExpressionNameUpdate={(i, name, cb) => { + const expressions: DebugVariable[] = JSON.parse(JSON.stringify(this.state.expressions)); + if (!name) { + expressions.splice(i, 1); + } else if (expressions.find(item => item.name === name)) { + return cb && cb(); + } else { + expressions[i].name = name; + } - this.setState({expressions}, () => { - name && this.readExpressions(i); - cb && cb(); - }); - window.localStorage.setItem(`javascript.tools.exps.${this.props.src}`, JSON.stringify(expressions.map(item => item.name))); - }} - />; + this.setState({ expressions }, () => { + name && this.readExpressions(i); + cb && cb(); + }); + window.localStorage.setItem( + `javascript.tools.exps.${this.props.src}`, + JSON.stringify(expressions.map(item => item.name)), + ); + }} + /> + ); } - renderConsole() { - return this.setState({ - console: [], - logErrors: 0, - logWarning: 0, - logs: 0, - })} - />; + renderConsole(): React.JSX.Element { + return ( + + this.setState({ + console: [], + logErrors: 0, + logWarnings: 0, + logs: 0, + }) + } + /> + ); } - renderTools() { + renderTools(): React.JSX.Element { const disabled = !this.state.tabs || !this.state.started; let _console; if (this.state.logErrors) { - _console = - {I18n.t('Console')} - ; + _console = ( + + {I18n.t('Console')} + + ); } else if (this.state.logWarnings) { - _console = - {I18n.t('Console')} - ; + _console = ( + + {I18n.t('Console')} + + ); } else if (this.state.logs) { - _console = - {I18n.t('Console')} - ; + _console = ( + + {I18n.t('Console')} + + ); } else { _console = I18n.t('Console'); } - return
- { - const newState = { toolsTab: value }; - - // load logs from buffer - if (this.console && value === 'console') { - newState.console = this.console; - this.console = null; - newState.logs = 0; - newState.logWarnings = 0; - newState.logErrors = 0; - } - - window.localStorage.setItem('javascript.tools.tab', value); - - this.setState(newState); - }} - scrollButtons="auto" - > - - - -
- {this.state.toolsTab === 'stack' && !disabled ? this.renderFrames() : null} - {this.state.toolsTab === 'console' && !disabled ? this.renderConsole() : null} + return ( +
+ { + const newState: Partial = { toolsTab: value }; + + // load logs from buffer + if (this.console && value === 'console') { + newState.console = this.console; + this.console = null; + newState.logs = 0; + newState.logWarnings = 0; + newState.logErrors = 0; + } + + window.localStorage.setItem('javascript.tools.tab', value); + + this.setState(newState as DebuggerState); + }} + scrollButtons="auto" + > + + + +
+ {this.state.toolsTab === 'stack' && !disabled ? this.renderFrames() : null} + {this.state.toolsTab === 'console' && !disabled ? this.renderConsole() : null} +
-
; + ); } - render() { - return - {this.state.starting ? : null} - {this.renderToolbar()} - { - this.setState({ toolSizes }); - window.localStorage.setItem('JS.toolSizes', JSON.stringify(toolSizes)); - }} - gutterClassName={this.props.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} + render(): React.JSX.Element { + return ( + -
- {this.renderCode()} - {this.renderQueryBreakpoints()} -
-
- {this.renderTools()} -
-
- {this.renderError()} -
; + {this.state.starting ? : null} + {this.renderToolbar()} + { + this.setState({ toolSizes }); + window.localStorage.setItem('JS.toolSizes', JSON.stringify(toolSizes)); + }} + gutterClassName={this.props.themeType === 'dark' ? 'Dark visGutter' : 'Light visGutter'} + > +
+ {this.renderCode()} + {this.renderQueryBreakpoints()} +
+
{this.renderTools()}
+
+ {this.renderError()} + + ); } } -Debugger.propTypes = { - runningInstances: PropTypes.object, - adapterName: PropTypes.string, - src: PropTypes.string, - socket: PropTypes.object.isRequired, - style: PropTypes.object, - themeType: PropTypes.string, - theme: PropTypes.object, - themeName: PropTypes.string, - debugInstance: PropTypes.object, -}; - export default Debugger; diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionEmpty.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionEmpty.tsx index b31eac32..97ab522f 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionEmpty.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionEmpty.tsx @@ -1,36 +1,45 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionEmpty, + RuleBlockDescription, + RuleTagCardTitle, +} from '@/Components/RulesEditor/types'; -class ActionEmpty extends GenericBlock { - constructor(props) { +class ActionEmpty extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionEmpty.getStaticData()); } - static compile(/* config, context */) { + static compile(/* config, context */): string { return ``; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderNameText', - attr: 'textTime', - defaultValue: 'Block not found', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderNameText', + attr: 'textTime', + defaultValue: 'Block not found', + }, + ], + }, + () => super.onTagChange(tagCard), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Not found', id: 'ActionEmpty', icon: 'Shuffle', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionEmpty.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionExec.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionExec.tsx index df05b658..7d83601d 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionExec.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionExec.tsx @@ -1,46 +1,53 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionExec, RuleBlockDescription, RuleContext, RuleTagCardTitle } from '../../types'; -class ActionExec extends GenericBlock { - constructor(props) { +class ActionExec extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionExec.getStaticData()); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionExec, context: RuleContext): string { return `// exec "${config.exec}" \t\tconst subActionVar${config._id} = "${(config.exec || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {exec: subActionVar${config._id}}); \t\tconsole.log(subActionVar${config._id});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: RuleBlockConfigActionExec }): string { return `Exec: ${debugMessage.data.exec}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderModalInput', - attr: 'exec', - defaultValue: 'ls /opt/iobroker', - nameBlock: 'Shell command', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderModalInput', + attr: 'exec', + defaultValue: 'ls /opt/iobroker', + nameBlock: 'Shell command', + }, + ], + }, + () => super.onTagChange(tagCard), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Exec', id: 'ActionExec', icon: 'Apps', title: 'Executes some shell command', - helpDialog: 'You can use %s in the command to use current trigger value or %id to use the triggered object ID', - } + helpDialog: + 'You can use %s in the command to use current trigger value or %id to use the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionExec.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionFunction.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionFunction.tsx index 5de378d0..b72d3850 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionFunction.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionFunction.tsx @@ -1,15 +1,18 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; - -class ActionFunction extends GenericBlock { - constructor(props) { +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionFunction, + RuleBlockDescription, + RuleTagCardTitle, +} from '@/Components/RulesEditor/types'; + +class ActionFunction extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionFunction.getStaticData()); } - static compile(config, context) { - const lines = (config.func || '') - .split('\n') - .map((line, i) => ` ${line}`); + static compile(config: RuleBlockConfigActionFunction): string { + const lines = (config.func || '').split('\n').map(line => ` ${line}`); lines.unshift(`\t\t_sendToFrontEnd(${config._id}, {func: 'executed'});`); lines.unshift(`// user function`); @@ -17,25 +20,29 @@ class ActionFunction extends GenericBlock { return lines.join('\n'); } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(): string { return I18n.t('Function: executed'); } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderModalInput', - attr: 'func', - noTextEdit: true, - defaultValue: 'console.log("Test")', - nameBlock: 'Function', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderModalInput', + attr: 'func', + noTextEdit: true, + defaultValue: 'console.log("Test")', + nameBlock: 'Function', + }, + ], + }, + () => super.onTagChange(tagCard), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'User function', @@ -43,10 +50,11 @@ class ActionFunction extends GenericBlock { icon: 'Functions', title: 'Write your own code', helpDialog: 'This is advances option. You can write your own code here and it will be executed on trigger', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionFunction.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionHTTPCall.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionHTTPCall.tsx index 7a78ccdd..2ccdda80 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionHTTPCall.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionHTTPCall.tsx @@ -1,35 +1,45 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionHTTPCall, + RuleBlockDescription, + RuleContext, + RuleTagCardTitle, +} from '@/Components/RulesEditor/types'; -class ActionHTTPCall extends GenericBlock { - constructor(props) { +class ActionHTTPCall extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionHTTPCall.getStaticData()); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionHTTPCall, context: RuleContext): string { return `// HTTP request ${config.url} \t\tconst subActionVar${config._id} = "${(config.url || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {url: subActionVar${config._id}}); \t\trequest(subActionVar${config._id});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: RuleBlockConfigActionHTTPCall }): string { return `URL: ${debugMessage.data.url}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderModalInput', - attr: 'url', - defaultValue: 'http://mydevice.com?...', - nameBlock: 'URL', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderModalInput', + attr: 'url', + defaultValue: 'http://mydevice.com?...', + nameBlock: 'URL', + }, + ], + }, + () => super.onTagChange(tagCard), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'HTTP Call', @@ -37,10 +47,11 @@ class ActionHTTPCall extends GenericBlock { icon: 'Language', title: 'Make a HTTP get request', helpDialog: 'You can use %s in the URL to use current trigger value or %id to use the triggered object ID', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionHTTPCall.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionOperateStates.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionOperateStates.tsx index 502b4fdf..b3f44444 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionOperateStates.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionOperateStates.tsx @@ -1,19 +1,31 @@ +import React from 'react'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; - -class ActionOperateStates extends GenericBlock { - constructor(props) { +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import { renderValue } from '../../helpers/utils'; +import type { + RuleBlockConfigActionOperationState, + RuleBlockDescription, + RuleInputAny, + RuleInputNameText, + RuleInputObjectID, + RuleInputSelect, +} from '@/Components/RulesEditor/types'; + +class ActionOperateStates extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionOperateStates.getStaticData()); } - isAllTriggersOnState() { - return this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && - !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState'); + isAllTriggersOnState(): boolean { + return ( + !!this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && + !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState') + ); } - static compile(config, context) { - let oid1 = `const val2_${config._id} = (await getStateAsync("${config.oid1}")).val;`; - let oid2 = `const val1_${config._id} = (await getStateAsync("${config.oid2}")).val;`; + static compile(config: RuleBlockConfigActionOperationState): string { + const oid1 = `const val2_${config._id} = (await getStateAsync("${config.oid1}")).val;`; + const oid2 = `const val1_${config._id} = (await getStateAsync("${config.oid2}")).val;`; return `// ${config.oid1} ${config.operation} ${config.oid2} => ${config.oidResult} \t\t ${oid1} @@ -22,26 +34,21 @@ class ActionOperateStates extends GenericBlock { \t\tawait setStateAsync("${config.oidResult}", val1_${config._id} ${config.operation} val2_${config._id}, ${config.tagCard === 'update'});`; } - static renderValue(val) { - if (val === null) { - return 'null'; - } else if (val === undefined) { - return 'undefined'; - } else if (Array.isArray(val)) { - return val.join(', '); - } else if (typeof val === 'object') { - return JSON.stringify(val); - } else { - return val.toString(); - } - } - - renderDebug(debugMessage) { - return {I18n.t('Set:')} {ActionOperateStates.renderValue(debugMessage.data.val)}; + renderDebug(debugMessage: { data: { ack: boolean; val: any } }): React.JSX.Element { + return ( + + {I18n.t('Set:')}{' '} + + {renderValue(debugMessage.data.val)} + + + ); } - onTagChange(tagCard, cb, ignore, toggle, useTrigger) { - const inputs = []; + onTagChange(): void { + const inputs: RuleInputAny[] = []; inputs.push({ nameRender: 'renderObjectID', @@ -49,21 +56,21 @@ class ActionOperateStates extends GenericBlock { attr: 'oid1', defaultValue: '', checkReadOnly: false, - }); + } as RuleInputObjectID); inputs.push({ nameRender: 'renderSelect', - //frontText: 'with', + // frontText: 'with', options: [ - {value: '+', title: '+'}, - {value: '-', title: '-'}, - {value: '*', title: '*'}, - {value: '/', title: '/'}, + { value: '+', title: '+' }, + { value: '-', title: '-' }, + { value: '*', title: '*' }, + { value: '/', title: '/' }, ], doNotTranslate: true, defaultValue: '+', - attr: 'operation' - }); + attr: 'operation', + } as RuleInputSelect); inputs.push({ nameRender: 'renderObjectID', @@ -71,36 +78,38 @@ class ActionOperateStates extends GenericBlock { attr: 'oid2', defaultValue: '', checkReadOnly: false, - }); + } as RuleInputObjectID); inputs.push({ nameRender: 'renderNameText', defaultValue: 'store in', attr: 'textEqual', - }); + } as RuleInputNameText); inputs.push({ nameRender: 'renderObjectID', attr: 'oidResult', defaultValue: '', checkReadOnly: true, - }); - - this.setState({inputs}, () => super.onTagChange(null, () => { - const settings = JSON.parse(JSON.stringify(this.state.settings)); - this.props.onChange(settings); - })); + } as RuleInputObjectID); + + this.setState({ inputs }, () => + super.onTagChange(null, () => { + const settings = JSON.parse(JSON.stringify(this.state.settings)); + this.props.onChange(settings); + }), + ); } - onValueChanged(value, attr, context) { - this.onTagChange(undefined, undefined, undefined, attr === 'toggle' ? value : undefined, attr === 'useTrigger' ? value : undefined); + onValueChanged(_value: any, _attr: string): void { + this.onTagChange(); } - onUpdate() { + onUpdate(): void { this.onTagChange(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Operate two states', @@ -108,10 +117,11 @@ class ActionOperateStates extends GenericBlock { icon: 'AddBox', tagCardArray: ['control', 'update'], title: 'Operations with two states', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionOperateStates.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPause.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPause.tsx index 0487144d..eea207ba 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPause.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPause.tsx @@ -1,13 +1,14 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionPause, RuleBlockDescription } from '../../types'; -class ActionPause extends GenericBlock { - constructor(props) { +class ActionPause extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionPause.getStaticData()); } - static compile(config, context) { - const ms = config.unit === 'ms' ? 1 : (config.unit === 's' ? 1000 : (config.unit === 'm' ? 60000 : 3600000)) + static compile(config: RuleBlockConfigActionPause): string { + const ms = config.unit === 'ms' ? 1 : config.unit === 's' ? 1000 : config.unit === 'm' ? 60000 : 3600000; return `// pause for ${ms}ms \t\t_sendToFrontEnd(${config._id}, {paused: true});\n @@ -15,11 +16,12 @@ class ActionPause extends GenericBlock { \t\t_sendToFrontEnd(${config._id}, {paused: false});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { paused: number } }): string { return I18n.t('Paused: %s', debugMessage.data.paused); } - _getOptions(pause) { + _getOptions(pause?: number | string): { value: string; title: string }[] { pause = pause === undefined ? this.state.settings.pause : pause; if (pause === 1 || pause === '1') { return [ @@ -28,56 +30,59 @@ class ActionPause extends GenericBlock { { value: 'm', title: 'minute' }, { value: 'h', title: 'hour' }, ]; - } else { - return [ - { value: 'ms', title: 'milliseconds' }, - { value: 's', title: 'seconds' }, - { value: 'm', title: 'minutes' }, - { value: 'h', title: 'hours' }, - ]; } + return [ + { value: 'ms', title: 'milliseconds' }, + { value: 's', title: 'seconds' }, + { value: 'm', title: 'minutes' }, + { value: 'h', title: 'hours' }, + ]; } - _setInputs(pause) { - this.setState({ - inputs: [ - { - nameRender: 'renderNumber', - attr: 'pause', - defaultValue: 100, - noHelperText: true, - }, - { - nameRender: 'renderSelect', - attr: 'unit', - defaultValue: 'ms', - options: this._getOptions(pause), - }, - ] - }, () => super.onTagChange()); + _setInputs(pause?: number | string): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderNumber', + attr: 'pause', + defaultValue: 100, + noHelperText: true, + }, + { + nameRender: 'renderSelect', + attr: 'unit', + defaultValue: 'ms', + options: this._getOptions(pause), + }, + ], + }, + () => super.onTagChange(), + ); } - onValueChanged(value, attr) { + onValueChanged(value: any, attr: string): void { if (attr === 'pause') { this._setInputs(value); } } - onTagChange(tagCard) { + onTagChange(): void { this._setInputs(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Pause', id: 'ActionPause', icon: 'Pause', title: 'Make a pause between actions', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionPause.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPrintText.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPrintText.tsx index ef0a4624..060e2d90 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPrintText.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPrintText.tsx @@ -1,47 +1,54 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionPrintText, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; -class ActionPrintText extends GenericBlock { - constructor(props) { +class ActionPrintText extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionPrintText.getStaticData()); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionPrintText, context: RuleContext): string { return `// Log ${config.text} \t\tconst subActionVar${config._id} = "${(config.text || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tconsole.log(subActionVar${config._id});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return I18n.t('Log: %s', debugMessage.data.text); } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderModalInput', - attr: 'text', - defaultValue: 'My device triggered', - nameBlock: 'Log text', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderModalInput', + attr: 'text', + defaultValue: 'My device triggered', + nameBlock: 'Log text', + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Log text', id: 'ActionPrintText', icon: 'Subject', title: 'Print some text in log', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionPrintText.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushover.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushover.tsx index 26c01271..b31d3e72 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushover.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushover.tsx @@ -1,18 +1,18 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionPushover, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; -class ActionPushover extends GenericBlock { - constructor(props) { +class ActionPushover extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionPushover.getStaticData()); - this.cachePromises = {}; } - static compile(config, context) { - let text = (config.text || '').replace(/"/g, '\\"'); + static compile(config: RuleBlockConfigActionPushover, context: RuleContext): string { + const text = (config.text || '').replace(/"/g, '\\"'); if (!text) { return `// no text defined _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; - } else { - return `// Pushover ${config.text || ''} + } + return `// Pushover ${config.text || ''} \t\tconst subActionVar${config._id} = "${text}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tsendTo("${config.instance}", "send", { @@ -21,95 +21,100 @@ _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; \t\t sound: "${config.sound}", \t\t priority: ${config.priority} \t\t});`; - } } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `Sent: ${debugMessage.data.text}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderInstance', - adapter: 'pushover', - frontText: 'Instance:', - defaultValue: 'pushover.0', - attr: 'instance', - }, - { - nameRender: 'renderModalInput', - attr: 'text', - defaultValue: 'Hello', - nameBlock: '', - frontText: 'Text:', - }, - { - nameRender: 'renderText', - attr: 'title', - defaultValue: 'ioBroker', - frontText: 'Title:', - }, - { - nameRender: 'renderSelect', - attr: 'sound', - defaultValue: 'magic', - frontText: 'Sound:', - doNotTranslate: true, - options: [ - { value: 'pushover', title: 'pushover' }, - { value: 'bike', title: 'bike' }, - { value: 'bugle', title: 'bugle' }, - { value: 'cashregister', title: 'cashregister' }, - { value: 'classical', title: 'classical' }, - { value: 'cosmic', title: 'cosmic' }, - { value: 'falling', title: 'falling' }, - { value: 'gamelan', title: 'gamelan' }, - { value: 'incoming', title: 'incoming' }, - { value: 'intermission', title: 'intermission' }, - { value: 'magic', title: 'magic' }, - { value: 'mechanical', title: 'mechanical' }, - { value: 'pianobar', title: 'pianobar' }, - { value: 'siren', title: 'siren' }, - { value: 'spacealarm', title: 'spacealarm' }, - { value: 'tugboat', title: 'tugboat' }, - { value: 'alien', title: 'alien' }, - { value: 'climb', title: 'climb' }, - { value: 'persistent', title: 'persistent' }, - { value: 'echo', title: 'echo' }, - { value: 'updown', title: 'updown' }, - { value: 'none', title: 'none' }, - ] - }, - { - nameRender: 'renderSelect', - attr: 'priority', - defaultValue: -1, - frontText: 'Priority:', - options: [ - { value: -1, title: 'quiet' }, - { value: 0, title: 'normal' }, - { value: 1, title: 'high-priority' }, - { value: 2, title: 'acknowledgment' }, - ] - } - ] - }, () => super.onTagChange()); + onTagChange(): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderInstance', + adapter: 'pushover', + frontText: 'Instance:', + defaultValue: 'pushover.0', + attr: 'instance', + }, + { + nameRender: 'renderModalInput', + attr: 'text', + defaultValue: 'Hello', + nameBlock: '', + frontText: 'Text:', + }, + { + nameRender: 'renderText', + attr: 'title', + defaultValue: 'ioBroker', + frontText: 'Title:', + }, + { + nameRender: 'renderSelect', + attr: 'sound', + defaultValue: 'magic', + frontText: 'Sound:', + doNotTranslate: true, + options: [ + { value: 'pushover', title: 'pushover' }, + { value: 'bike', title: 'bike' }, + { value: 'bugle', title: 'bugle' }, + { value: 'cashregister', title: 'cashregister' }, + { value: 'classical', title: 'classical' }, + { value: 'cosmic', title: 'cosmic' }, + { value: 'falling', title: 'falling' }, + { value: 'gamelan', title: 'gamelan' }, + { value: 'incoming', title: 'incoming' }, + { value: 'intermission', title: 'intermission' }, + { value: 'magic', title: 'magic' }, + { value: 'mechanical', title: 'mechanical' }, + { value: 'pianobar', title: 'pianobar' }, + { value: 'siren', title: 'siren' }, + { value: 'spacealarm', title: 'spacealarm' }, + { value: 'tugboat', title: 'tugboat' }, + { value: 'alien', title: 'alien' }, + { value: 'climb', title: 'climb' }, + { value: 'persistent', title: 'persistent' }, + { value: 'echo', title: 'echo' }, + { value: 'updown', title: 'updown' }, + { value: 'none', title: 'none' }, + ], + }, + { + nameRender: 'renderSelect', + attr: 'priority', + defaultValue: -1, + frontText: 'Priority:', + options: [ + { value: -1, title: 'quiet' }, + { value: 0, title: 'normal' }, + { value: 1, title: 'high-priority' }, + { value: 2, title: 'acknowledgment' }, + ], + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Pushover', id: 'ActionPushover', adapter: 'pushover', title: 'Sends message via pushover', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionPushover.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushsafer.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushsafer.tsx index 52834e12..c15ce75b 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushsafer.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionPushsafer.tsx @@ -1,172 +1,176 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionPushsafer, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; -class ActionPushsafer extends GenericBlock { - constructor(props) { +class ActionPushsafer extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionPushsafer.getStaticData()); - this.cachePromises = {}; } - static compile(config, context) { - let text = (config.text || '').replace(/"/g, '\\"'); + static compile(config: RuleBlockConfigActionPushsafer, context: RuleContext): string { + const text = (config.text || '').replace(/"/g, '\\"'); if (!text) { return `// no text defined _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; - } else { - return `// Pushsafer ${config.text || ''} + } + return `// Pushsafer ${config.text || ''} \t\tconst subActionVar${config._id} = "${text}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tsendTo("${config.instance}", "send", { \t\t message: subActionVar${config._id}, \t\t title: "${(config.title || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}, -\t\t ${config.device ? `device: "${config.device}",` : ''} \t\t ${config.sound && config.sound !== '_' ? `sound: "${config.sound}",` : ''} \t\t priority: ${config.priority}, \t\t ${config.vibration && config.vibration !== '_' ? `vibration: ${config.vibration},` : ''} \t\t});`; - } } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `Sent: ${debugMessage.data.text}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderInstance', - adapter: 'pushsafer', - frontText: 'Instance:', - defaultValue: 'pushsafer.0', - attr: 'instance', - }, - { - nameRender: 'renderModalInput', - attr: 'text', - defaultValue: 'Hello', - nameBlock: '', - frontText: 'Text:', - }, - { - nameRender: 'renderText', - attr: 'title', - defaultValue: 'ioBroker', - frontText: 'Title:', - }, - { - nameRender: 'renderSelect', - attr: 'sound', - defaultValue: 'magic', - frontText: 'Sound:', - doNotTranslate: true, - options: [ - { value: '_', title: 'Device Default' }, - { value: '0', title: 'Silent' }, - { value: '1', title: 'Ahem (IM)' }, - { value: '2', title: 'Applause (Mail)' }, - { value: '3', title: 'Arrow (Reminder)' }, - { value: '4', title: 'Baby (SMS)' }, - { value: '5', title: 'Bell (Alarm)' }, - { value: '6', title: 'Bicycle (Alarm2)' }, - { value: '7', title: 'Boing (Alarm3)' }, - { value: '8', title: 'Buzzer (Alarm4)' }, - { value: '9', title: 'Camera (Alarm5)' }, - { value: '10', title: 'Car Horn (Alarm6)' }, - { value: '11', title: 'Cash Register (Alarm7)' }, - { value: '12', title: 'Chime (Alarm8)' }, - { value: '13', title: 'Creaky Door (Alarm9)' }, - { value: '14', title: 'Cuckoo Clock (Alarm10)' }, - { value: '15', title: 'Disconnect (Call)' }, - { value: '16', title: 'Dog (Call2)' }, - { value: '17', title: 'Doorbell (Call3)' }, - { value: '18', title: 'Fanfare (Call4)' }, - { value: '19', title: 'Gun Shot (Call5)' }, - { value: '20', title: 'Honk (Call6)' }, - { value: '21', title: 'Jaw Harp (Call7)' }, - { value: '22', title: 'Morse (Call8)' }, - { value: '23', title: 'Electricity (Call9)' }, - { value: '24', title: 'Radio Tuner (Call10)' }, - { value: '25', title: 'Sirens' }, - { value: '26', title: 'Military Trumpets' }, - { value: '27', title: 'Ufo' }, - { value: '28', title: 'Whah Whah Whah' }, - { value: '29', title: 'Man Saying Goodbye' }, - { value: '30', title: 'Man Saying Hello' }, - { value: '31', title: 'Man Saying No' }, - { value: '32', title: 'Man Saying Ok' }, - { value: '33', title: 'Man Saying Ooohhhweee' }, - { value: '34', title: 'Man Saying Warning' }, - { value: '35', title: 'Man Saying Welcome' }, - { value: '36', title: 'Man Saying Yeah' }, - { value: '37', title: 'Man Saying Yes' }, - { value: '38', title: 'Beep short' }, - { value: '39', title: 'Weeeee short' }, - { value: '40', title: 'Cut in and out short' }, - { value: '41', title: 'Finger flicking glas short' }, - { value: '42', title: 'Wa Wa Waaaa short' }, - { value: '43', title: 'Laser short' }, - { value: '44', title: 'Wind Chime short' }, - { value: '45', title: 'Echo short' }, - { value: '46', title: 'Zipper short' }, - { value: '47', title: 'HiHat short' }, - { value: '48', title: 'Beep 2 short' }, - { value: '49', title: 'Beep 3 short' }, - { value: '50', title: 'Beep 4 short' }, - { value: '51', title: 'The Alarm is armed' }, - { value: '52', title: 'The Alarm is disarmed' }, - { value: '53', title: 'The Backup is ready' }, - { value: '54', title: 'The Door is closed' }, - { value: '55', title: 'The Door is opend' }, - { value: '56', title: 'The Window is closed' }, - { value: '57', title: 'The Window is open' }, - { value: '58', title: 'The Light is off' }, - { value: '59', title: 'The Light is on' }, - { value: '60', title: 'The Doorbell rings' }, - { value: '61', title: 'Pager short' }, - { value: '62', title: 'Pager long' }, - ] - }, - { - nameRender: 'renderSelect', - attr: 'priority', - defaultValue: 0, - frontText: 'Priority:', - options: [ - { value: -2, title: 'lowest priority' }, - { value: -1, title: 'lower priority' }, - { value: 0, title: 'normal priority' }, - { value: 1, title: 'high priority' }, - { value: 2, title: 'highest priority' }, - ] - }, - { - nameRender: 'renderSelect', - attr: 'vibration', - defaultValue: 0, - frontText: 'Vibration:', - options: [ - { value: '_', title: 'default' }, - { value: 1, title: '1' }, - { value: 2, title: '2' }, - { value: 3, title: '3' }, - ] - } - ] - }, () => super.onTagChange()); + onTagChange(): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderInstance', + adapter: 'pushsafer', + frontText: 'Instance:', + defaultValue: 'pushsafer.0', + attr: 'instance', + }, + { + nameRender: 'renderModalInput', + attr: 'text', + defaultValue: 'Hello', + nameBlock: '', + frontText: 'Text:', + }, + { + nameRender: 'renderText', + attr: 'title', + defaultValue: 'ioBroker', + frontText: 'Title:', + }, + { + nameRender: 'renderSelect', + attr: 'sound', + defaultValue: 'magic', + frontText: 'Sound:', + doNotTranslate: true, + options: [ + { value: '_', title: 'Device Default' }, + { value: '0', title: 'Silent' }, + { value: '1', title: 'Ahem (IM)' }, + { value: '2', title: 'Applause (Mail)' }, + { value: '3', title: 'Arrow (Reminder)' }, + { value: '4', title: 'Baby (SMS)' }, + { value: '5', title: 'Bell (Alarm)' }, + { value: '6', title: 'Bicycle (Alarm2)' }, + { value: '7', title: 'Boing (Alarm3)' }, + { value: '8', title: 'Buzzer (Alarm4)' }, + { value: '9', title: 'Camera (Alarm5)' }, + { value: '10', title: 'Car Horn (Alarm6)' }, + { value: '11', title: 'Cash Register (Alarm7)' }, + { value: '12', title: 'Chime (Alarm8)' }, + { value: '13', title: 'Creaky Door (Alarm9)' }, + { value: '14', title: 'Cuckoo Clock (Alarm10)' }, + { value: '15', title: 'Disconnect (Call)' }, + { value: '16', title: 'Dog (Call2)' }, + { value: '17', title: 'Doorbell (Call3)' }, + { value: '18', title: 'Fanfare (Call4)' }, + { value: '19', title: 'Gun Shot (Call5)' }, + { value: '20', title: 'Honk (Call6)' }, + { value: '21', title: 'Jaw Harp (Call7)' }, + { value: '22', title: 'Morse (Call8)' }, + { value: '23', title: 'Electricity (Call9)' }, + { value: '24', title: 'Radio Tuner (Call10)' }, + { value: '25', title: 'Sirens' }, + { value: '26', title: 'Military Trumpets' }, + { value: '27', title: 'Ufo' }, + { value: '28', title: 'Whah Whah Whah' }, + { value: '29', title: 'Man Saying Goodbye' }, + { value: '30', title: 'Man Saying Hello' }, + { value: '31', title: 'Man Saying No' }, + { value: '32', title: 'Man Saying Ok' }, + { value: '33', title: 'Man Saying Ooohhhweee' }, + { value: '34', title: 'Man Saying Warning' }, + { value: '35', title: 'Man Saying Welcome' }, + { value: '36', title: 'Man Saying Yeah' }, + { value: '37', title: 'Man Saying Yes' }, + { value: '38', title: 'Beep short' }, + { value: '39', title: 'Weeeee short' }, + { value: '40', title: 'Cut in and out short' }, + { value: '41', title: 'Finger flicking glas short' }, + { value: '42', title: 'Wa Wa Waaaa short' }, + { value: '43', title: 'Laser short' }, + { value: '44', title: 'Wind Chime short' }, + { value: '45', title: 'Echo short' }, + { value: '46', title: 'Zipper short' }, + { value: '47', title: 'HiHat short' }, + { value: '48', title: 'Beep 2 short' }, + { value: '49', title: 'Beep 3 short' }, + { value: '50', title: 'Beep 4 short' }, + { value: '51', title: 'The Alarm is armed' }, + { value: '52', title: 'The Alarm is disarmed' }, + { value: '53', title: 'The Backup is ready' }, + { value: '54', title: 'The Door is closed' }, + { value: '55', title: 'The Door is opend' }, + { value: '56', title: 'The Window is closed' }, + { value: '57', title: 'The Window is open' }, + { value: '58', title: 'The Light is off' }, + { value: '59', title: 'The Light is on' }, + { value: '60', title: 'The Doorbell rings' }, + { value: '61', title: 'Pager short' }, + { value: '62', title: 'Pager long' }, + ], + }, + { + nameRender: 'renderSelect', + attr: 'priority', + defaultValue: 0, + frontText: 'Priority:', + options: [ + { value: -2, title: 'lowest priority' }, + { value: -1, title: 'lower priority' }, + { value: 0, title: 'normal priority' }, + { value: 1, title: 'high priority' }, + { value: 2, title: 'highest priority' }, + ], + }, + { + nameRender: 'renderSelect', + attr: 'vibration', + defaultValue: 0, + frontText: 'Vibration:', + options: [ + { value: '_', title: 'default' }, + { value: 1, title: '1' }, + { value: 2, title: '2' }, + { value: 3, title: '3' }, + ], + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Pushsafer', id: 'ActionPushsafer', adapter: 'pushsafer', title: 'Sends message via Pushsafer', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionPushsafer.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSayText.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSayText.tsx index 19c47fe3..604e4f28 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSayText.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSayText.tsx @@ -1,208 +1,1053 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionSayText, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; // copied from https://github.com/ioBroker/ioBroker.sayit/blob/master/admin/blockly.js#L37 const sayitEngines = { - 'en': { name: 'Google - English', engine: 'google', params: [] }, - 'de': { name: 'Google - Deutsch', engine: 'google', params: [] }, - 'ru': { name: 'Google - Русский', engine: 'google', params: [] }, - 'it': { name: 'Google - Italiano', engine: 'google', params: [] }, - 'es': { name: 'Google - Espaniol', engine: 'google', params: [] }, - 'fr': { name: 'Google - Français', engine: 'google', params: [] }, - 'ru_YA': { name: 'Yandex - Русский', engine: 'yandex', params: ['key', 'voice', 'emotion', 'ill', 'drunk', 'robot'], voice: ['jane', 'zahar'], emotion: ['none', 'good', 'neutral', 'evil', 'mixed'] }, - 'ru_YA_CLOUD': { name: 'Yandex Cloud - Русский', engine: 'yandexCloud', params: ['key', 'folderID', 'voice', 'emotion'], voice: ['alyss', 'oksana', 'jane', 'zahar'], emotion: [ 'good', 'neutral', 'evil'] }, + en: { name: 'Google - English', engine: 'google', params: [] }, + de: { name: 'Google - Deutsch', engine: 'google', params: [] }, + ru: { name: 'Google - Русский', engine: 'google', params: [] }, + it: { name: 'Google - Italiano', engine: 'google', params: [] }, + es: { name: 'Google - Espaniol', engine: 'google', params: [] }, + fr: { name: 'Google - Français', engine: 'google', params: [] }, + ru_YA: { + name: 'Yandex - Русский', + engine: 'yandex', + params: ['key', 'voice', 'emotion', 'ill', 'drunk', 'robot'], + voice: ['jane', 'zahar'], + emotion: ['none', 'good', 'neutral', 'evil', 'mixed'], + }, + ru_YA_CLOUD: { + name: 'Yandex Cloud - Русский', + engine: 'yandexCloud', + params: ['key', 'folderID', 'voice', 'emotion'], + voice: ['alyss', 'oksana', 'jane', 'zahar'], + emotion: ['good', 'neutral', 'evil'], + }, - 'en-US': { name: 'PicoTTS - Englisch US', engine: 'PicoTTS', params: [] }, - 'en-GB': { name: 'PicoTTS - Englisch GB', engine: 'PicoTTS', params: [] }, - 'de-DE': { name: 'PicoTTS - Deutsch', engine: 'PicoTTS', params: [] }, - 'it-IT': { name: 'PicoTTS - Italiano', engine: 'PicoTTS', params: [] }, - 'es-ES': { name: 'PicoTTS - Espaniol', engine: 'PicoTTS', params: [] }, - 'fr-FR': { name: 'PicoTTS - Français', engine: 'PicoTTS', params: [] }, + 'en-US': { name: 'PicoTTS - Englisch US', engine: 'PicoTTS', params: [] }, + 'en-GB': { name: 'PicoTTS - Englisch GB', engine: 'PicoTTS', params: [] }, + 'de-DE': { name: 'PicoTTS - Deutsch', engine: 'PicoTTS', params: [] }, + 'it-IT': { name: 'PicoTTS - Italiano', engine: 'PicoTTS', params: [] }, + 'es-ES': { name: 'PicoTTS - Espaniol', engine: 'PicoTTS', params: [] }, + 'fr-FR': { name: 'PicoTTS - Français', engine: 'PicoTTS', params: [] }, - 'ru-RU_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'ru-RU', ename: 'Tatyana', ssml: true, name: 'Cloud - Русский - Татьяна' }, - 'ru-RU_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'ru-RU', ename: 'Maxim', ssml: true, name: 'Cloud - Русский - Максим' }, - 'de-DE_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'de-DE', ename: 'Marlene', ssml: true, name: 'Cloud - Deutsch - Marlene' }, - 'de-DE_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'de-DE', ename: 'Hans', ssml: true, name: 'Cloud - Deutsch - Hans' }, - 'en-US_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Salli', ssml: true, name: 'Cloud - en-US - Female - Salli' }, - 'en-US_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Joey', ssml: true, name: 'Cloud - en-US - Male - Joey' }, - 'da-DK_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'da-DK', ename: 'Naja', ssml: true, name: 'Cloud - da-DK - Female - Naja' }, - 'da-DK_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'da-DK', ename: 'Mads', ssml: true, name: 'Cloud - da-DK - Male - Mads' }, - 'en-AU_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-AU', ename: 'Nicole', ssml: true, name: 'Cloud - en-AU - Female - Nicole' }, - 'en-AU_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-AU', ename: 'Russell', ssml: true, name: 'Cloud - en-AU - Male - Russell' }, - 'en-GB_CLOUD_Female_Amy': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-GB', ename: 'Amy', ssml: true, name: 'Cloud - en-GB - Female - Amy' }, - 'en-GB_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-GB', ename: 'Brian', ssml: true, name: 'Cloud - en-GB - Male - Brian' }, - 'en-GB_CLOUD_Female_Emma': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-GB', ename: 'Emma', ssml: true, name: 'Cloud - en-GB - Female - Emma' }, - 'en-GB-WLS_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-GB-WLS', ename: 'Gwyneth', ssml: true, name: 'Cloud - en-GB-WLS - Female - Gwyneth' }, - 'en-GB-WLS_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-GB-WLS', ename: 'Geraint', ssml: true, name: 'Cloud - en-GB-WLS - Male - Geraint' }, - 'cy-GB_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'cy-GB', ename: 'Gwyneth', ssml: true, name: 'Cloud - cy-GB - Female - Gwyneth' }, - 'cy-GB_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'cy-GB', ename: 'Geraint', ssml: true, name: 'Cloud - cy-GB - Male - Geraint' }, - 'en-IN_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-IN', ename: 'Raveena', ssml: true, name: 'Cloud - en-IN - Female - Raveena' }, - 'en-US_CLOUD_Male_Chipmunk':{ gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Chipmunk', ssml: true, name: 'Cloud - en-US - Male - Chipmunk' }, - 'en-US_CLOUD_Male_Eric': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Eric', ssml: true, name: 'Cloud - en-US - Male - Eric' }, - 'en-US_CLOUD_Female_Ivy': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Ivy', ssml: true, name: 'Cloud - en-US - Female - Ivy' }, - 'en-US_CLOUD_Female_Jennifer': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Jennifer', ssml: true, name: 'Cloud - en-US - Female - Jennifer' }, - 'en-US_CLOUD_Male_Justin': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Justin', ssml: true, name: 'Cloud - en-US - Male - Justin' }, - 'en-US_CLOUD_Female_Kendra': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Kendra', ssml: true, name: 'Cloud - en-US - Female - Kendra' }, - 'en-US_CLOUD_Female_Kimberly': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'en-US', ename: 'Kimberly', ssml: true, name: 'Cloud - en-US - Female - Kimberly' }, - 'es-ES_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'es-ES', ename: 'Conchita', ssml: true, name: 'Cloud - es-ES - Female - Conchita' }, - 'es-ES_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'es-ES', ename: 'Enrique', ssml: true, name: 'Cloud - es-ES - Male - Enrique' }, - 'es-US_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'es-US', ename: 'Penelope', ssml: true, name: 'Cloud - es-US - Female - Penelope' }, - 'es-US_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'es-US', ename: 'Miguel', ssml: true, name: 'Cloud - es-US - Male - Miguel' }, - 'fr-CA_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'fr-CA', ename: 'Chantal', ssml: true, name: 'Cloud - fr-CA - Female - Chantal' }, - 'fr-FR_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'fr-FR', ename: 'Celine', ssml: true, name: 'Cloud - fr-FR - Female - Celine' }, - 'fr-FR_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'fr-FR', ename: 'Mathieu', ssml: true, name: 'Cloud - fr-FR - Male - Mathieu' }, - 'is-IS_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'is-IS', ename: 'Dora', ssml: true, name: 'Cloud - is-IS - Female - Dora' }, - 'is-IS_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'is-IS', ename: 'Karl', ssml: true, name: 'Cloud - is-IS - Male - Karl' }, - 'it-IT_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'it-IT', ename: 'Carla', ssml: true, name: 'Cloud - it-IT - Female - Carla' }, - 'it-IT_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'it-IT', ename: 'Giorgio', ssml: true, name: 'Cloud - it-IT - Male - Giorgio' }, - 'nb-NO_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'nb-NO', ename: 'Liv', ssml: true, name: 'Cloud - nb-NO - Female - Liv' }, - 'nl-NL_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'nl-NL', ename: 'Lotte', ssml: true, name: 'Cloud - nl-NL - Female - Lotte' }, - 'nl-NL_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'nl-NL', ename: 'Ruben', ssml: true, name: 'Cloud - nl-NL - Male - Ruben' }, - 'pl-PL_CLOUD_Female_Agnieszka': { gender: 'Female', engine: 'cloud',params: ['cloud'], language: 'pl-PL', ename: 'Agnieszka', ssml: true, name: 'Cloud - pl-PL - Female - Agnieszka' }, - 'pl-PL_CLOUD_Male_Jacek': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'pl-PL', ename: 'Jacek', ssml: true, name: 'Cloud - pl-PL - Male - Jacek' }, - 'pl-PL_CLOUD_Female_Ewa': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'pl-PL', ename: 'Ewa', ssml: true, name: 'Cloud - pl-PL - Female - Ewa' }, - 'pl-PL_CLOUD_Male_Jan': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'pl-PL', ename: 'Jan', ssml: true, name: 'Cloud - pl-PL - Male - Jan' }, - 'pl-PL_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'pl-PL', ename: 'Maja', ssml: true, name: 'Cloud - pl-PL - Female - Maja' }, - 'pt-BR_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'pt-BR', ename: 'Vitoria', ssml: true, name: 'Cloud - pt-BR - Female - Vitoria' }, - 'pt-BR_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'pt-BR', ename: 'Ricardo', ssml: true, name: 'Cloud - pt-BR - Male - Ricardo' }, - 'pt-PT_CLOUD_Male': { gender: 'Male', engine: 'cloud', params: ['cloud'], language: 'pt-PT', ename: 'Cristiano', ssml: true, name: 'Cloud - pt-PT - Male - Cristiano' }, - 'pt-PT_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'pt-PT', ename: 'Ines', ssml: true, name: 'Cloud - pt-PT - Female - Ines' }, - 'ro-RO_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'ro-RO', ename: 'Carmen', ssml: true, name: 'Cloud - ro-RO - Female - Carmen' }, - 'sv-SE_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'sv-SE', ename: 'Astrid', ssml: true, name: 'Cloud - sv-SE - Female - Astrid' }, - 'tr-TR_CLOUD_Female': { gender: 'Female', engine: 'cloud', params: ['cloud'], language: 'tr-TR', ename: 'Filiz', ssml: true, name: 'Cloud - tr-TR - Female - Filiz' }, + 'ru-RU_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'ru-RU', + ename: 'Tatyana', + ssml: true, + name: 'Cloud - Русский - Татьяна', + }, + 'ru-RU_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'ru-RU', + ename: 'Maxim', + ssml: true, + name: 'Cloud - Русский - Максим', + }, + 'de-DE_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'de-DE', + ename: 'Marlene', + ssml: true, + name: 'Cloud - Deutsch - Marlene', + }, + 'de-DE_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'de-DE', + ename: 'Hans', + ssml: true, + name: 'Cloud - Deutsch - Hans', + }, + 'en-US_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Salli', + ssml: true, + name: 'Cloud - en-US - Female - Salli', + }, + 'en-US_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Joey', + ssml: true, + name: 'Cloud - en-US - Male - Joey', + }, + 'da-DK_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'da-DK', + ename: 'Naja', + ssml: true, + name: 'Cloud - da-DK - Female - Naja', + }, + 'da-DK_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'da-DK', + ename: 'Mads', + ssml: true, + name: 'Cloud - da-DK - Male - Mads', + }, + 'en-AU_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-AU', + ename: 'Nicole', + ssml: true, + name: 'Cloud - en-AU - Female - Nicole', + }, + 'en-AU_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-AU', + ename: 'Russell', + ssml: true, + name: 'Cloud - en-AU - Male - Russell', + }, + 'en-GB_CLOUD_Female_Amy': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-GB', + ename: 'Amy', + ssml: true, + name: 'Cloud - en-GB - Female - Amy', + }, + 'en-GB_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-GB', + ename: 'Brian', + ssml: true, + name: 'Cloud - en-GB - Male - Brian', + }, + 'en-GB_CLOUD_Female_Emma': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-GB', + ename: 'Emma', + ssml: true, + name: 'Cloud - en-GB - Female - Emma', + }, + 'en-GB-WLS_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-GB-WLS', + ename: 'Gwyneth', + ssml: true, + name: 'Cloud - en-GB-WLS - Female - Gwyneth', + }, + 'en-GB-WLS_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-GB-WLS', + ename: 'Geraint', + ssml: true, + name: 'Cloud - en-GB-WLS - Male - Geraint', + }, + 'cy-GB_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'cy-GB', + ename: 'Gwyneth', + ssml: true, + name: 'Cloud - cy-GB - Female - Gwyneth', + }, + 'cy-GB_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'cy-GB', + ename: 'Geraint', + ssml: true, + name: 'Cloud - cy-GB - Male - Geraint', + }, + 'en-IN_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-IN', + ename: 'Raveena', + ssml: true, + name: 'Cloud - en-IN - Female - Raveena', + }, + 'en-US_CLOUD_Male_Chipmunk': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Chipmunk', + ssml: true, + name: 'Cloud - en-US - Male - Chipmunk', + }, + 'en-US_CLOUD_Male_Eric': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Eric', + ssml: true, + name: 'Cloud - en-US - Male - Eric', + }, + 'en-US_CLOUD_Female_Ivy': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Ivy', + ssml: true, + name: 'Cloud - en-US - Female - Ivy', + }, + 'en-US_CLOUD_Female_Jennifer': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Jennifer', + ssml: true, + name: 'Cloud - en-US - Female - Jennifer', + }, + 'en-US_CLOUD_Male_Justin': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Justin', + ssml: true, + name: 'Cloud - en-US - Male - Justin', + }, + 'en-US_CLOUD_Female_Kendra': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Kendra', + ssml: true, + name: 'Cloud - en-US - Female - Kendra', + }, + 'en-US_CLOUD_Female_Kimberly': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'en-US', + ename: 'Kimberly', + ssml: true, + name: 'Cloud - en-US - Female - Kimberly', + }, + 'es-ES_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'es-ES', + ename: 'Conchita', + ssml: true, + name: 'Cloud - es-ES - Female - Conchita', + }, + 'es-ES_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'es-ES', + ename: 'Enrique', + ssml: true, + name: 'Cloud - es-ES - Male - Enrique', + }, + 'es-US_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'es-US', + ename: 'Penelope', + ssml: true, + name: 'Cloud - es-US - Female - Penelope', + }, + 'es-US_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'es-US', + ename: 'Miguel', + ssml: true, + name: 'Cloud - es-US - Male - Miguel', + }, + 'fr-CA_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'fr-CA', + ename: 'Chantal', + ssml: true, + name: 'Cloud - fr-CA - Female - Chantal', + }, + 'fr-FR_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'fr-FR', + ename: 'Celine', + ssml: true, + name: 'Cloud - fr-FR - Female - Celine', + }, + 'fr-FR_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'fr-FR', + ename: 'Mathieu', + ssml: true, + name: 'Cloud - fr-FR - Male - Mathieu', + }, + 'is-IS_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'is-IS', + ename: 'Dora', + ssml: true, + name: 'Cloud - is-IS - Female - Dora', + }, + 'is-IS_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'is-IS', + ename: 'Karl', + ssml: true, + name: 'Cloud - is-IS - Male - Karl', + }, + 'it-IT_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'it-IT', + ename: 'Carla', + ssml: true, + name: 'Cloud - it-IT - Female - Carla', + }, + 'it-IT_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'it-IT', + ename: 'Giorgio', + ssml: true, + name: 'Cloud - it-IT - Male - Giorgio', + }, + 'nb-NO_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'nb-NO', + ename: 'Liv', + ssml: true, + name: 'Cloud - nb-NO - Female - Liv', + }, + 'nl-NL_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'nl-NL', + ename: 'Lotte', + ssml: true, + name: 'Cloud - nl-NL - Female - Lotte', + }, + 'nl-NL_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'nl-NL', + ename: 'Ruben', + ssml: true, + name: 'Cloud - nl-NL - Male - Ruben', + }, + 'pl-PL_CLOUD_Female_Agnieszka': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'pl-PL', + ename: 'Agnieszka', + ssml: true, + name: 'Cloud - pl-PL - Female - Agnieszka', + }, + 'pl-PL_CLOUD_Male_Jacek': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'pl-PL', + ename: 'Jacek', + ssml: true, + name: 'Cloud - pl-PL - Male - Jacek', + }, + 'pl-PL_CLOUD_Female_Ewa': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'pl-PL', + ename: 'Ewa', + ssml: true, + name: 'Cloud - pl-PL - Female - Ewa', + }, + 'pl-PL_CLOUD_Male_Jan': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'pl-PL', + ename: 'Jan', + ssml: true, + name: 'Cloud - pl-PL - Male - Jan', + }, + 'pl-PL_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'pl-PL', + ename: 'Maja', + ssml: true, + name: 'Cloud - pl-PL - Female - Maja', + }, + 'pt-BR_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'pt-BR', + ename: 'Vitoria', + ssml: true, + name: 'Cloud - pt-BR - Female - Vitoria', + }, + 'pt-BR_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'pt-BR', + ename: 'Ricardo', + ssml: true, + name: 'Cloud - pt-BR - Male - Ricardo', + }, + 'pt-PT_CLOUD_Male': { + gender: 'Male', + engine: 'cloud', + params: ['cloud'], + language: 'pt-PT', + ename: 'Cristiano', + ssml: true, + name: 'Cloud - pt-PT - Male - Cristiano', + }, + 'pt-PT_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'pt-PT', + ename: 'Ines', + ssml: true, + name: 'Cloud - pt-PT - Female - Ines', + }, + 'ro-RO_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'ro-RO', + ename: 'Carmen', + ssml: true, + name: 'Cloud - ro-RO - Female - Carmen', + }, + 'sv-SE_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'sv-SE', + ename: 'Astrid', + ssml: true, + name: 'Cloud - sv-SE - Female - Astrid', + }, + 'tr-TR_CLOUD_Female': { + gender: 'Female', + engine: 'cloud', + params: ['cloud'], + language: 'tr-TR', + ename: 'Filiz', + ssml: true, + name: 'Cloud - tr-TR - Female - Filiz', + }, - 'ru-RU_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'ru-RU', ename: 'Tatyana', ssml: true, name: 'AWS Polly - Русский - Татьяна' }, - 'ru-RU_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'ru-RU', ename: 'Maxim', ssml: true, name: 'AWS Polly - Русский - Максим' }, - 'de-DE_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'de-DE', ename: 'Marlene', ssml: true, name: 'AWS Polly - Deutsch - Marlene' }, - 'de-DE_AP_Female_Vicky': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'de-DE', ename: 'Vicky', ssml: true, name: 'AWS Polly - Deutsch - Vicky' }, - 'de-DE_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'de-DE', ename: 'Hans', ssml: true, name: 'AWS Polly - Deutsch - Hans' }, - 'en-US_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Salli', ssml: true, name: 'AWS Polly - en-US - Female - Salli' }, - 'en-US_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Joey', ssml: true, name: 'AWS Polly - en-US - Male - Joey' }, - 'da-DK_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'da-DK', ename: 'Naja', ssml: true, name: 'AWS Polly - da-DK - Female - Naja' }, - 'da-DK_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'da-DK', ename: 'Mads', ssml: true, name: 'AWS Polly - da-DK - Male - Mads' }, - 'en-AU_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-AU', ename: 'Nicole', ssml: true, name: 'AWS Polly - en-AU - Female - Nicole' }, - 'en-AU_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-AU', ename: 'Russell', ssml: true, name: 'AWS Polly - en-AU - Male - Russell' }, - 'en-GB_AP_Female_Amy': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-GB', ename: 'Amy', ssml: true, name: 'AWS Polly - en-GB - Female - Amy' }, - 'en-GB_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-GB', ename: 'Brian', ssml: true, name: 'AWS Polly - en-GB - Male - Brian' }, - 'en-GB_AP_Female_Emma': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-GB', ename: 'Emma', ssml: true, name: 'AWS Polly - en-GB - Female - Emma' }, - 'en-GB-WLS_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-GB-WLS', ename: 'Gwyneth', ssml: true, name: 'AWS Polly - en-GB-WLS - Female - Gwyneth' }, - 'en-GB-WLS_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-GB-WLS', ename: 'Geraint', ssml: true, name: 'AWS Polly - en-GB-WLS - Male - Geraint' }, - 'cy-GB_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'cy-GB', ename: 'Gwyneth', ssml: true, name: 'AWS Polly - cy-GB - Female - Gwyneth' }, - 'cy-GB_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'cy-GB', ename: 'Geraint', ssml: true, name: 'AWS Polly - cy-GB - Male - Geraint' }, - 'en-IN_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-IN', ename: 'Raveena', ssml: true, name: 'AWS Polly - en-IN - Female - Raveena' }, - 'en-US_AP_Male_Chipmunk': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Chipmunk', ssml: true, name: 'AWS Polly - en-US - Male - Chipmunk' }, - 'en-US_AP_Male_Eric': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Eric', ssml: true, name: 'AWS Polly - en-US - Male - Eric' }, - 'en-US_AP_Female_Ivy': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Ivy', ssml: true, name: 'AWS Polly - en-US - Female - Ivy' }, - 'en-US_AP_Female_Jennifer': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Jennifer', ssml: true, name: 'AWS Polly - en-US - Female - Jennifer' }, - 'en-US_AP_Male_Justin': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Justin', ssml: true, name: 'AWS Polly - en-US - Male - Justin' }, - 'en-US_AP_Female_Kendra': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Kendra', ssml: true, name: 'AWS Polly - en-US - Female - Kendra' }, - 'en-US_AP_Female_Kimberly': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'en-US', ename: 'Kimberly', ssml: true, name: 'AWS Polly - en-US - Female - Kimberly' }, - 'es-ES_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'es-ES', ename: 'Conchita', ssml: true, name: 'AWS Polly - es-ES - Female - Conchita' }, - 'es-ES_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'es-ES', ename: 'Enrique', ssml: true, name: 'AWS Polly - es-ES - Male - Enrique' }, - 'es-US_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'es-US', ename: 'Penelope', ssml: true, name: 'AWS Polly - es-US - Female - Penelope' }, - 'es-US_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'es-US', ename: 'Miguel', ssml: true, name: 'AWS Polly - es-US - Male - Miguel' }, - 'fr-CA_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'fr-CA', ename: 'Chantal', ssml: true, name: 'AWS Polly - fr-CA - Female - Chantal' }, - 'fr-FR_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'fr-FR', ename: 'Celine', ssml: true, name: 'AWS Polly - fr-FR - Female - Celine' }, - 'fr-FR_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'fr-FR', ename: 'Mathieu', ssml: true, name: 'AWS Polly - fr-FR - Male - Mathieu' }, - 'is-IS_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'is-IS', ename: 'Dora', ssml: true, name: 'AWS Polly - is-IS - Female - Dora' }, - 'is-IS_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'is-IS', ename: 'Karl', ssml: true, name: 'AWS Polly - is-IS - Male - Karl' }, - 'it-IT_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'it-IT', ename: 'Carla', ssml: true, name: 'AWS Polly - it-IT - Female - Carla' }, - 'it-IT_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'it-IT', ename: 'Giorgio', ssml: true, name: 'AWS Polly - it-IT - Male - Giorgio' }, - 'nb-NO_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'nb-NO', ename: 'Liv', ssml: true, name: 'AWS Polly - nb-NO - Female - Liv' }, - 'nl-NL_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'nl-NL', ename: 'Lotte', ssml: true, name: 'AWS Polly - nl-NL - Female - Lotte' }, - 'nl-NL_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'nl-NL', ename: 'Ruben', ssml: true, name: 'AWS Polly - nl-NL - Male - Ruben' }, - 'pl-PL_AP_Female_Agnieszka':{ gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pl-PL', ename: 'Agnieszka', ssml: true, name: 'AWS Polly - pl-PL - Female - Agnieszka' }, - 'pl-PL_AP_Male_Jacek': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pl-PL', ename: 'Jacek', ssml: true, name: 'AWS Polly - pl-PL - Male - Jacek' }, - 'pl-PL_AP_Female_Ewa': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pl-PL', ename: 'Ewa', ssml: true, name: 'AWS Polly - pl-PL - Female - Ewa' }, - 'pl-PL_AP_Male_Jan': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pl-PL', ename: 'Jan', ssml: true, name: 'AWS Polly - pl-PL - Male - Jan' }, - 'pl-PL_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pl-PL', ename: 'Maja', ssml: true, name: 'AWS Polly - pl-PL - Female - Maja' }, - 'pt-BR_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pt-BR', ename: 'Vitoria', ssml: true, name: 'AWS Polly - pt-BR - Female - Vitoria' }, - 'pt-BR_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pt-BR', ename: 'Ricardo', ssml: true, name: 'AWS Polly - pt-BR - Male - Ricardo' }, - 'pt-PT_AP_Male': { gender: 'Male', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pt-PT', ename: 'Cristiano', ssml: true, name: 'AWS Polly - pt-PT - Male - Cristiano' }, - 'pt-PT_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'pt-PT', ename: 'Ines', ssml: true, name: 'AWS Polly - pt-PT - Female - Ines' }, - 'ro-RO_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'ro-RO', ename: 'Carmen', ssml: true, name: 'AWS Polly - ro-RO - Female - Carmen' }, - 'sv-SE_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'sv-SE', ename: 'Astrid', ssml: true, name: 'AWS Polly - sv-SE - Female - Astrid' }, - 'tr-TR_AP_Female': { gender: 'Female', engine: 'polly', params: ['accessKey', 'secretKey', 'region'], language: 'tr-TR', ename: 'Filiz', ssml: true, name: 'AWS Polly - tr-TR - Female - Filiz' }, + 'ru-RU_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'ru-RU', + ename: 'Tatyana', + ssml: true, + name: 'AWS Polly - Русский - Татьяна', + }, + 'ru-RU_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'ru-RU', + ename: 'Maxim', + ssml: true, + name: 'AWS Polly - Русский - Максим', + }, + 'de-DE_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'de-DE', + ename: 'Marlene', + ssml: true, + name: 'AWS Polly - Deutsch - Marlene', + }, + 'de-DE_AP_Female_Vicky': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'de-DE', + ename: 'Vicky', + ssml: true, + name: 'AWS Polly - Deutsch - Vicky', + }, + 'de-DE_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'de-DE', + ename: 'Hans', + ssml: true, + name: 'AWS Polly - Deutsch - Hans', + }, + 'en-US_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Salli', + ssml: true, + name: 'AWS Polly - en-US - Female - Salli', + }, + 'en-US_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Joey', + ssml: true, + name: 'AWS Polly - en-US - Male - Joey', + }, + 'da-DK_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'da-DK', + ename: 'Naja', + ssml: true, + name: 'AWS Polly - da-DK - Female - Naja', + }, + 'da-DK_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'da-DK', + ename: 'Mads', + ssml: true, + name: 'AWS Polly - da-DK - Male - Mads', + }, + 'en-AU_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-AU', + ename: 'Nicole', + ssml: true, + name: 'AWS Polly - en-AU - Female - Nicole', + }, + 'en-AU_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-AU', + ename: 'Russell', + ssml: true, + name: 'AWS Polly - en-AU - Male - Russell', + }, + 'en-GB_AP_Female_Amy': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-GB', + ename: 'Amy', + ssml: true, + name: 'AWS Polly - en-GB - Female - Amy', + }, + 'en-GB_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-GB', + ename: 'Brian', + ssml: true, + name: 'AWS Polly - en-GB - Male - Brian', + }, + 'en-GB_AP_Female_Emma': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-GB', + ename: 'Emma', + ssml: true, + name: 'AWS Polly - en-GB - Female - Emma', + }, + 'en-GB-WLS_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-GB-WLS', + ename: 'Gwyneth', + ssml: true, + name: 'AWS Polly - en-GB-WLS - Female - Gwyneth', + }, + 'en-GB-WLS_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-GB-WLS', + ename: 'Geraint', + ssml: true, + name: 'AWS Polly - en-GB-WLS - Male - Geraint', + }, + 'cy-GB_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'cy-GB', + ename: 'Gwyneth', + ssml: true, + name: 'AWS Polly - cy-GB - Female - Gwyneth', + }, + 'cy-GB_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'cy-GB', + ename: 'Geraint', + ssml: true, + name: 'AWS Polly - cy-GB - Male - Geraint', + }, + 'en-IN_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-IN', + ename: 'Raveena', + ssml: true, + name: 'AWS Polly - en-IN - Female - Raveena', + }, + 'en-US_AP_Male_Chipmunk': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Chipmunk', + ssml: true, + name: 'AWS Polly - en-US - Male - Chipmunk', + }, + 'en-US_AP_Male_Eric': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Eric', + ssml: true, + name: 'AWS Polly - en-US - Male - Eric', + }, + 'en-US_AP_Female_Ivy': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Ivy', + ssml: true, + name: 'AWS Polly - en-US - Female - Ivy', + }, + 'en-US_AP_Female_Jennifer': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Jennifer', + ssml: true, + name: 'AWS Polly - en-US - Female - Jennifer', + }, + 'en-US_AP_Male_Justin': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Justin', + ssml: true, + name: 'AWS Polly - en-US - Male - Justin', + }, + 'en-US_AP_Female_Kendra': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Kendra', + ssml: true, + name: 'AWS Polly - en-US - Female - Kendra', + }, + 'en-US_AP_Female_Kimberly': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'en-US', + ename: 'Kimberly', + ssml: true, + name: 'AWS Polly - en-US - Female - Kimberly', + }, + 'es-ES_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'es-ES', + ename: 'Conchita', + ssml: true, + name: 'AWS Polly - es-ES - Female - Conchita', + }, + 'es-ES_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'es-ES', + ename: 'Enrique', + ssml: true, + name: 'AWS Polly - es-ES - Male - Enrique', + }, + 'es-US_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'es-US', + ename: 'Penelope', + ssml: true, + name: 'AWS Polly - es-US - Female - Penelope', + }, + 'es-US_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'es-US', + ename: 'Miguel', + ssml: true, + name: 'AWS Polly - es-US - Male - Miguel', + }, + 'fr-CA_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'fr-CA', + ename: 'Chantal', + ssml: true, + name: 'AWS Polly - fr-CA - Female - Chantal', + }, + 'fr-FR_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'fr-FR', + ename: 'Celine', + ssml: true, + name: 'AWS Polly - fr-FR - Female - Celine', + }, + 'fr-FR_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'fr-FR', + ename: 'Mathieu', + ssml: true, + name: 'AWS Polly - fr-FR - Male - Mathieu', + }, + 'is-IS_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'is-IS', + ename: 'Dora', + ssml: true, + name: 'AWS Polly - is-IS - Female - Dora', + }, + 'is-IS_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'is-IS', + ename: 'Karl', + ssml: true, + name: 'AWS Polly - is-IS - Male - Karl', + }, + 'it-IT_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'it-IT', + ename: 'Carla', + ssml: true, + name: 'AWS Polly - it-IT - Female - Carla', + }, + 'it-IT_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'it-IT', + ename: 'Giorgio', + ssml: true, + name: 'AWS Polly - it-IT - Male - Giorgio', + }, + 'nb-NO_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'nb-NO', + ename: 'Liv', + ssml: true, + name: 'AWS Polly - nb-NO - Female - Liv', + }, + 'nl-NL_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'nl-NL', + ename: 'Lotte', + ssml: true, + name: 'AWS Polly - nl-NL - Female - Lotte', + }, + 'nl-NL_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'nl-NL', + ename: 'Ruben', + ssml: true, + name: 'AWS Polly - nl-NL - Male - Ruben', + }, + 'pl-PL_AP_Female_Agnieszka': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pl-PL', + ename: 'Agnieszka', + ssml: true, + name: 'AWS Polly - pl-PL - Female - Agnieszka', + }, + 'pl-PL_AP_Male_Jacek': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pl-PL', + ename: 'Jacek', + ssml: true, + name: 'AWS Polly - pl-PL - Male - Jacek', + }, + 'pl-PL_AP_Female_Ewa': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pl-PL', + ename: 'Ewa', + ssml: true, + name: 'AWS Polly - pl-PL - Female - Ewa', + }, + 'pl-PL_AP_Male_Jan': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pl-PL', + ename: 'Jan', + ssml: true, + name: 'AWS Polly - pl-PL - Male - Jan', + }, + 'pl-PL_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pl-PL', + ename: 'Maja', + ssml: true, + name: 'AWS Polly - pl-PL - Female - Maja', + }, + 'pt-BR_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pt-BR', + ename: 'Vitoria', + ssml: true, + name: 'AWS Polly - pt-BR - Female - Vitoria', + }, + 'pt-BR_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pt-BR', + ename: 'Ricardo', + ssml: true, + name: 'AWS Polly - pt-BR - Male - Ricardo', + }, + 'pt-PT_AP_Male': { + gender: 'Male', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pt-PT', + ename: 'Cristiano', + ssml: true, + name: 'AWS Polly - pt-PT - Male - Cristiano', + }, + 'pt-PT_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'pt-PT', + ename: 'Ines', + ssml: true, + name: 'AWS Polly - pt-PT - Female - Ines', + }, + 'ro-RO_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'ro-RO', + ename: 'Carmen', + ssml: true, + name: 'AWS Polly - ro-RO - Female - Carmen', + }, + 'sv-SE_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'sv-SE', + ename: 'Astrid', + ssml: true, + name: 'AWS Polly - sv-SE - Female - Astrid', + }, + 'tr-TR_AP_Female': { + gender: 'Female', + engine: 'polly', + params: ['accessKey', 'secretKey', 'region'], + language: 'tr-TR', + ename: 'Filiz', + ssml: true, + name: 'AWS Polly - tr-TR - Female - Filiz', + }, }; -class ActionSayText extends GenericBlock { - constructor(props) { +class ActionSayText extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionSayText.getStaticData()); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionSayText, context: RuleContext): string { if (!config.text) { return `// no text defined _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; - } else { - return `// Sayit ${config.text || ''} + } + return `// Sayit ${config.text || ''} \t\tconst subActionVar${config._id} = "${config.language && config.language !== '_' ? `${config.language};` : ''}${config.volume ? `${config.volume};` : ''}${(config.text || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tawait setStateAsync("${config.instance}.tts.text", subActionVar${config._id});`; - } } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `${I18n.t('Say:')} ${debugMessage.data.text}`; } - onTagChange(tagCard) { + onTagChange(): void { const lang = I18n.getLanguage(); const languages = Object.keys(sayitEngines).filter(l => l.startsWith(lang)); - const options = languages.map(lang => ({ title: sayitEngines[lang].name, value: lang })); + const options = languages.map(lang => ({ + title: (sayitEngines as Record)[lang].name, + value: lang, + })); options.unshift({ title: 'Default', value: '_' }); - this.setState({ - inputs: [ - { - attr: 'instance', - nameRender: 'renderInstance', - adapter: 'sayit', - defaultValue: 'sayit.0', - frontText: 'Instance:', - }, - { - nameRender: 'renderSelect', - frontText: 'Language:', - options, - defaultValue: '_', - attr: 'language', - }, - { - nameRender: 'renderNameText', - defaultValue: 'Volume', - attr: 'textVol', - }, - { - nameRender: 'renderSlider', - attr: 'volume', - defaultValue: 100, - min: 0, - max: 100 - }, - { - attr: 'text', - nameRender: 'renderModalInput', - defaultValue: 'Hallo', - nameBlock: '', - frontText: 'Text:', - } - ] - }, () => super.onTagChange(tagCard)); + this.setState( + { + inputs: [ + { + attr: 'instance', + nameRender: 'renderInstance', + adapter: 'sayit', + defaultValue: 'sayit.0', + frontText: 'Instance:', + }, + { + nameRender: 'renderSelect', + frontText: 'Language:', + options, + defaultValue: '_', + attr: 'language', + }, + { + nameRender: 'renderNameText', + defaultValue: 'Volume', + attr: 'textVol', + }, + { + nameRender: 'renderSlider', + attr: 'volume', + defaultValue: 100, + min: 0, + max: 100, + }, + { + attr: 'text', + nameRender: 'renderModalInput', + defaultValue: 'Hallo', + nameBlock: '', + frontText: 'Text:', + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Say It', id: 'ActionSayText', adapter: 'sayit', title: 'Say some text via sayit adapter', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionSayText.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSendEmail.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSendEmail.tsx index ad72a05f..6baed1b6 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSendEmail.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSendEmail.tsx @@ -1,17 +1,18 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionSendEmail, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; -class ActionSendEmail extends GenericBlock { - constructor(props) { +class ActionSendEmail extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionSendEmail.getStaticData()); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionSendEmail, context: RuleContext): string { if (!config.recipients) { return `// no recipients defined' _sendToFrontEnd(${config._id}, {text: 'No recipients defined'});`; - } else { - return `// Send Email ${config.text || ''} + } + return `// Send Email ${config.text || ''} \t\tconst subActionVar${config._id} = "${(config.text || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tsendTo("${config.instance || 'email.0'}", { @@ -19,59 +20,64 @@ _sendToFrontEnd(${config._id}, {text: 'No recipients defined'});`; \t\t subject: "${(config.subject || 'ioBroker').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}, \t\t text: subActionVar${config._id} \t\t});`; - } } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `${I18n.t('Sent:')} ${debugMessage.data.text}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - attr: 'instance', - nameRender: 'renderInstance', - defaultValue: 'email.0', - frontText: 'Instance:', - adapter: 'email', - }, - { - attr: 'recipients', - nameRender: 'renderText', - defaultValue: 'user@mail.ru', - frontText: 'To:', - }, - { - attr: 'subject', - nameRender: 'renderText', - defaultValue: 'Email from iobroker', - nameBlock: '', - frontText: 'Subject:', - }, - { - attr: 'text', - nameRender: 'renderModalInput', - defaultValue: 'Email from iobroker', - nameBlock: '', - frontText: 'Body:', - } - ] - }, () => super.onTagChange(tagCard)); + onTagChange(): void { + this.setState( + { + inputs: [ + { + attr: 'instance', + nameRender: 'renderInstance', + defaultValue: 'email.0', + frontText: 'Instance:', + adapter: 'email', + }, + { + attr: 'recipients', + nameRender: 'renderText', + defaultValue: 'user@mail.ru', + frontText: 'To:', + }, + { + attr: 'subject', + nameRender: 'renderText', + defaultValue: 'Email from iobroker', + nameBlock: '', + frontText: 'Subject:', + }, + { + attr: 'text', + nameRender: 'renderModalInput', + defaultValue: 'Email from iobroker', + nameBlock: '', + frontText: 'Body:', + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Send email', id: 'ActionSendEmail', adapter: 'email', title: 'Sends an email', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionSendEmail.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetState.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetState.tsx index 5feddeb0..de328d3e 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetState.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetState.tsx @@ -1,5 +1,15 @@ +import React from 'react'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionSetState, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleInputObjectID, + RuleTagCardTitle, +} from '../../types'; +import { renderValue } from '../../helpers/utils'; const styles = { valueAck: { @@ -10,17 +20,19 @@ const styles = { }, }; -class ActionSetState extends GenericBlock { - constructor(props) { +class ActionSetState extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionSetState.getStaticData()); } - isAllTriggersOnState() { - return this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && - !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState'); + isAllTriggersOnState(): boolean { + return ( + !!this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && + !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState') + ); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionSetState, context: RuleContext): string { let value = config.value; if (config.useTrigger) { value = config.toggle ? '!obj.state.val' : 'obj.state.val'; @@ -29,12 +41,13 @@ class ActionSetState extends GenericBlock { value = ''; } - if (typeof config.value === 'string' && + if ( + typeof config.value === 'string' && parseFloat(config.value).toString() !== config.value && config.value !== 'true' && config.value !== 'false' ) { - value = `"${value.replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}`; + value = `"${(value as string).replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}`; } } let v; @@ -50,33 +63,30 @@ class ActionSetState extends GenericBlock { \t\tawait setStateAsync("${config.oid}", subActionVar${config._id}, ${config.tagCard === 'update'});`; } - static renderValue(val) { - if (val === null) { - return 'null'; - } else if (val === undefined) { - return 'undefined'; - } else if (Array.isArray(val)) { - return val.join(', '); - } else if (typeof val === 'object') { - return JSON.stringify(val); - } else { - return val.toString(); - } - } - - renderDebug(debugMessage) { - return {I18n.t('Set:')} {ActionSetState.renderValue(debugMessage.data.val)}; + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { ack: boolean; val: any } }): React.JSX.Element { + return ( + + {I18n.t('Set:')}{' '} + + {renderValue(debugMessage.data.val)} + + + ); } - _setInputs(useTrigger, toggle) { + _setInputs( + useTrigger?: boolean, + toggle?: boolean, + ): { inputs: RuleInputAny[]; newSettings?: Partial } { const isAllTriggersOnState = this.isAllTriggersOnState(); - toggle = toggle === undefined ? this.state.settings.toggle : toggle; + toggle = toggle === undefined ? this.state.settings.toggle : toggle; useTrigger = useTrigger === undefined ? this.state.settings.useTrigger : useTrigger; - let type = ''; - let options; - const {oidType, oidUnit, oidStates, oidMax, oidMin, oidRole, oidWrite, oidStep} = this.state.settings; - let settings; + let type: 'number' | 'string' | 'boolean' | 'button' | '' | 'slider' | 'color' | 'select' = ''; + let options: { value: string; title: string }[] | undefined; + const { oidType, oidUnit, oidStates, oidMax, oidMin, oidRole, oidWrite, oidStep } = this.state.settings; + let settings: Partial | undefined; if (oidType) { if (oidType === 'number') { @@ -102,7 +112,7 @@ class ActionSetState extends GenericBlock { } } - let inputs; + let inputs: RuleInputAny[]; if (isAllTriggersOnState && useTrigger) { inputs = [ { @@ -123,46 +133,57 @@ class ActionSetState extends GenericBlock { } else { switch (type) { case 'number': - inputs = [{ - backText: oidUnit || '', - frontText: 'with', - nameRender: 'renderNumber', - defaultValue: oidMax === undefined ? 0 : oidMax, - attr: 'value' - }]; - if (this.state.settings.value !== undefined && isNaN(parseFloat(this.state.settings.value))) { + inputs = [ + { + backText: oidUnit || '', + frontText: 'with', + nameRender: 'renderNumber', + defaultValue: oidMax === undefined ? 0 : oidMax, + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + isNaN(parseFloat(this.state.settings.value as string)) + ) { settings = { value: oidMax === undefined ? 0 : oidMax }; } break; - case 'slider': - inputs = [{ - nameRender: 'renderSlider', - defaultValue: oidMax, - min: oidMin, - max: oidMax, - unit: oidUnit, - step: oidStep, - attr: 'value' - }]; - const f = parseFloat(this.state.settings.value); - if (this.state.settings.value !== undefined && - (isNaN(f) || f < oidMin || f > oidMax) - ) { + case 'slider': { + inputs = [ + { + nameRender: 'renderSlider', + defaultValue: oidMax, + min: oidMin, + max: oidMax, + unit: oidUnit, + step: oidStep, + attr: 'value', + }, + ]; + const f = parseFloat(this.state.settings.value as string); + if (this.state.settings.value !== undefined && (isNaN(f) || f < oidMin || f > oidMax)) { settings = { value: oidMax }; } break; + } case 'select': - inputs = [{ - nameRender: 'renderSelect', - frontText: 'with', - options, - defaultValue: options[0].value, - attr: 'value' - }]; - if (this.state.settings.value !== undefined && !options.find(item => item.value === this.state.settings.value)) { - settings = { value: options[0].value }; + inputs = [ + { + nameRender: 'renderSelect', + frontText: 'with', + options: options as { title: string; value: string }[], + defaultValue: options?.[0].value || '', + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + !options?.find(item => item.value === this.state.settings.value) + ) { + settings = { value: options?.[0].value || '' }; } break; @@ -173,7 +194,7 @@ class ActionSetState extends GenericBlock { attr: 'toggle', nameRender: 'renderCheckbox', defaultValue: false, - } + }, ]; if (!toggle) { inputs.push({ @@ -181,51 +202,61 @@ class ActionSetState extends GenericBlock { frontText: 'false', nameRender: 'renderSwitch', defaultValue: false, - attr: 'value' + attr: 'value', }); } - if (this.state.settings.value !== undefined && this.state.settings.value !== false && this.state.settings.value !== true) { + if ( + this.state.settings.value !== undefined && + this.state.settings.value !== false && + this.state.settings.value !== true + ) { settings = { value: false }; } break; case 'button': - inputs = [{ - nameRender: 'renderButton', - defaultValue: true, - attr: 'value' - }]; + inputs = [ + { + nameRender: 'renderButton', + defaultValue: true, + attr: 'value', + }, + ]; if (this.state.settings.value !== undefined && this.state.settings.value !== true) { settings = { value: true }; } break; case 'color': - inputs = [{ - nameRender: 'renderColor', - frontText: 'with', - defaultValue: '#FFFFFF', - attr: 'value' - }]; - if (this.state.settings.value !== undefined && - ( - typeof this.state.settings.value !== 'string' || - (typeof this.state.settings.value.startsWith('#') && - typeof this.state.settings.value.startsWith('rgb')) - )) { + inputs = [ + { + nameRender: 'renderColor', + frontText: 'with', + defaultValue: '#FFFFFF', + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + (typeof this.state.settings.value !== 'string' || + (!this.state.settings.value.startsWith('#') && + !this.state.settings.value.startsWith('rgb'))) + ) { settings = { value: '#FFFFFF' }; } break; default: - inputs = [{ - backText: oidUnit || '', - frontText: 'with', - nameRender: 'renderText', - defaultValue: '', - attr: 'value' - }]; + inputs = [ + { + backText: oidUnit || '', + frontText: 'with', + nameRender: 'renderText', + defaultValue: '', + attr: 'value', + }, + ]; break; } @@ -241,7 +272,13 @@ class ActionSetState extends GenericBlock { return { inputs, newSettings: settings }; } - onTagChange(tagCard, cb, ignore, toggle, useTrigger) { + onTagChange( + _tagCard?: RuleTagCardTitle, + cb?: () => void, + _ignore?: any, + toggle?: boolean, + useTrigger?: boolean, + ): void { useTrigger = useTrigger === undefined ? this.state.settings.useTrigger : useTrigger; const { inputs, newSettings } = this._setInputs(useTrigger, toggle); inputs.unshift({ @@ -249,7 +286,7 @@ class ActionSetState extends GenericBlock { attr: 'oid', defaultValue: '', checkReadOnly: true, - }); + } as RuleInputObjectID); this.setState({ inputs }, () => super.onTagChange(null, () => { @@ -259,18 +296,25 @@ class ActionSetState extends GenericBlock { this.setState(settings); this.props.onChange(settings); } - })); + }), + ); } - onValueChanged(value, attr, context) { - this.onTagChange(undefined, undefined, undefined, attr === 'toggle' ? value : undefined, attr === 'useTrigger' ? value : undefined); + onValueChanged(value?: any, attr?: string): void { + this.onTagChange( + undefined, + undefined, + undefined, + attr === 'toggle' ? value : undefined, + attr === 'useTrigger' ? value : undefined, + ); } - onUpdate() { + onUpdate(): void { this.onTagChange(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Set state action', @@ -278,11 +322,13 @@ class ActionSetState extends GenericBlock { icon: 'PlayForWork', tagCardArray: ['control', 'update'], title: 'Control or update some state', - helpDialog: 'You can use %s in the value to use the current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the value to use the current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionSetState.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetStateDelayed.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetStateDelayed.tsx index 0412f3d6..d3d05c73 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetStateDelayed.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionSetStateDelayed.tsx @@ -1,17 +1,31 @@ +import React from 'react'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionSetState, + RuleBlockConfigActionSetStateDelayed, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleInputNumber, + RuleInputObjectID, + RuleTagCardTitle, +} from '@/Components/RulesEditor/types'; +import { renderValue } from '../../helpers/utils'; -class ActionSetStateDelayed extends GenericBlock { - constructor(props) { +class ActionSetStateDelayed extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionSetStateDelayed.getStaticData()); } - isAllTriggersOnState() { - return this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && - !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState'); + isAllTriggersOnState(): boolean { + return ( + !!this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && + !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState') + ); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionSetStateDelayed, context: RuleContext): string { let value = config.value; if (config.useTrigger) { value = config.toggle ? '!obj.state.val' : 'obj.state.val'; @@ -20,12 +34,13 @@ class ActionSetStateDelayed extends GenericBlock { value = ''; } - if (typeof config.value === 'string' && + if ( + typeof config.value === 'string' && parseFloat(config.value).toString() !== config.value && config.value !== 'true' && config.value !== 'false' ) { - value = `"${value.replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}`; + value = `"${(value as string).replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}`; } } let v; @@ -38,34 +53,32 @@ class ActionSetStateDelayed extends GenericBlock { return `// set delayed state ${config.oid} to ${config.toggle && !config.useTrigger ? 'toggle' : value} with delay of ${config.delay}ms \t\t${v}; \t\t_sendToFrontEnd(${config._id}, {val: subActionVar${config._id}, ack: ${config.tagCard === 'update'}}); -\t\tsetStateDelayed("${config.oid}", subActionVar${config._id}, ${config.tagCard === 'update'}, ${parseInt(config.delay, 10)}, ${config.clearRunning ? 'true' : 'false'});`; +\t\tsetStateDelayed("${config.oid}", subActionVar${config._id}, ${config.tagCard === 'update'}, ${parseInt(config.delay as string, 10)}, ${config.clearRunning ? 'true' : 'false'});`; } - static renderValue(val) { - if (val === null) { - return 'null'; - } else if (val === undefined) { - return 'undefined'; - } else if (Array.isArray(val)) { - return val.join(', '); - } else if (typeof val === 'object') { - return JSON.stringify(val); - } else { - return val.toString(); - } + renderDebug(debugMessage: { data: { ack: boolean; val: any } }): React.JSX.Element { + return ( + + {I18n.t('Set:')}{' '} + + {renderValue(debugMessage.data.val)} + + + ); } - renderDebug(debugMessage) { - return {I18n.t('Set:')} {ActionSetStateDelayed.renderValue(debugMessage.data.val)}; - } - - _setInputs(useTrigger, toggle) { + _setInputs( + useTrigger?: boolean, + toggle?: boolean, + ): { inputs: RuleInputAny[]; newSettings?: Partial } { const isAllTriggersOnState = this.isAllTriggersOnState(); toggle = toggle === undefined ? this.state.settings.toggle : toggle; useTrigger = useTrigger === undefined ? this.state.settings.useTrigger : useTrigger; - let type = ''; - let options; + let type: 'number' | 'string' | 'boolean' | 'button' | '' | 'slider' | 'color' | 'select' = ''; + let options: { value: string; title: string }[] | undefined; const { oidType, oidUnit, oidStates, oidMax, oidMin, oidRole, oidWrite, oidStep } = this.state.settings; let settings; @@ -92,7 +105,7 @@ class ActionSetStateDelayed extends GenericBlock { type = 'select'; } } - let inputs; + let inputs: RuleInputAny[]; if (isAllTriggersOnState && useTrigger) { inputs = [ { @@ -113,46 +126,57 @@ class ActionSetStateDelayed extends GenericBlock { } else { switch (type) { case 'number': - inputs = [{ - backText: oidUnit || '', - frontText: 'with', - nameRender: 'renderNumber', - defaultValue: oidMax === undefined ? 0 : oidMax, - attr: 'value', - }]; - if (this.state.settings.value !== undefined && isNaN(parseFloat(this.state.settings.value))) { + inputs = [ + { + backText: oidUnit || '', + frontText: 'with', + nameRender: 'renderNumber', + defaultValue: oidMax === undefined ? 0 : oidMax, + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + isNaN(parseFloat(this.state.settings.value as string)) + ) { settings = { value: oidMax === undefined ? 0 : oidMax }; } break; - case 'slider': - inputs = [{ - nameRender: 'renderSlider', - defaultValue: oidMax, - min: oidMin, - max: oidMax, - unit: oidUnit, - step: oidStep, - attr: 'value', - }]; - const f = parseFloat(this.state.settings.value); - if (this.state.settings.value !== undefined && - (isNaN(f) || f < oidMin || f > oidMax) - ) { + case 'slider': { + inputs = [ + { + nameRender: 'renderSlider', + defaultValue: oidMax, + min: oidMin, + max: oidMax, + unit: oidUnit, + step: oidStep, + attr: 'value', + }, + ]; + const f = parseFloat(this.state.settings.value as string); + if (this.state.settings.value !== undefined && (isNaN(f) || f < oidMin || f > oidMax)) { settings = { value: oidMax }; } break; + } case 'select': - inputs = [{ - nameRender: 'renderSelect', - frontText: 'with', - options, - defaultValue: options[0].value, - attr: 'value', - }]; - if (this.state.settings.value !== undefined && !options.find(item => item.value === this.state.settings.value)) { - settings = { value: options[0].value }; + inputs = [ + { + nameRender: 'renderSelect', + frontText: 'with', + options: options as { value: string; title: string }[], + defaultValue: options?.[0].value || '', + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + !options?.find(item => item.value === this.state.settings.value) + ) { + settings = { value: options?.[0].value || '' }; } break; @@ -163,7 +187,7 @@ class ActionSetStateDelayed extends GenericBlock { attr: 'toggle', nameRender: 'renderCheckbox', defaultValue: false, - } + }, ]; if (!toggle) { inputs.push({ @@ -175,47 +199,57 @@ class ActionSetStateDelayed extends GenericBlock { }); } - if (this.state.settings.value !== undefined && this.state.settings.value !== false && this.state.settings.value !== true) { + if ( + this.state.settings.value !== undefined && + this.state.settings.value !== false && + this.state.settings.value !== true + ) { settings = { value: false }; } break; case 'button': - inputs = [{ - nameRender: 'renderButton', - defaultValue: true, - attr: 'value', - }]; + inputs = [ + { + nameRender: 'renderButton', + defaultValue: true, + attr: 'value', + }, + ]; if (this.state.settings.value !== undefined && this.state.settings.value !== true) { settings = { value: true }; } break; case 'color': - inputs = [{ - nameRender: 'renderColor', - frontText: 'with', - defaultValue: '#FFFFFF', - attr: 'value', - }]; - if (this.state.settings.value !== undefined && - ( - typeof this.state.settings.value !== 'string' || - (typeof this.state.settings.value.startsWith('#') && - typeof this.state.settings.value.startsWith('rgb')) - )) { + inputs = [ + { + nameRender: 'renderColor', + frontText: 'with', + defaultValue: '#FFFFFF', + attr: 'value', + }, + ]; + if ( + this.state.settings.value !== undefined && + (typeof this.state.settings.value !== 'string' || + (!this.state.settings.value.startsWith('#') && + !this.state.settings.value.startsWith('rgb'))) + ) { settings = { value: '#FFFFFF' }; } break; default: - inputs = [{ - backText: oidUnit || '', - frontText: 'with', - nameRender: 'renderText', - defaultValue: '', - attr: 'value', - }]; + inputs = [ + { + backText: oidUnit || '', + frontText: 'with', + nameRender: 'renderText', + defaultValue: '', + attr: 'value', + }, + ]; break; } if (isAllTriggersOnState) { @@ -231,31 +265,38 @@ class ActionSetStateDelayed extends GenericBlock { backText: 'ms', frontText: 'Delay', nameRender: 'renderNumber', - defaultValue: '1000', + defaultValue: 1000, noHelperText: true, attr: 'delay', - }); + } as RuleInputNumber); + inputs.push({ backText: 'clear running', nameRender: 'renderCheckbox', defaultValue: true, attr: 'clearRunning', - }) + }); return { inputs, newSettings: settings }; } - onTagChange(tagCard, cb, ignore, toggle, useTrigger) { + onTagChange( + _tagCard?: RuleTagCardTitle, + cb?: () => void, + _ignore?: any, + toggle?: boolean, + useTrigger?: boolean, + ): void { useTrigger = useTrigger === undefined ? this.state.settings.useTrigger : useTrigger; - const {inputs, newSettings} = this._setInputs(useTrigger, toggle); + const { inputs, newSettings } = this._setInputs(useTrigger, toggle); inputs.unshift({ nameRender: 'renderObjectID', attr: 'oid', defaultValue: '', checkReadOnly: true, - }); + } as RuleInputObjectID); - this.setState({inputs}, () => + this.setState({ inputs }, () => super.onTagChange(null, () => { if (newSettings) { const settings = JSON.parse(JSON.stringify(this.state.settings)); @@ -263,18 +304,25 @@ class ActionSetStateDelayed extends GenericBlock { this.setState(settings); this.props.onChange(settings); } - })); + }), + ); } - onValueChanged(value, attr, context) { - this.onTagChange(undefined, undefined, undefined, attr === 'toggle' ? value : undefined, attr === 'useTrigger' ? value : undefined); + onValueChanged(value?: any, attr?: string): void { + this.onTagChange( + undefined, + undefined, + undefined, + attr === 'toggle' ? value : undefined, + attr === 'useTrigger' ? value : undefined, + ); } - onUpdate() { + onUpdate(): void { this.onTagChange(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Set with delay', @@ -282,11 +330,13 @@ class ActionSetStateDelayed extends GenericBlock { icon: 'PlayForWork', tagCardArray: ['control', 'update'], title: 'Control or update some state with delay', - helpDialog: 'You can use %s in the value to use the current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the value to use the current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionSetStateDelayed.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionTelegram.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionTelegram.tsx index 5fd1d69c..f4ef071b 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionTelegram.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionTelegram.tsx @@ -1,14 +1,17 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { RuleBlockConfigActionTelegram, RuleBlockDescription, RuleContext } from '@/Components/RulesEditor/types'; -class ActionTelegram extends GenericBlock { - constructor(props) { +class ActionTelegram extends GenericBlock { + private readonly cachePromises: Record>; + + constructor(props: GenericBlockProps) { super(props, ActionTelegram.getStaticData()); this.cachePromises = {}; } - static compile(config, context) { - let text = (config.text || '').replace(/"/g, '\\"'); + static compile(config: RuleBlockConfigActionTelegram, context: RuleContext): string { + const text = (config.text || '').replace(/"/g, '\\"'); if (!text) { return `// no text defined _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; @@ -19,59 +22,71 @@ _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; \t\tsendTo("${config.instance}", "send", ${config.user && config.user !== '_' ? `{user: "${(config.user || '').replace(/"/g, '\\"')}", text: subActionVar${config._id}}` : `subActionVar${config._id}`});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `${I18n.t('Sent:')} ${debugMessage.data.text}`; } - onValueChanged(value, attr) { + onValueChanged(value: any, attr: string): void { if (attr === 'instance') { this._setUsers(value); } } - _setUsers(instance) { + _setUsers(instance?: string): void { instance = instance || this.state.settings.instance || 'telegram.0'; - this.cachePromises[instance] = this.cachePromises[instance] || this.props.socket.getState(`${instance}.communicate.users`); + if (!(this.cachePromises[instance] instanceof Promise)) { + this.cachePromises[instance] = this.props.socket.getState(`${instance}.communicate.users`); + } if (!this.state.settings._id) { - return this.setState({ - inputs: [ - { - nameRender: 'renderSelect', - adapter: 'telegram', - frontText: 'Instance:', - defaultValue: 'telegram.0', - attr: 'instance', - }, - { - nameRender: 'renderSelect', - attr: 'user', - options: [{title: 'telegram.0', value: 'telegram.0'}], - defaultValue: '', - frontText: 'User:', - }, - { - nameRender: 'renderModalInput', - attr: 'text', - defaultValue: 'Hallo', - nameBlock: '', - frontText: 'Text:', - } - ] - }, () => super.onTagChange()); + return this.setState( + { + inputs: [ + { + nameRender: 'renderSelect', + adapter: 'telegram', + frontText: 'Instance:', + defaultValue: 'telegram.0', + attr: 'instance', + }, + { + nameRender: 'renderSelect', + attr: 'user', + options: [{ title: 'telegram.0', value: 'telegram.0' }], + defaultValue: '', + frontText: 'User:', + }, + { + nameRender: 'renderModalInput', + attr: 'text', + defaultValue: 'Hallo', + nameBlock: '', + frontText: 'Text:', + }, + ], + }, + () => super.onTagChange(), + ); } - this.cachePromises[instance] - .then(users => { - try { - users = users?.val ? JSON.parse(users.val) : null; - users = users && Object.keys(users).map(user => ({title: users[user].userName || users[user].firstName, value: user})); - users = users || []; - users.unshift({ title: 'all', value: '' }); - } catch (e) { - users = [{ title: 'all', value: '' }]; - } + void this.cachePromises[instance].then((usersObj: ioBroker.State | null | undefined) => { + let users: { title: string; value: string }[]; + try { + const usersA = usersObj?.val ? JSON.parse(usersObj.val as string) : null; + users = usersA + ? Object.keys(usersA).map(user => ({ + title: usersA[user].userName || usersA[user].firstName, + value: user, + })) + : []; + users = users || []; + users.unshift({ title: 'all', value: '' }); + } catch { + users = [{ title: 'all', value: '' }]; + } - this.setState({ + this.setState( + { inputs: [ { nameRender: 'renderInstance', @@ -94,27 +109,31 @@ _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; nameBlock: '', frontText: 'Text:', }, - ] - }, () => super.onTagChange()); - }); + ], + }, + () => super.onTagChange(), + ); + }); } - onTagChange(tagCard) { + onTagChange(): void { this._setUsers(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Telegram', id: 'ActionTelegram', adapter: 'telegram', title: 'Sends message via telegram', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionTelegram.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ActionWhatsappcmb.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ActionWhatsappcmb.tsx index 2215dea5..795de825 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ActionWhatsappcmb.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ActionWhatsappcmb.tsx @@ -1,69 +1,78 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigActionWhatsappcmb, + RuleBlockDescription, + RuleContext, +} from '@/Components/RulesEditor/types'; -class ActionWhatsappcmb extends GenericBlock { - constructor(props) { +class ActionWhatsappcmb extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ActionWhatsappcmb.getStaticData()); - this.cachePromises = {}; } - static compile(config, context) { - let text = (config.text || '').replace(/"/g, '\\"'); + static compile(config: RuleBlockConfigActionWhatsappcmb, context: RuleContext): string { + const text = (config.text || '').replace(/"/g, '\\"'); if (!text) { return `// no text defined _sendToFrontEnd(${config._id}, {text: 'No text defined'});`; - } else { - return `// whatsapp ${text || ''} + } + return `// whatsapp ${text || ''} \t\tconst subActionVar${config._id} = "${(text || '').replace(/"/g, '\\"')}"${GenericBlock.getReplacesInText(context)}; \t\t_sendToFrontEnd(${config._id}, {text: subActionVar${config._id}}); \t\tsendTo("${config.instance}", "send", {text: subActionVar${config._id}${config.phone ? `, phone: "${config.phone.replace(/"/g, '\\"')}"` : ''}});`; - } } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { text: string } }): string { return `${I18n.t('Sent:')} ${debugMessage.data.text}`; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderInstance', - adapter: 'whatsapp-cmb', - frontText: 'Instance:', - defaultValue: 'whatsapp-cmb.0', - attr: 'instance', - }, - { - nameRender: 'renderModalInput', - attr: 'text', - defaultValue: 'Hello', - nameBlock: '', - frontText: 'Text:', - }, - { - nameRender: 'renderText', - attr: 'phone', - defaultValue: '', - frontText: 'Phone:', - backText: '(optional)', - } - ] - }, () => super.onTagChange()); + onTagChange(): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderInstance', + adapter: 'whatsapp-cmb', + frontText: 'Instance:', + defaultValue: 'whatsapp-cmb.0', + attr: 'instance', + }, + { + nameRender: 'renderModalInput', + attr: 'text', + defaultValue: 'Hello', + nameBlock: '', + frontText: 'Text:', + }, + { + nameRender: 'renderText', + attr: 'phone', + defaultValue: '', + frontText: 'Phone:', + backText: '(optional)', + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'actions', name: 'Whatsapp-cmb', id: 'ActionWhatsappcmb', adapter: 'whatsapp-cmb', title: 'Sends message via whatsapp-cmb', - helpDialog: 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', - } + helpDialog: + 'You can use %s in the text to display current trigger value or %id to display the triggered object ID', + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ActionWhatsappcmb.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionAstronomical.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionAstronomical.tsx index 612ae18c..b44c9acb 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionAstronomical.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionAstronomical.tsx @@ -1,18 +1,26 @@ +// @ts-expect-error no types available import SunCalc from 'suncalc2'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigConditionAstronomical, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleTagCard, +} from '../../types'; -class ConditionAstronomical extends GenericBlock { - constructor(props) { +class ConditionAstronomical extends GenericBlock { + private coordinates: { latitude: number; longitude: number } | null = null; + constructor(props: GenericBlockProps) { super(props, ConditionAstronomical.getStaticData()); - this.coordinates = null; } - static compile(config, context) { - const compare = config.tagCard === '=' ? '===' : (config.tagCard === '<>' ? '!==' : config.tagCard); + static compile(config: RuleBlockConfigConditionAstronomical, context: RuleContext): string { + const compare = config.tagCard === '=' ? '===' : config.tagCard === '<>' ? '!==' : config.tagCard; let offset; if (config.offset) { - offset = parseInt(config.offsetValue, 10) || 0; + offset = parseInt(config.offsetValue as unknown as string, 10) || 0; } const cond = `formatDate(Date.now(), 'hh:mm') ${compare} formatDate(getAstroDate("${config.astro}"${offset ? `, undefined, ${offset}` : ''}), 'hh:mm')`; context.conditionsVars.push(`const subCond${config._id} = ${cond};`); @@ -20,73 +28,76 @@ class ConditionAstronomical extends GenericBlock { return cond; } - static _time2String(time) { + static _time2String(time: Date): string { if (!time) { return '--:--'; } return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`; } - onValueChanged(value, attr) { + onValueChanged(value: any, attr: string): void { if (attr === 'astro') { - this._setAstro(value); + void this._setAstro(value); } else if (attr === 'offset') { - this._setAstro(undefined, value); + void this._setAstro(undefined, value); } else if (attr === 'offsetValue') { - this._setAstro(undefined, undefined, value); + void this._setAstro(undefined, undefined, value); } } - async _setAstro(astro, offset, offsetValue) { + async _setAstro(astro?: string, offset?: boolean, offsetValue?: number): Promise { astro = astro || this.state.settings.astro || 'solarNoon'; offset = offset === undefined ? this.state.settings.offset : offset; offsetValue = offsetValue === undefined ? this.state.settings.offsetValue : offsetValue; - offsetValue = parseInt(offsetValue, 10) || 0; + offsetValue = parseInt(offsetValue as unknown as string, 10) || 0; if (!this.coordinates) { - await this.props.socket.getObject('system.adapter.javascript.0') - .then(({ native: { latitude, longitude } }) => { - if (!latitude && !longitude) { - return this.props.socket.getObject('system.config') - .then(obj => { - if (obj && (obj.common.latitude || obj.common.longitude)) { - this.coordinates = { - latitude: obj.common.latitude, - longitude: obj.common.longitude, - } - } else { - this.coordinates = null; - } - }); - } else { - this.coordinates = { - latitude, - longitude, - }; - } - }); + const jsInstance: ioBroker.InstanceObject | null | undefined = + await this.props.socket.getObject('system.adapter.javascript.0'); + if (!jsInstance?.native.latitude && !jsInstance?.native.longitude) { + const systemConfig: ioBroker.SystemConfigObject | null | undefined = + await this.props.socket.getObject('system.config'); + if (systemConfig && (systemConfig.common.latitude || systemConfig.common.longitude)) { + this.coordinates = { + latitude: systemConfig.common.latitude as number, + longitude: systemConfig.common.longitude as number, + }; + } else { + this.coordinates = null; + } + } else { + this.coordinates = { + latitude: jsInstance?.native.latitude, + longitude: jsInstance?.native.longitude, + }; + } } - const sunValue = this.coordinates && SunCalc.getTimes(new Date(), this.coordinates.latitude, this.coordinates.longitude); - const options = sunValue ? Object.keys(sunValue).map(name => ({ - value: name, - title: name, - title2: `[${ConditionAstronomical._time2String(sunValue[name])}]`, - order: ConditionAstronomical._time2String(sunValue[name]), - })) : []; - options.sort((a, b) => a.order > b.order ? 1 : (a.order < b.order ? -1 : 0)); + const sunValue: Record | null = this.coordinates + ? SunCalc.getTimes(new Date(), this.coordinates.latitude, this.coordinates.longitude) + : null; + const options = sunValue + ? Object.keys(sunValue).map(name => ({ + value: name, + title: name, + title2: `[${ConditionAstronomical._time2String(sunValue[name])}]`, + order: ConditionAstronomical._time2String(sunValue[name]), + })) + : []; + options.sort((a, b) => (a.order > b.order ? 1 : a.order < b.order ? -1 : 0)); // calculate time text - const tagCardArray = ConditionAstronomical.getStaticData().tagCardArray; - const tag = tagCardArray.find(item => item.title === this.state.settings.tagCard); + const tagCardArray: RuleTagCard[] = ConditionAstronomical.getStaticData().tagCardArray as RuleTagCard[]; + const tag: RuleTagCard = + tagCardArray.find(item => item.title === this.state.settings.tagCard) || tagCardArray[0]; let time = '--:--'; if (astro && sunValue && sunValue[astro]) { const astroTime = new Date(sunValue[astro]); - offset && astroTime.setMinutes(astroTime.getMinutes() + parseInt(offsetValue, 10)); + offset && astroTime.setMinutes(astroTime.getMinutes() + parseInt(offsetValue as unknown as string, 10)); time = `(${I18n.t(tag.text)} ${ConditionAstronomical._time2String(astroTime)})`; } - let inputs; + let inputs: RuleInputAny[]; if (offset) { inputs = [ @@ -155,11 +166,11 @@ class ConditionAstronomical extends GenericBlock { this.setState({ inputs }, () => super.onTagChange()); } - onTagChange(tagCard) { - this._setAstro(); + onTagChange(): void { + void this._setAstro(); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'conditions', name: 'Astronomical', @@ -198,10 +209,11 @@ class ConditionAstronomical extends GenericBlock { }, ], title: 'Compares current time with astronomical event', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ConditionAstronomical.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionState.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionState.tsx index 1b05d6f2..42116ad6 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionState.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionState.tsx @@ -1,18 +1,23 @@ import React from 'react'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps, type GenericBlockState } from '../GenericBlock'; import HysteresisImage from '../../../assets/hysteresis.png'; +import type { + RuleBlockConfigActionActionState, + RuleBlockConfigTriggerState, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleInputDialog, + RuleInputSelect, + RuleTagCard, + RuleTagCardTitle, +} from '../../types'; const HYSTERESIS = `function __hysteresis(val, limit, state, hist, comp) { let cond1, cond2; @@ -45,26 +50,32 @@ const HYSTERESIS = `function __hysteresis(val, limit, state, hist, comp) { } }`; -class ConditionState extends GenericBlock { - constructor(props) { +interface ConditionStateState extends GenericBlockState { + showHysteresisHelp: boolean; +} + +class ConditionState extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ConditionState.getStaticData()); } - isAllTriggersOnState() { - return this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && - !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState'); + isAllTriggersOnState(): boolean { + return ( + !!this.props.userRules?.triggers?.find(item => item.id === 'TriggerState') && + !this.props.userRules?.triggers?.find(item => item.id !== 'TriggerState') + ); } - static compile(config, context) { + static compile(config: RuleBlockConfigActionActionState, context: RuleContext): string { let value = config.value; if (value === null || value === undefined) { value = false; } - let debugValue = ''; + let debugValue: string; let result; if (config.tagCard === '()') { - context.prelines = context.prelines || []; + context.prelines = context.prelines || []; !context.prelines.find(item => item !== HYSTERESIS) && context.prelines.push(HYSTERESIS); if (config.useTrigger) { debugValue = 'obj.state.val'; @@ -83,13 +94,12 @@ class ConditionState extends GenericBlock { result = `__hysteresis(subCondVar${config._id}, ${value}, __%%STATE%%__, ${config.hist}, "${config.histComp}")`; } - } else - if (config.tagCard !== 'includes') { - const compare = config.tagCard === '=' ? '==' : (config.tagCard === '<>' ? '!=' : config.tagCard); + } else if (config.tagCard !== 'includes') { + const compare = config.tagCard === '=' ? '==' : config.tagCard === '<>' ? '!=' : config.tagCard; if (config.useTrigger) { debugValue = 'obj.state.val'; - if (context?.trigger?.oidType === 'string') { - value = value.replace(/"/g, '\\"'); + if ((context?.trigger as RuleBlockConfigTriggerState)?.oidType === 'string') { + value = (value as string).replace(/"/g, '\\"'); result = `subCondVar${config._id} ${compare} "${value}"`; } else { if (value === '') { @@ -103,7 +113,7 @@ class ConditionState extends GenericBlock { } else { debugValue = `(await getStateAsync("${config.oid}")).val`; if (config.oidType === 'string') { - value = value.replace(/"/g, '\\"'); + value = (value as string).replace(/"/g, '\\"'); result = `subCondVar${config._id} ${compare} "${value}"`; } else { if (value === '') { @@ -118,8 +128,8 @@ class ConditionState extends GenericBlock { } else { if (config.useTrigger) { debugValue = 'obj.state.val'; - if (context?.trigger?.oidType === 'string') { - value = value.replace(/"/g, '\\"'); + if ((context?.trigger as RuleBlockConfigTriggerState)?.oidType === 'string') { + value = (value as string).replace(/"/g, '\\"'); result = `obj.state.val.includes("${value}")`; } else { result = `false`; @@ -127,7 +137,7 @@ class ConditionState extends GenericBlock { } else { debugValue = `(await getStateAsync("${config.oid}")).val`; if (config.oidType === 'string') { - value = value.replace(/"/g, '\\"'); + value = (value as string).replace(/"/g, '\\"'); result = `subCondVar${config._id}.includes("${value}")`; } else { result = `false`; @@ -137,11 +147,13 @@ class ConditionState extends GenericBlock { context.conditionsStates.push({ name: `subCondVar${config._id}`, id: config.oid }); context.conditionsVars.push(`const subCondVar${config._id} = ${debugValue};`); context.conditionsVars.push(`const subCond${config._id} = ${result};`); - context.conditionsDebug.push(`_sendToFrontEnd(${config._id}, {result: subCond${config._id}, value: subCondVar${config._id}, compareWith: "${value}"});`); + context.conditionsDebug.push( + `_sendToFrontEnd(${config._id}, {result: subCond${config._id}, value: subCondVar${config._id}, compareWith: "${value}"});`, + ); return `subCond${config._id}`; } - renderDebug(debugMessage) { + renderDebug(debugMessage: { data: { result: boolean; value: string; compareWith: string } }): string { const condition = this.state.settings.tagCard; if (condition === '()') { // TODO @@ -152,26 +164,35 @@ class ConditionState extends GenericBlock { return I18n.t('Triggered'); } - onShowHelp = () => this.setState({showHysteresisHelp: true}); + onShowHelp = (): void => this.setState({ showHysteresisHelp: true }); - _setInputs(useTrigger, tagCard, oidType, oidUnit, oidStates) { + _setInputs( + useTrigger: boolean | undefined, + tagCard?: RuleTagCardTitle, + oidType?: string, + oidUnit?: string, + oidStates?: Record, + ): void { const isAllTriggersOnState = this.isAllTriggersOnState(); - tagCard = tagCard || this.state.settings.tagCard; - oidType = oidType || this.state.settings.oidType; - oidUnit = oidUnit || this.state.settings.oidUnit; + tagCard = tagCard || this.state.settings.tagCard; + oidType = oidType || this.state.settings.oidType; + oidUnit = oidUnit || this.state.settings.oidUnit; oidStates = oidStates || this.state.settings.oidStates; + if (useTrigger === undefined) { + useTrigger = this.state.settings.useTrigger; + } if (isAllTriggersOnState && useTrigger && this.props.userRules?.triggers?.length === 1) { - oidType = this.props.userRules.triggers[0].oidType; - oidUnit = this.props.userRules.triggers[0].oidUnit; - oidStates = this.props.userRules.triggers[0].oidStates; + oidType = (this.props.userRules.triggers[0] as RuleBlockConfigTriggerState).oidType; + oidUnit = (this.props.userRules.triggers[0] as RuleBlockConfigTriggerState).oidUnit; + oidStates = (this.props.userRules.triggers[0] as RuleBlockConfigTriggerState).oidStates; } - const _tagCardArray = ConditionState.getStaticData().tagCardArray; - const tag = _tagCardArray.find(item => item.title === tagCard); - let tagCardArray; - let options = null; + const _tagCardArray: RuleTagCard[] = ConditionState.getStaticData().tagCardArray as RuleTagCard[]; + const tag: RuleTagCard = _tagCardArray.find(item => item.title === tagCard) || _tagCardArray[0]; + let tagCardArray: RuleTagCard[]; + let options: { value: string | boolean; title: string }[] | null = null; if (oidType === 'number') { tagCardArray = [ @@ -213,7 +234,14 @@ class ConditionState extends GenericBlock { ]; if (oidStates) { - options = Object.keys(oidStates).map(val => ({ value: val, title: oidStates[val] })); + options = Object.keys(oidStates) + .map(val => { + if (oidStates) { + return { value: val, title: oidStates[val] }; + } + return null; + }) + .filter(i => i) as { value: string | boolean; title: string }[]; } } else if (oidType === 'boolean') { tagCardArray = [ @@ -226,7 +254,7 @@ class ConditionState extends GenericBlock { title: '<>', title2: '[not equal]', text: 'not equal to', - } + }, ]; options = [ { title: 'false', value: false }, @@ -268,26 +296,29 @@ class ConditionState extends GenericBlock { title: '.', title2: '[includes]', text: 'includes', - } + }, ]; if (oidStates) { - options = Object.keys(oidStates).map(val => ({ value: val, title: oidStates[val] })); + options = Object.keys(oidStates).map(val => ({ + value: val, + title: oidStates ? oidStates[val] : val.toString(), + })); } } - let settings = null; + let settings: RuleBlockConfigActionActionState | null = null; if (!tagCardArray.find(item => item.title === tagCard)) { tagCard = tagCardArray[0].title; settings = settings || { ...this.state.settings }; settings.tagCard = tagCard; } - let inputs; - let renderText = { + let inputs: RuleInputAny[]; + let renderText: RuleInputAny = { nameRender: 'renderText', defaultValue: '', attr: 'value', - frontText: tagCard === '()' ? 'Limit' : (tag?.text || 'compare with'), + frontText: tagCard === '()' ? 'Limit' : tag?.text || 'compare with', doNotTranslateBack: true, backText: oidUnit, }; @@ -301,7 +332,8 @@ class ConditionState extends GenericBlock { frontText: tag?.text || 'compare with', doNotTranslateBack: true, backText: oidUnit, - }; + } as RuleInputSelect; + if (!options.find(item => item.value === this.state.settings.value)) { settings = settings || { ...this.state.settings }; settings.value = options[0].value; @@ -317,7 +349,7 @@ class ConditionState extends GenericBlock { title: '<>', title2: '[not equal]', text: 'not equal to', - } + }, ]; } } @@ -363,7 +395,8 @@ class ConditionState extends GenericBlock { icon: 'HelpOutline', frontText: 'Explanation', onShowDialog: this.onShowHelp, - }); + } as RuleInputDialog); + inputs.splice(2, 0, { nameRender: 'renderSelect', attr: 'histComp', @@ -371,13 +404,13 @@ class ConditionState extends GenericBlock { frontText: 'Condition', doNotTranslate: true, options: [ - { title: '>', value: '>' }, + { title: '>', value: '>' }, { title: '>=', value: '>=' }, - { title: '<', value: '<' }, + { title: '<', value: '<' }, { title: '<=', value: '<=' }, - { title: '=', value: '=' }, + { title: '=', value: '=' }, { title: '<>', value: '<>' }, - ] + ], }); inputs.push({ frontText: 'Δ', @@ -397,40 +430,41 @@ class ConditionState extends GenericBlock { inputs, }; - this.setState(state,() => + this.setState(state, () => super.onTagChange(null, () => { if (settings) { - this.setState({settings}); + this.setState({ settings }); this.props.onChange(settings); } - })); + }), + ); } - onValueChanged(value, attr, context) { + onValueChanged(value: any, attr: string): void { if (typeof value === 'object') { - this._setInputs(value.useTrigger, value.tagCard, value.oidType, value.states); + void this._setInputs(value.useTrigger, value.tagCard, value.oidType, value.states); } else { if (attr === 'useTrigger') { - this._setInputs(value); + void this._setInputs(value as boolean); } else if (attr === 'oidType') { - this._setInputs(value, undefined, value); + void this._setInputs(undefined, undefined, value as string); } else if (attr === 'oidUnit') { - this._setInputs(value, undefined, undefined, value); + void this._setInputs(undefined, undefined, undefined, value as string); } else if (attr === 'oidStates') { - this._setInputs(value, undefined, undefined, undefined, value); + void this._setInputs(undefined, undefined, undefined, undefined, value as Record); } } } - onUpdate() { + onUpdate(): void { this._setInputs(this.state.settings.useTrigger); } - onTagChange(tagCard) { + onTagChange(tagCard: RuleTagCardTitle): void { this._setInputs(this.state.settings.useTrigger, tagCard); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'conditions', name: 'State condition', @@ -476,39 +510,48 @@ class ConditionState extends GenericBlock { title: '()', title2: '[hysteresis]', text: 'hysteresis', - } + }, ], title: 'Compares the state value with user defined value', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ConditionState.getStaticData(); } - renderSpecific() { + renderSpecific(): React.JSX.Element | null { if (this.state.showHysteresisHelp) { - return this.setState({ showHysteresisHelp: false })} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - - - Hysteresis - - - - - - ; - } else { - return null; + return ( + this.setState({ showHysteresisHelp: false })} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + + Hysteresis + + + + + + + ); } + return null; } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionTime.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionTime.tsx index d51212de..c2932daf 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/ConditionTime.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/ConditionTime.tsx @@ -1,6 +1,14 @@ -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { + RuleBlockConfigConditionTime, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleTagCard, + RuleTagCardTitle, +} from '../../types'; -const DAYS = [ +const DAYS: number[] = [ 31, // 1 29, // 2 31, // 3 @@ -15,19 +23,19 @@ const DAYS = [ 31, // 12 ]; -class ConditionTime extends GenericBlock { - constructor(props) { +class ConditionTime extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, ConditionTime.getStaticData()); } - static compile(config, context) { - const compare = config.tagCard === '=' ? '===' : (config.tagCard === '<>' ? '!==' : config.tagCard); + static compile(config: RuleBlockConfigConditionTime, context: RuleContext): string { + const compare = config.tagCard === '=' ? '===' : config.tagCard === '<>' ? '!==' : config.tagCard; let cond; if (config.withDate) { - let [month, date] = (config.date || '01.01').toString().split('.'); - date = parseInt(date, 10) || 0; - month = parseInt(month, 10) || 0; + const [monthStr, dateStr] = (config.date || '01.01').toString().split('.'); + let date = parseInt(dateStr, 10) || 0; + let month = parseInt(monthStr, 10) || 0; if (month > 12) { month = 12; } else if (month < 0) { @@ -56,12 +64,12 @@ class ConditionTime extends GenericBlock { return `subCond${config._id}`; } - _setInputs(tagCard, withDate, ) { + _setInputs(tagCard?: RuleTagCardTitle, withDate?: boolean): void { withDate = withDate === undefined ? this.state.settings.withDate : withDate; tagCard = tagCard || this.state.settings.tagCard; - const tagCardArray = ConditionTime.getStaticData().tagCardArray; - const tag = tagCardArray.find(item => item.title === tagCard); - const inputs = [ + const tagCardArray: RuleTagCard[] = ConditionTime.getStaticData().tagCardArray as RuleTagCard[]; + const tag = tagCardArray?.find(item => item.title === tagCard); + const inputs: RuleInputAny[] = [ { nameRender: 'renderNameText', attr: 'interval', @@ -78,7 +86,7 @@ class ConditionTime extends GenericBlock { nameRender: 'renderCheckbox', attr: 'withDate', defaultValue: false, - } + }, ]; if (withDate) { inputs.push({ @@ -87,23 +95,26 @@ class ConditionTime extends GenericBlock { defaultValue: '01.01', }); } - this.setState({ - inputs, - iconTag:true - }, () => super.onTagChange()); + this.setState( + { + inputs, + iconTag: true, + }, + () => super.onTagChange(), + ); } - onValueChanged(value, attr) { + onValueChanged(value: any, attr: string): void { if (attr === 'withDate') { this._setInputs(undefined, value); } } - onTagChange(tagCard) { + onTagChange(tagCard: RuleTagCardTitle): void { this._setInputs(tagCard); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'conditions', name: 'Time condition', @@ -139,13 +150,14 @@ class ConditionTime extends GenericBlock { title: '<>', title2: '[not equal]', text: 'not equal to', - } + }, ], title: 'Compares current time with the user specific time', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return ConditionTime.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.tsx index 4804908f..581d6d40 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.tsx @@ -1,34 +1,73 @@ import React from 'react'; +// @ts-expect-error no types in suncalc2 import SunCalc from 'suncalc2'; -import { - ComplexCron, - Schedule, - I18n, -} from '@iobroker/adapter-react-v5'; -import convertCronToText from '@iobroker/adapter-react-v5/Components/SimpleCron/cronText'; +import { ComplexCron, Schedule, I18n } from '@iobroker/adapter-react-v5'; +import convertCronToText from '@iobroker/adapter-react-v5/build/Components/SimpleCron/cronText'; -import GenericBlock from '../GenericBlock'; -import Compile from '../../helpers/Compile'; +import { GenericBlock, type GenericBlockProps, type GenericBlockState } from '../GenericBlock'; +import { STANDARD_FUNCTION_STATE, STANDARD_FUNCTION_STATE_ONCHANGE } from '../../helpers/Compile'; import CustomInput from '../CustomInput'; import CustomButton from '../CustomButton'; import CustomModal from '../CustomModal'; +import type { + RuleBlockConfigTriggerSchedule, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleInputCron, + RuleInputNameText, + RuleInputText, + RuleInputWizard, + RuleTagCardTitle, +} from '@/Components/RulesEditor/types'; const DEFAULT_WIZARD = '{"time":{"start":"00:00","end":"24:00","mode":"hours","interval":1},"period":{"days":1}}'; -class TriggerScheduleBlock extends GenericBlock { - constructor(props) { +// todo use from adapter-react-v5 +export interface ScheduleConfig { + time: { + exactTime: boolean; + start: string; + end: string; + mode: string; + interval: number; + }; + period: { + once: string; + days: number; + dows: string; + dates: string; + weeks: number; + months: string | number; + years: number; + yearMonth: number; + yearDate: number; + }; + valid: { + from: string; + to?: string; + }; +} + +interface TriggerScheduleBlockState extends GenericBlockState { + openDialog?: boolean; +} + +class TriggerScheduleBlock extends GenericBlock { + private coordinates: { latitude: number; longitude: number } | null = null; + + constructor(props: GenericBlockProps) { super(props, TriggerScheduleBlock.getStaticData()); - this.coordinates = null; } - static compile(config, context) { + static compile(config: RuleBlockConfigTriggerSchedule, context: RuleContext): string { let text = ''; - let func = context.justCheck ? Compile.STANDARD_FUNCTION_STATE : Compile.STANDARD_FUNCTION_STATE_ONCHANGE; + let func = context.justCheck ? STANDARD_FUNCTION_STATE : STANDARD_FUNCTION_STATE_ONCHANGE; func = func.replace('"__%%DEBUG_TRIGGER%%__"', `_sendToFrontEnd(${config._id}, {trigger: true})`); if (config.tagCard === 'interval') { - text = `setInterval(${func}, ${config.interval || 1} * ${config.unit === 's' ? 1000 : (config.unit === 'm' ? 60000 : 3600000)});`; + text = `setInterval(${func}, ${config.interval || 1} * ${config.unit === 's' ? 1000 : config.unit === 'm' ? 60000 : 3600000});`; } else if (config.tagCard === 'cron') { text = `schedule("${config.cron}", ${func});`; } else if (config.tagCard === 'at') { @@ -38,30 +77,30 @@ class TriggerScheduleBlock extends GenericBlock { const _dow = [...config.dow].map(item => parseInt(item, 10)); _dow.sort(); - let intervals = []; + const intervals: string[] = []; let start = _dow[0]; - let i = 1 + let i = 1; for (; i < _dow.length; i++) { if (_dow[i] - _dow[i - 1] > 1) { if (start === _dow[i - 1]) { - intervals.push(start); + intervals.push(start.toString()); } else if (_dow[i - 1] - start === 1) { - intervals.push(start + ',' + _dow[i - 1]); + intervals.push(`${start},${_dow[i - 1]}`); } else { - intervals.push(start + '-' + _dow[i - 1]); + intervals.push(`${start}-${_dow[i - 1]}`); } start = _dow[i]; } else if (i === _dow.length - 1) { if (start === _dow[i - 1] || _dow[i] - start === 1) { - intervals.push(start + ',' + _dow[i]); + intervals.push(`${start},${_dow[i]}`); } else { - intervals.push(start + '-' + _dow[i]); + intervals.push(`${start}-${_dow[i]}`); } } } - dow = intervals.join(',') + dow = intervals.join(','); } text = `schedule("${minutes || '0'} ${hours || '0'} * * ${dow}", ${func});`; } else if (config.tagCard === 'astro') { @@ -73,62 +112,63 @@ class TriggerScheduleBlock extends GenericBlock { return text; } - static _time2String(time) { + static _time2String(time: Date): string { if (!time) { return '--:--'; } return `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`; } - async _setAstro(astro, offset, offsetValue) { + async _setAstro(astro?: string, offset?: boolean, offsetValue?: number): Promise { astro = astro || this.state.settings.astro || 'solarNoon'; offset = offset === undefined ? this.state.settings.offset : offset; offsetValue = offsetValue === undefined ? this.state.settings.offsetValue : offsetValue; - offsetValue = parseInt(offsetValue, 10) || 0; + offsetValue = parseInt(offsetValue as unknown as string, 10) || 0; if (!this.coordinates) { - await this.props.socket.getObject('system.adapter.javascript.0') - .then(({ native: { latitude, longitude } }) => { - if (!latitude && !longitude) { - return this.props.socket.getObject('system.config') - .then(obj => { - if (obj && (obj.common.latitude || obj.common.longitude)) { - this.coordinates = { - latitude: obj.common.latitude, - longitude: obj.common.longitude - } - } else { - this.coordinates = null; - } - }); - } else { - this.coordinates = { - latitude, - longitude - } - } - }); + const jsObject = await this.props.socket.getObject('system.adapter.javascript.0'); + const latitude: string | number | undefined = jsObject?.native?.latitude; + const longitude: string | number | undefined = jsObject?.native?.longitude; + if (!latitude && !longitude) { + const systemConfig = await this.props.socket.getObject('system.config'); + if (systemConfig?.common && (systemConfig.common.latitude || systemConfig.common.longitude)) { + this.coordinates = { + latitude: parseFloat(systemConfig.common.latitude as unknown as string), + longitude: parseFloat(systemConfig.common.longitude as unknown as string), + }; + } else { + this.coordinates = null; + } + } else { + this.coordinates = { + latitude: parseFloat(latitude as unknown as string), + longitude: parseFloat(longitude as unknown as string), + }; + } } - const sunValue = this.coordinates && SunCalc.getTimes(new Date(), this.coordinates.latitude, this.coordinates.longitude); - const options = sunValue ? Object.keys(sunValue).map(name => ({ - value: name, - title: name, - title2: `[${TriggerScheduleBlock._time2String(sunValue[name])}]`, - order: sunValue ? TriggerScheduleBlock._time2String(sunValue[name]) : '??:??', - })) : []; - options.sort((a, b) => a.order > b.order ? 1 : (a.order < b.order ? -1 : 0)); + const sunValue = + this.coordinates && SunCalc.getTimes(new Date(), this.coordinates.latitude, this.coordinates.longitude); + const options = sunValue + ? Object.keys(sunValue).map(name => ({ + value: name, + title: name, + title2: `[${TriggerScheduleBlock._time2String(sunValue[name])}]`, + order: sunValue ? TriggerScheduleBlock._time2String(sunValue[name]) : '??:??', + })) + : []; + options.sort((a, b) => (a.order > b.order ? 1 : a.order < b.order ? -1 : 0)); // calculate time text let time = '--:--'; if (astro && sunValue && sunValue[astro]) { const astroTime = new Date(sunValue[astro]); - offset && astroTime.setMinutes(astroTime.getMinutes() + parseInt(offsetValue, 10)); + offset && astroTime.setMinutes(astroTime.getMinutes() + parseInt(offsetValue as unknown as string, 10)); time = `(at ${TriggerScheduleBlock._time2String(astroTime)})`; // translate } - let inputs; + let inputs: RuleInputAny[]; if (offset) { inputs = [ @@ -156,7 +196,7 @@ class TriggerScheduleBlock extends GenericBlock { nameRender: 'renderNameText', attr: 'textTime', defaultValue: time, - } + }, ]; } else { inputs = [ @@ -174,17 +214,17 @@ class TriggerScheduleBlock extends GenericBlock { }, { nameRender: 'renderNameText', - attr: 'textTime', + attr: 'textTime1', defaultValue: time, - } + }, ]; } this.setState({ inputs }, () => super.onTagChange()); } - async _setInterval(interval) { - interval = parseInt(interval || this.state.settings.interval, 10) || 30; + _setInterval(interval?: string | number): void { + interval = parseInt((interval || this.state.settings.interval) as unknown as string, 10) || 30; let options; if (interval === 1) { options = [ @@ -200,40 +240,44 @@ class TriggerScheduleBlock extends GenericBlock { ]; } - this.setState({ - inputs: [ - { - nameRender: 'renderNumber', - prefix: { - en: 'every', + this.setState( + { + inputs: [ + { + nameRender: 'renderNumber', + /*prefix: { + en: 'every', + },*/ + attr: 'interval', + frontText: 'every', + defaultValue: 30, + className: 'block-input-interval', }, - attr: 'interval', - frontText: 'every', - defaultValue: 30, - className: 'block-input-interval', - }, - { - nameRender: 'renderSelect', - attr: 'unit', - defaultValue: 's', - options, - } - ] - }, () => super.onTagChange()); + { + nameRender: 'renderSelect', + attr: 'unit', + defaultValue: 's', + options, + }, + ], + }, + () => super.onTagChange(), + ); } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(_debugMessage: any): string { return I18n.t('Triggered'); } - onValueChanged(value, attr) { + onValueChanged(value: any, attr: string): void { if (this.state.settings.tagCard === 'astro') { if (attr === 'astro') { - this._setAstro(value); + void this._setAstro(value); } else if (attr === 'offset') { - this._setAstro(undefined, value); + void this._setAstro(undefined, value); } else if (attr === 'offsetValue') { - this._setAstro(undefined, undefined, value); + void this._setAstro(undefined, undefined, value); } } else if (this.state.settings.tagCard === 'interval') { if (attr === 'interval') { @@ -242,100 +286,134 @@ class TriggerScheduleBlock extends GenericBlock { } } - renderCron(input, value, onChange) { + renderCron( + input: RuleInputCron, + value: string, + onChange: (value: string, attr?: string, cb?: () => void) => void, + ): React.JSX.Element | null { const { className } = this.props; let textCron = ''; const { settings } = this.state; const { attr } = input; - return
-
-
- {this.renderText({ - attr: attr, - defaultValue: value - }, !!settings[attr] ? settings[attr] : value, onChange)} + return ( +
+
+
+ {this.renderText( + { + nameRender: 'renderText', + attr, + defaultValue: value, + } as RuleInputText, + (settings as Record)[attr] ? (settings as Record)[attr] : value, + onChange, + )} +
+ this.setState({ openDialog: true })} + />
- this.setState({ openDialog: true })} - /> + {this.state.openDialog ? ( + { + onChange(textCron, attr, () => { + onChange(convertCronToText(textCron, I18n.getLanguage()), 'addText'); + this.setState({ openDialog: false }); + }); + }} + onClose={() => this.setState({ openDialog: false })} + > + )[attr] ? (settings as Record)[attr] : '' + } + onChange={el => (textCron = el)} + language={I18n.getLanguage()} + /> + + ) : null} + {this.renderNameText( + { + nameRender: 'renderNameText', + defaultValue: I18n.t('every hour at 0 minutes'), + attr: 'addText', + signature: true, + doNotTranslate: true, + } as RuleInputNameText, + settings.addText ? settings.addText : I18n.t('every hour at 0 minutes'), + )}
- { - await onChange(textCron, attr); - await onChange(convertCronToText(textCron, I18n.getLanguage()), 'addText'); - this.setState({ openDialog: false }); - }} - onClose={() => this.setState({ openDialog: false })}> - textCron = el} - language={I18n.getLanguage()} - /> - - {this.renderNameText({ - defaultValue: I18n.t('every hour at 0 minutes'), - attr: 'addText', - signature: true, - doNotTranslate: true, - }, !!settings['addText'] ? settings['addText'] : I18n.t('every hour at 0 minutes'), onChange)} -
; + ); } - renderWizard(input, value, onChange) { + renderWizard( + input: RuleInputWizard, + value: string, + onChange: (newData: Record | string) => void, + ): React.JSX.Element { const { className } = this.props; const { attr } = input; let wizardText = ''; - let wizard = null; - - return
-
- onChange(el)} - customValue - /> - this.setState({ openDialog: true })} - /> + let wizard: string | null = null; + + return ( +
+
+ )[`${attr}Text`]} + onChange={el => onChange(el as string)} + customValue + /> + this.setState({ openDialog: true })} + /> +
+ {this.state.openDialog ? ( + + this.setState({ openDialog: false }, () => + onChange({ + [`${attr}Text`]: wizardText, + [attr]: wizard, + }), + ) + } + onClose={() => this.setState({ openDialog: false })} + > + { + wizardText = description || ''; + const wizardObj: ScheduleConfig = JSON.parse(schedule) as ScheduleConfig; + wizardObj.valid = wizardObj.valid || {}; + wizardObj.valid.from = wizardObj.valid.from || Schedule.now2string(); + wizard = JSON.stringify(wizardObj); + }} + /> + + ) : null}
- - this.setState({ openDialog: false }, () => - onChange({ - [`${attr}Text`]: wizardText, - [attr]: wizard, - }))} - onClose={() => this.setState({ openDialog: false })}> - { - wizardText = text; - wizard = typeof val === 'object' ? JSON.parse(JSON.stringify(val)) : JSON.parse(val); - wizard.valid = wizard.valid || {}; - wizard.valid.from = wizard.valid.from || Schedule.now2string(); - wizard = JSON.stringify(wizard); - }} /> - -
; + ); } - onTagChange(tagCard) { + onTagChange(tagCard: RuleTagCardTitle): void { tagCard = tagCard || this.state.settings.tagCard; switch (tagCard) { case 'interval': @@ -343,73 +421,84 @@ class TriggerScheduleBlock extends GenericBlock { break; case 'cron': - this.setState({ - inputs: [ - { - nameRender: 'renderCron', - attr: 'cron', - defaultValue: '0 * * * *', - } - ] - }, () => super.onTagChange()); + this.setState( + { + inputs: [ + { + nameRender: 'renderCron', + attr: 'cron', + defaultValue: '0 * * * *', + }, + ], + }, + () => super.onTagChange(), + ); break; - case 'wizard': - const wizard = JSON.parse(DEFAULT_WIZARD); + case 'wizard': { + const wizard: ScheduleConfig = JSON.parse(DEFAULT_WIZARD); wizard.valid = wizard.valid || {}; wizard.valid.from = wizard.valid.from || Schedule.now2string(); - this.setState({ - inputs: [ - { - nameRender: 'renderWizard', - attr: 'wizard', - defaultValue: JSON.stringify(wizard), - }, - ], - }, () => super.onTagChange(null, () => { - const wizardText = Schedule.state2text(this.state.settings.wizard || wizard); - if (this.state.settings.wizardText !== wizardText) { - const settings = JSON.parse(JSON.stringify(this.state.settings)); - settings.wizardText = wizardText; - this.setState({ settings }); - this.props.onChange(settings); - } - })); + this.setState( + { + inputs: [ + { + nameRender: 'renderWizard', + attr: 'wizard', + defaultValue: JSON.stringify(wizard), + }, + ], + }, + () => + super.onTagChange(null, () => { + const wizardText = Schedule.state2text(this.state.settings.wizard || wizard); + if (this.state.settings.wizard !== wizardText) { + const settings = JSON.parse(JSON.stringify(this.state.settings)); + settings.wizardText = wizardText; + this.setState({ settings }); + this.props.onChange(settings); + } + }), + ); break; + } case 'at': - this.setState({ - inputs: [ - { - nameRender: 'renderTime', - prefix: 'at', - attr: 'at', - defaultValue: '07:30', - }, - { - nameRender: 'renderSelect', - attr: 'dow', - default: '', - multiple: true, - defaultValue: ['_', '1', '2', '3', '4', '5', '6', '0'], - options: [ - { value: '_', title: 'Every day', only: true }, - { value: '1', title: 'Monday', titleShort: 'Mo' }, - { value: '2', title: 'Tuesday', titleShort: 'Tu' }, - { value: '3', title: 'Wednesday', titleShort: 'We' }, - { value: '4', title: 'Thursday', titleShort: 'Th' }, - { value: '5', title: 'Friday', titleShort: 'Fr' }, - { value: '6', title: 'Saturday', titleShort: 'Sa' }, - { value: '0', title: 'Sunday', titleShort: 'Su' }, - ] - } - ] - }, () => super.onTagChange()); + this.setState( + { + inputs: [ + { + nameRender: 'renderTime', + prefix: 'at', + attr: 'at', + defaultValue: '07:30', + }, + { + nameRender: 'renderSelect', + attr: 'dow', + default: '', + multiple: true, + defaultValue: ['_', '1', '2', '3', '4', '5', '6', '0'], + options: [ + { value: '_', title: 'Every day', only: true }, + { value: '1', title: 'Monday', titleShort: 'Mo' }, + { value: '2', title: 'Tuesday', titleShort: 'Tu' }, + { value: '3', title: 'Wednesday', titleShort: 'We' }, + { value: '4', title: 'Thursday', titleShort: 'Th' }, + { value: '5', title: 'Friday', titleShort: 'Fr' }, + { value: '6', title: 'Saturday', titleShort: 'Sa' }, + { value: '0', title: 'Sunday', titleShort: 'Su' }, + ], + }, + ], + }, + () => super.onTagChange(), + ); break; case 'astro': - this._setAstro(); + void this._setAstro(); break; default: @@ -417,7 +506,7 @@ class TriggerScheduleBlock extends GenericBlock { } } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'triggers', name: 'Schedule', @@ -425,10 +514,11 @@ class TriggerScheduleBlock extends GenericBlock { icon: 'AccessTime', tagCardArray: ['cron', 'wizard', 'interval', 'at', 'astro'], title: 'Triggers the rule periodically or on some specific time', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return TriggerScheduleBlock.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerScriptSave.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerScriptSave.tsx index 9ed4f9f2..559d9aa3 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerScriptSave.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerScriptSave.tsx @@ -1,43 +1,54 @@ import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; -import Compile from '../../helpers/Compile'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import { NO_FUNCTION } from '../../helpers/Compile'; +import type { + RuleBlockConfigTriggerScriptSave, + RuleBlockDescription, + RuleContext, + RuleTagCardTitle, +} from '../../types'; -class TriggerScriptSave extends GenericBlock { - constructor(props) { +class TriggerScriptSave extends GenericBlock { + constructor(props: GenericBlockProps) { super(props, TriggerScriptSave.getStaticData()); } - static compile(config /* , context */) { - return Compile.NO_FUNCTION.replace('"__%%DEBUG_TRIGGER%%__"', `_sendToFrontEnd(${config._id}, {trigger: true})`); + static compile(config: RuleBlockConfigTriggerScriptSave, _context: RuleContext): string { + return NO_FUNCTION.replace('"__%%DEBUG_TRIGGER%%__"', `_sendToFrontEnd(${config._id}, {trigger: true})`); } - renderDebug(/* debugMessage */) { + // eslint-disable-next-line class-methods-use-this + renderDebug(/* debugMessage */): string { return I18n.t('Triggered'); } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderNameText', - defaultValue: 'On script save or adapter start', - attr: 'script', - }, - ], - }, () => super.onTagChange()); + onTagChange(_tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderNameText', + defaultValue: 'On script save or adapter start', + attr: 'script', + }, + ], + }, + () => super.onTagChange(), + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'triggers', name: 'Start script', id: 'TriggerScriptSave', icon: 'PlayArrow', title: 'Triggers the on script saving or the javascript instance restart', - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return TriggerScriptSave.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerState.tsx b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerState.tsx index b11c0af4..a0b4b7e2 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerState.tsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerState.tsx @@ -12,18 +12,18 @@ import { FormControlLabel, Checkbox, } from '@mui/material'; +import type { TransitionProps } from '@mui/material/transitions'; -import { - MdCancel as IconCancel, - MdCheck as IconCheck, -} from 'react-icons/md'; +import { MdCancel as IconCancel, MdCheck as IconCheck } from 'react-icons/md'; import { I18n } from '@iobroker/adapter-react-v5'; -import GenericBlock from '../GenericBlock'; -import Compile from '../../helpers/Compile'; +import { GenericBlock, type GenericBlockProps, type GenericBlockState } from '../GenericBlock'; +import { STANDARD_FUNCTION_STATE, STANDARD_FUNCTION_STATE_ONCHANGE } from '../../helpers/Compile'; +import { renderValue } from '../../helpers/utils'; +import type { RuleBlockConfigTriggerState, RuleBlockDescription, RuleContext, RuleTagCardTitle } from '../../types'; -const styles = { +const styles: Record = { valueAck: { color: '#b02323', }, @@ -32,74 +32,116 @@ const styles = { }, }; -const Transition = React.forwardRef((props, ref) => - ); +interface TransitionOwnProps { + children: React.ReactElement; +} + +const Transition: React.FC = React.forwardRef( + (props, ref): React.JSX.Element => ( + + ), +); + +Transition.displayName = 'Transition'; + +interface TriggerStateState extends GenericBlockState { + openSimulate?: boolean; + simulateValue?: string | boolean | number; + simulateAck?: boolean; +} -class TriggerState extends GenericBlock { - constructor(props) { +class TriggerState extends GenericBlock { + private readonly inputRef: React.RefObject; + + constructor(props: GenericBlockProps) { super(props, TriggerState.getStaticData()); this.inputRef = React.createRef(); } - static compile(config, context) { - let func = context.justCheck ? Compile.STANDARD_FUNCTION_STATE : Compile.STANDARD_FUNCTION_STATE_ONCHANGE; - func = func.replace('"__%%DEBUG_TRIGGER%%__"', `_sendToFrontEnd(${config._id}, {val: obj.state.val, ack: obj.state.ack, valOld: obj.oldState && obj.oldState.val, ackOld: obj.oldState && obj.oldState.ack})`); - return `on({id: "${config.oid || ''}", change: "${config.tagCard === 'on update' ? 'any' : 'ne'}"}, ${func});` - } - - static renderValue(val) { - if (val === null) { - return 'null'; - } else if (val === undefined) { - return 'undefined'; - } else if (Array.isArray(val)) { - return val.join(', '); - } else if (typeof val === 'object') { - return JSON.stringify(val); - } else { - return val.toString(); - } + static compile(config: RuleBlockConfigTriggerState, context: RuleContext): string { + let func = context.justCheck ? STANDARD_FUNCTION_STATE : STANDARD_FUNCTION_STATE_ONCHANGE; + func = func.replace( + '"__%%DEBUG_TRIGGER%%__"', + `_sendToFrontEnd(${config._id}, {val: obj.state.val, ack: obj.state.ack, valOld: obj.oldState && obj.oldState.val, ackOld: obj.oldState && obj.oldState.ack})`, + ); + return `on({id: "${config.oid || ''}", change: "${config.tagCard === 'on update' ? 'any' : 'ne'}"}, ${func});`; } - renderDebug(debugMessage) { + // eslint-disable-next-line class-methods-use-this + renderDebug(debugMessage: { data: { val: any; ack: boolean; valOld?: any; ackOld?: boolean } }): React.JSX.Element { if (debugMessage.data.valOld !== undefined) { - return {I18n.t('Triggered')} {TriggerState.renderValue(debugMessage.data.valOld)}{TriggerState.renderValue(debugMessage.data.val)}; + return ( + + {I18n.t('Triggered')}{' '} + + {renderValue(debugMessage.data.valOld)} + {' '} + →{' '} + + {renderValue(debugMessage.data.val)} + + + ); } - return {I18n.t('Triggered')} {TriggerState.renderValue(debugMessage.data.val)}; + return ( + + {I18n.t('Triggered')}{' '} + + {renderValue(debugMessage.data.val)} + + + ); } - onWriteValue() { - this.setState({openSimulate: false}); + onWriteValue(): void { + this.setState({ openSimulate: false }); let simulateValue = this.state.simulateValue; - window.localStorage.setItem(`javascript.app.${this.state.settings.oid}_ack`, this.state.simulateAck); + window.localStorage.setItem( + `javascript.app.${this.state.settings.oid}_ack`, + this.state.simulateAck ? 'true' : 'false', + ); if (this.state.settings.oidType === 'boolean') { simulateValue = simulateValue === true || simulateValue === 'true' || simulateValue === '1'; + window.localStorage.setItem(`javascript.app.${this.state.settings.oid}`, simulateValue ? 'true' : 'false'); } else if (this.state.settings.oidType === 'number') { - simulateValue = parseFloat(simulateValue) || 0; + simulateValue = parseFloat(simulateValue as unknown as string) || 0; + window.localStorage.setItem(`javascript.app.${this.state.settings.oid}`, simulateValue.toString()); + } else { + window.localStorage.setItem(`javascript.app.${this.state.settings.oid}`, simulateValue?.toString() || ''); } - window.localStorage.setItem(`javascript.app.${this.state.settings.oid}`, simulateValue); - this.props.socket.setState(this.state.settings.oid, { val: simulateValue, ack: !!this.state.simulateAck }); + void this.props.socket.setState(this.state.settings.oid, { val: simulateValue, ack: !!this.state.simulateAck }); } - renderWriteState() { - return <> + renderWriteState(): React.JSX.Element[] { + return [ + }} + > + {I18n.t('Simulate')} + , {I18n.t('Trigger with value')} - {this.state.settings.oidType === 'boolean' ? + {this.state.settings.oidType === 'boolean' ? ( e.keyCode === 13 && this.onWriteValue()} - value={!!this.state.simulateValue} - onChange={e => this.setState({ simulateValue: e.target.checked })} - />} + control={ + e.key === 'Enter' && this.onWriteValue()} + value={!!this.state.simulateValue} + onChange={e => this.setState({ simulateValue: e.target.checked })} + /> + } label={I18n.t('Value')} /> - : e.keyCode === 13 && this.onWriteValue()} - value={this.state.simulateValue} + onKeyUp={e => e.key === 'Enter' && this.onWriteValue()} + value={ + !this.state.simulateValue && this.state.simulateValue !== 0 + ? '' + : this.state.simulateValue + } onChange={e => this.setState({ simulateValue: e.target.value })} /> - } -
+ )} +
this.onWriteValue()} - color="primary"> - {I18n.t('Write')} + color="primary" + > + + {I18n.t('Write')} - -
- ; + , + ]; } - onTagChange(tagCard) { - this.setState({ - inputs: [ - { - nameRender: 'renderObjectID', - attr: 'oid', - defaultValue: '', - }, - { - nameRender: 'renderWriteState', - }, - ] - }, () => { - super.onTagChange(); - }); + onTagChange(_tagCard: RuleTagCardTitle): void { + this.setState( + { + inputs: [ + { + nameRender: 'renderObjectID', + attr: 'oid', + defaultValue: '', + }, + { + nameRender: 'renderWriteState', + }, + ], + }, + () => { + super.onTagChange(); + }, + ); } - static getStaticData() { + static getStaticData(): RuleBlockDescription { return { acceptedBy: 'triggers', name: 'State', @@ -184,10 +240,11 @@ class TriggerState extends GenericBlock { icon: 'FlashOn', tagCardArray: ['on change', 'on update'], title: 'Triggers the rule on update or change of some state', // translate - } + }; } - getData() { + // eslint-disable-next-line class-methods-use-this + getData(): RuleBlockDescription { return TriggerState.getStaticData(); } } diff --git a/src-editor/src/Components/RulesEditor/components/CardMenu/CustomDragItem.tsx b/src-editor/src/Components/RulesEditor/components/CardMenu/CustomDragItem.tsx index fd5304d8..4ca8a7bb 100644 --- a/src-editor/src/Components/RulesEditor/components/CardMenu/CustomDragItem.tsx +++ b/src-editor/src/Components/RulesEditor/components/CardMenu/CustomDragItem.tsx @@ -3,51 +3,87 @@ import CardMenu from '.'; import { deepCopy } from '../../helpers/deepCopy'; import DragWrapper from '../DragWrapper'; import { STEPS } from '../../helpers/Tour'; +import type { AdminConnection } from '@iobroker/adapter-react-v5'; +import type { BlockValue, RuleBlockDescription, RuleUserRules } from '../../types'; -const CustomDragItem = props => { - const { allProperties, allProperties: { acceptedBy, id }, setUserRules, userRules, setTourStep, tourStep, isTourOpen, onTouchMove } = props; - return - { - (isTourOpen && - tourStep === STEPS.addScheduleByDoubleClick && - id === 'TriggerScheduleBlock' && - setTourStep(STEPS.openTagsMenu) - ); - (isTourOpen && - tourStep === STEPS.addActionPrintText && - id === 'ActionPrintText' && - setTourStep(STEPS.showJavascript) - ); - let _id = Date.now(); - let blockValue; - switch (acceptedBy) { - case 'actions': - blockValue = 'then'; - break; +interface CustomDragItemProps { + adapter: string | undefined; + allProperties: RuleBlockDescription; + icon: string | undefined; + id: string; + isActive: boolean; + isTourOpen: boolean; + name: string; + onTouchMove: (e: React.TouchEvent) => void; + setTourStep: (step: number) => void; + setUserRules: (value: RuleUserRules) => void; + socket: AdminConnection | null; + tourStep: number; + userRules: RuleUserRules; +} - case 'conditions': - blockValue = userRules[acceptedBy].length - 1; - break; +const CustomDragItem = (props: CustomDragItemProps): React.JSX.Element => { + const { + allProperties, + allProperties: { acceptedBy, id }, + setUserRules, + userRules, + setTourStep, + tourStep, + isTourOpen, + onTouchMove, + isActive, + } = props; - default: - break; - } - let newUserRules = deepCopy(acceptedBy, userRules, blockValue); - const newItem = { id, _id, acceptedBy }; - if (blockValue !== undefined) { - newUserRules[acceptedBy][blockValue].push({ ...newItem }); - } else { - newUserRules[acceptedBy].push({ ...newItem }); - } - setUserRules(newUserRules); - }} - onDoubl - {...props} - {...allProperties} - /> - ; -} + return ( + + { + if (isTourOpen && tourStep === STEPS.addScheduleByDoubleClick && id === 'TriggerScheduleBlock') { + setTourStep(STEPS.openTagsMenu); + } + if (isTourOpen && tourStep === STEPS.addActionPrintText && id === 'ActionPrintText') { + setTourStep(STEPS.showJavascript); + } + const _id = Date.now(); + let blockValue: BlockValue; + switch (acceptedBy) { + case 'actions': + blockValue = 'then'; + break; + + case 'conditions': + blockValue = userRules[acceptedBy].length - 1; + break; + + default: + break; + } + const newUserRules = deepCopy(acceptedBy, userRules, blockValue); + const newItem = { id, _id, acceptedBy }; + if (blockValue !== undefined) { + if (acceptedBy === 'actions') { + newUserRules.actions[blockValue as 'then' | 'else'].push({ ...newItem }); + } else if (acceptedBy === 'conditions') { + newUserRules.conditions[blockValue as number].push({ ...newItem }); + } + } else { + newUserRules.triggers.push({ ...newItem }); + } + setUserRules(newUserRules); + }} + {...props} + {...allProperties} + onTouchMove={onTouchMove} + /> + + ); +}; export default CustomDragItem; diff --git a/src-editor/src/Components/RulesEditor/components/CardMenu/index.tsx b/src-editor/src/Components/RulesEditor/components/CardMenu/index.tsx index 40b0a6d7..e14d80a4 100644 --- a/src-editor/src/Components/RulesEditor/components/CardMenu/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CardMenu/index.tsx @@ -1,44 +1,51 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n, Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import MaterialDynamicIcon from '../../helpers/MaterialDynamicIcon'; -const CardMenu = ({ - name, id, active, icon, adapter, - socket, onDoubleClick, title, - onTouchMove, style, -}) =>
- - - {name ? I18n.t(name) : ''} - -
; - -CardMenu.defaultProps = { - name: '', - active: false, - id: '', - onDoubleClick: () => { }, -}; +interface CardMenuProps { + name: string; + id: string; + active?: boolean; + icon?: string | undefined; + adapter?: string; + socket: AdminConnection | null; + onDoubleClick: () => void; + title?: string; + onTouchMove: (e: React.TouchEvent) => void; + style?: React.CSSProperties; +} -CardMenu.propTypes = { - name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - active: PropTypes.bool -}; +const CardMenu = ({ + name, + id, + active, + icon, + adapter, + socket, + onDoubleClick, + title, + onTouchMove, + style, +}: CardMenuProps): React.JSX.Element => ( +
+ + {name ? I18n.t(name) : ''} +
+); export default CardMenu; diff --git a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogCondition.tsx b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogCondition.tsx index 71b6fa87..84b696a3 100644 --- a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogCondition.tsx +++ b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogCondition.tsx @@ -1,44 +1,38 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; -const DialogCondition = ({ onClose, open }) => - - -

{I18n.t('On condition change')}

-
{I18n.t('help_on_change')}
-

{I18n.t('Just check')}

-
{I18n.t('help_just_check')}
-
-
- - - -
; +interface DialogConditionProps { + onClose: () => void; + open: boolean; +} -DialogCondition.defaultProps = { - open: false, - onClose: () => { } -}; - -DialogCondition.propTypes = { - open: PropTypes.bool, - onClose: PropTypes.func -}; +const DialogCondition = ({ onClose, open }: DialogConditionProps): React.JSX.Element => ( + + + +

{I18n.t('On condition change')}

+
{I18n.t('help_on_change')}
+

{I18n.t('Just check')}

+
{I18n.t('help_just_check')}
+
+
+ + + +
+); export default DialogCondition; diff --git a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogHelp.tsx b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogHelp.tsx index 25deaf7c..8ad8519f 100644 --- a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogHelp.tsx +++ b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/DialogHelp.tsx @@ -1,52 +1,50 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - Dialog, - DialogActions, - DialogContent, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent } from '@mui/material'; import { Check as IconOk } from '@mui/icons-material'; import { I18n } from '@iobroker/adapter-react-v5'; -const DialogHelp = ({ onClose, open }) => - -
-

{I18n.t('On condition change')}

-
{I18n.t('help_on_change')}
-

{I18n.t('Just check')}

-
{I18n.t('help_just_check')}
-
-
- - - -
; +interface DialogHelpProps { + open: boolean; + onClose: () => void; +} -DialogHelp.defaultProps = { - open: false, - onClose: () => { } -}; - -DialogHelp.propTypes = { - open: PropTypes.bool, - onClose: PropTypes.func -}; +const DialogHelp = ({ onClose, open }: DialogHelpProps): React.JSX.Element => ( + + +
+

{I18n.t('On condition change')}

+
{I18n.t('help_on_change')}
+

{I18n.t('Just check')}

+
{I18n.t('help_just_check')}
+
+
+ + + +
+); export default DialogHelp; diff --git a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/index.tsx b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/index.tsx index c25bfd19..7fbec17e 100644 --- a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/index.tsx @@ -1,15 +1,17 @@ import React, { Fragment, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { useDrop } from 'react-dnd'; +import { type ConnectDropTarget, type XYCoord, useDrop } from 'react-dnd'; -import { - Select, - MenuItem, - IconButton, -} from '@mui/material'; +import { Select, MenuItem, IconButton } from '@mui/material'; import { HelpOutline as IconHelp } from '@mui/icons-material'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; +import { + type AdminConnection, + I18n, + type IobTheme, + type ThemeName, + type ThemeType, + Utils, +} from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import { deepCopy } from '../../helpers/deepCopy'; @@ -19,26 +21,57 @@ import DragWrapper from '../DragWrapper'; import MaterialDynamicIcon from '../../helpers/MaterialDynamicIcon'; import DialogHelp from './DialogHelp'; import DialogCondition from './DialogCondition'; +import type { BlockValue, RuleBlockDescription, RuleBlockType, RuleUserRules, RuleBlockConfig } from '../../types'; + +interface AdditionallyContentBlockItemsProps { + size: boolean; + blockValue: BlockValue; + boolean?: boolean; + typeBlock: RuleBlockType; + userRules: RuleUserRules; + setUserRules: (newRules: RuleUserRules) => void; + animation?: boolean; + setTourStep?: (step: number) => void; + tourStep?: number; + isTourOpen?: boolean; + theme: IobTheme; + themeType: ThemeType; + themeName: ThemeName; +} const AdditionallyContentBlockItems = ({ - size, itemsSwitchesRender, blockValue, boolean, typeBlock, - userRules, setUserRules, animation, setTourStep, tourStep, isTourOpen, - theme, themeType, themeName, -}) => { + size, + blockValue, + boolean, + typeBlock, + userRules, + setUserRules, + animation, + setTourStep, + tourStep, + isTourOpen, + theme, + themeType, + themeName, +}: AdditionallyContentBlockItemsProps): React.JSX.Element => { const [checkItem, setCheckItem] = useState(false); const [canDropCheck, setCanDropCheck] = useState(false); const [checkId, setCheckId] = useState(false); const [hoverBlock, setHoverBlock] = useState(''); - const options = useDrop({ + if (boolean === undefined) { + boolean = true; + } + + const options: [unknown, ConnectDropTarget] = useDrop({ accept: 'box', drop: () => ({ blockValue }), hover: ({ acceptedBy, _id }, monitor) => { setCheckItem(acceptedBy === typeBlock); setCheckId(!!_id); - setHoverBlock(monitor.getHandlerId()); + setHoverBlock((monitor.getHandlerId() as string) || ''); }, - canDrop: ({ acceptedBy }, monitor) => { + canDrop: ({ acceptedBy }) => { setCanDropCheck(acceptedBy === typeBlock); return acceptedBy === typeBlock; }, @@ -46,13 +79,28 @@ const AdditionallyContentBlockItems = ({ isOver: monitor.isOver(), canDrop: monitor.getItem()?.acceptedBy === typeBlock, offset: monitor.getClientOffset(), - targetId: monitor.targetId + targetId: monitor.getHandlerId(), }), }); - const [{ canDrop, isOver, offset, targetId }, drop] = options; + const pr: { + canDrop: boolean; + isOver: boolean; + offset: XYCoord | null; + targetId: string; + } = options[0] as { + canDrop: boolean; + isOver: boolean; + offset: XYCoord | null; + targetId: string; + }; - useEffect(() => { setHoverBlock('') }, [offset]); + const { canDrop, isOver, offset, targetId } = pr; + const drop = options[1]; + + useEffect(() => { + setHoverBlock(''); + }, [offset]); const isActive = canDrop && isOver; let backgroundColor = ''; @@ -64,62 +112,115 @@ const AdditionallyContentBlockItems = ({ backgroundColor = targetId === hoverBlock ? '#fb00002e' : ''; } - return
-
- {itemsSwitchesRender[blockValue]?.map(el => - +
+ {blocks.map((el: RuleBlockConfig) => ( + + + + ))} +
- )} -
+
-
; -} - -AdditionallyContentBlockItems.defaultProps = { - children: null, - boolean: true, - animation: false + ); }; +interface ContentBlockItemsProps { + size: boolean; + typeBlock: RuleBlockType; + name: string | React.JSX.Element; + nameAdditionally?: string; + additionally?: boolean; + border?: boolean; + userRules: RuleUserRules; + setUserRules: (newRules: RuleUserRules) => void; + iconName: string; + adapter?: string; + socket: AdminConnection; + setTourStep: (step: number) => void; + tourStep: number; + isTourOpen: boolean; + theme: IobTheme; + themeType: ThemeType; + themeName: ThemeName; +} + const ContentBlockItems = ({ - size, typeBlock, name, nameAdditionally, additionally, - border, userRules, setUserRules, iconName, adapter, - socket, setTourStep, tourStep, isTourOpen, theme, themeType, themeName, -}) => { - const [additionallyClickItems, setAdditionallyClickItems, checkLocal] = useStateLocal(typeBlock === 'actions' ? false : [], `additionallyClickItems_${typeBlock}`); + size, + typeBlock, + name, + nameAdditionally, + additionally, + border, + userRules, + setUserRules, + iconName, + adapter, + socket, + setTourStep, + tourStep, + isTourOpen, + theme, + themeType, + themeName, +}: ContentBlockItemsProps): React.JSX.Element => { + const [additionallyClickItems, setAdditionallyClickItems, checkLocal] = useStateLocal< + { _id: number; open: boolean }[] | boolean + >(typeBlock === 'actions' ? false : [], `additionallyClickItems_${typeBlock}`); + const [showHelp, setShowHelp] = useState(false); const [showConditionDialog, setShowConditionDialog] = useState(false); useEffect(() => { - if (typeBlock === 'conditions' && additionallyClickItems.length !== userRules['conditions'].length - 1) { - let newArray = []; - userRules['conditions'].forEach((el, idx) => { + if ( + typeBlock === 'conditions' && + (additionallyClickItems as { _id: number; open: boolean }[])?.length !== userRules.conditions.length - 1 + ) { + const newArray: { _id: number; open: boolean }[] = []; + userRules.conditions.forEach((el, idx) => { if (idx > 0) { newArray.push({ _id: Date.now(), @@ -127,145 +228,169 @@ const ContentBlockItems = ({ }); } }); - setAdditionallyClickItems([...additionallyClickItems, ...newArray]); + setAdditionallyClickItems([...(additionallyClickItems as { _id: number; open: boolean }[]), ...newArray]); } - if (typeBlock === 'actions' && !checkLocal && userRules['actions']['else'].length) { + if (typeBlock === 'actions' && !checkLocal && userRules.actions.else.length) { setAdditionallyClickItems(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [animation, setAnimation] = useState(false); + const [animation, setAnimation] = useState(false); - return
- - + + + {name} + + {typeBlock === 'conditions' ? ( +
+ + setShowHelp(true)} + > + + +
+ ) : null} + - {name} -
- {typeBlock === 'conditions' ? -
- - setShowHelp(true)}> - - -
- : null} - - {additionally && [...Array(typeBlock === 'actions' ? 1 : userRules.conditions.length - 1)].map((e, index) => { - const booleanAdditionally = (value = index) => Boolean(typeBlock === 'actions' ? additionallyClickItems : additionallyClickItems.find((el, idx) => idx === value && el.open)); - return + {additionally && + [...Array(typeBlock === 'actions' ? 1 : userRules.conditions.length - 1)].map((e, index) => { + const booleanAdditionally = (value = index): boolean => + typeBlock === 'actions' + ? !!additionallyClickItems + : !!(additionallyClickItems as { _id: number; open: boolean }[]).find( + (el, idx) => idx === value && el.open, + ); + + return ( + +
{ + if (typeBlock === 'actions') { + setAdditionallyClickItems(!additionallyClickItems); + return null; + } + let newAdditionally: { _id: number; open: boolean }[] = JSON.parse( + JSON.stringify(additionallyClickItems), + ); + if (userRules.conditions[index + 1].length) { + newAdditionally[index].open = !newAdditionally[index].open; + setAdditionallyClickItems(newAdditionally); + return null; + } + + newAdditionally = newAdditionally.filter((_el, idx) => idx !== index); + + setAdditionallyClickItems(newAdditionally); + + setAnimation(index); + + setTimeout(() => { + setAnimation(false); + setUserRules({ + ...userRules, + conditions: [ + ...userRules.conditions.filter((el, idx) => idx !== index + 1), + ], + }); + }, 250); + }} + key={index} + className={cls.blockCardAdd} + > + {booleanAdditionally() ? '-' : '+'} +
{nameAdditionally}
+
+ +
+ ); + })} + {additionally && typeBlock === 'conditions' && (
{ - if (typeBlock === 'actions') { - setAdditionallyClickItems(!additionallyClickItems); - return null; - } - let newAdditionally = JSON.parse(JSON.stringify(additionallyClickItems)); - if (userRules.conditions[index + 1].length) { - newAdditionally[index].open = !newAdditionally[index].open - setAdditionallyClickItems(newAdditionally); - return null; - } - newAdditionally = newAdditionally.filter((el, idx) => idx !== index); - setAdditionallyClickItems(newAdditionally); - setAnimation(typeBlock === 'actions' ? true : index); - setTimeout(() => { - setAnimation(false); - setUserRules({ ...userRules, conditions: [...userRules.conditions.filter((el, idx) => idx !== index + 1)] }); - }, 250); - + setAdditionallyClickItems([ + ...(additionallyClickItems as { _id: number; open: boolean }[]), + { + _id: Date.now(), + open: true, + }, + ]); + setUserRules({ ...userRules, conditions: [...userRules.conditions, []] }); + setAnimation(userRules.conditions.length - 1); + setTimeout(() => setAnimation(false), 1000); }} - key={index} className={cls.blockCardAdd}> - {booleanAdditionally() ? '-' : '+'}
- {nameAdditionally} -
+ className={cls.blockCardAdd} + > + {'+'} +
{nameAdditionally}
- -
- })} - {additionally && typeBlock === 'conditions' &&
{ - setAdditionallyClickItems([...additionallyClickItems, { - _id: Date.now(), - open: true, - }]); - setUserRules({ ...userRules, conditions: [...userRules.conditions, []] }); - setAnimation(typeBlock === 'actions' ? true : userRules.conditions.length - 1); - setTimeout(() => setAnimation(false), 1000); - }} - className={cls.blockCardAdd} - > - {'+'} -
- {nameAdditionally} -
-
} - setShowHelp(false)} /> - setShowConditionDialog(false)} /> -
; -} - -ContentBlockItems.defaultProps = { - children: null, - name: '', - nameAdditionally: '', - additionally: false, - border: false, - typeBlock: '', -}; - -ContentBlockItems.propTypes = { - name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - nameAdditionally: PropTypes.string, - border: PropTypes.bool, - additionally: PropTypes.bool, - children: PropTypes.object, - typeBlock: PropTypes.string, - blockValue: PropTypes.string, - userRules: PropTypes.object, - setUserRules: PropTypes.func, + )} + setShowHelp(false)} + /> + setShowConditionDialog(false)} + /> +
+ ); }; export default ContentBlockItems; diff --git a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/style.module.scss b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/style.module.scss index 54a682bd..9b975c4b 100644 --- a/src-editor/src/Components/RulesEditor/components/ContentBlockItems/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/ContentBlockItems/style.module.scss @@ -73,7 +73,9 @@ display: flex; flex-direction: column; overflow: auto; - transition: height 0.3s, background 0.5s; + transition: + height 0.3s, + background 0.5s; } .wrapperMargin { @@ -106,7 +108,7 @@ color: var(--lineColor); &::after, &::before { - content: ""; + content: ''; flex: 1; border-bottom: 1px solid; } diff --git a/src-editor/src/Components/RulesEditor/components/ContextWrapper/index.tsx b/src-editor/src/Components/RulesEditor/components/ContextWrapper/index.tsx index 1331fed5..04906d1a 100644 --- a/src-editor/src/Components/RulesEditor/components/ContextWrapper/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/ContextWrapper/index.tsx @@ -1,10 +1,6 @@ -import React, { - createContext, - useEffect, - useState, -} from 'react'; +import React, { createContext, useEffect, useState } from 'react'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5'; import ActionSayText from '../Blocks/ActionSayText'; import ActionSendEmail from '../Blocks/ActionSendEmail'; @@ -13,8 +9,10 @@ import ActionPushover from '../Blocks/ActionPushover'; import ActionWhatsappcmb from '../Blocks/ActionWhatsappcmb'; import ActionPushsafer from '../Blocks/ActionPushsafer'; import StandardBlocks from '../StandardBlocks'; +import type { GenericBlock } from '../GenericBlock'; +import type { DebugMessage } from '../../types'; -const ADAPTERS = { +const ADAPTERS: Record | null> = { telegram: ActionTelegram, email: ActionSendEmail, sayit: ActionSayText, @@ -23,36 +21,67 @@ const ADAPTERS = { pushsafer: ActionPushsafer, }; -export const ContextWrapperCreate = createContext(); +interface RuleContext { + blocks: (typeof GenericBlock)[] | null; + socket: AdminConnection | null; + onUpdate: boolean; + setOnUpdate: (value: boolean) => void; + onDebugMessage: DebugMessage[]; + setOnDebugMessage: (message: DebugMessage[]) => void; + enableSimulation: boolean; + setEnableSimulation: (enableSimulation: boolean) => void; +} -const getOrLoadRemote = (remote, shareScope, remoteFallbackUrl = undefined) => +export const ContextWrapperCreate = createContext({ + blocks: null, + socket: null, + + onUpdate: false, + setOnUpdate: (_onUpdate: boolean): void => {}, + + setOnDebugMessage: (_message: DebugMessage[]): void => {}, + onDebugMessage: [], + + enableSimulation: false, + setEnableSimulation: (_enableSimulation: boolean): void => {}, +}); + +const getOrLoadRemote = ( + remote: string, + shareScope: string, + remoteFallbackUrl: string | undefined = undefined, +): Promise<{ get: (module: string) => () => Promise<{ default: typeof GenericBlock }> }> => new Promise((resolve, reject) => { - // check if remote exists on window - if (!window[remote]) { + // check if remote exists on window + if (!(window as any)[remote]) { // search dom to see if remote tag exists, but might still be loading (async) - const existingRemote = document.querySelector(`[data-webpack="${remote}"]`); + const existingRemote: HTMLScriptElement | null = document.querySelector(`[data-webpack="${remote}"]`); + // when remote is loaded... - const onload = async () => { + const onload = async (): Promise => { // check if it was initialized - if (!window[remote]) { - return reject(`Cannot load Remote "${remote}" to inject`); + if (!(window as any)[remote]) { + reject(new Error(`Cannot load Remote "${remote}" to inject`)); + return; } - if (!window[remote].__initialized) { + if (!(window as any)[remote].__initialized) { // if share scope doesn't exist (like in webpack 4) then expect shareScope to be a manual object + // @ts-expect-error it is a trick and must be so if (typeof __webpack_share_scopes__ === 'undefined') { // use default share scope object passed in manually - await window[remote].init(shareScope.default); + await (window as any)[remote].init(shareScope); } else { // otherwise, init share scope as usual - // eslint-disable-next-line - await window[remote].init(__webpack_share_scopes__[shareScope]); + // @ts-expect-error it is a trick and must be so + await (window as any)[remote].init(__webpack_share_scopes__[shareScope]); } // mark remote as initialized - window[remote].__initialized = true; + (window as any)[remote].__initialized = true; } // resolve promise so marking remote as loaded - resolve(); + resolve((window as any)[remote]); }; + if (existingRemote) { // if existing remote but not loaded, hook into its onload and wait for it to be ready existingRemote.onload = onload; @@ -73,26 +102,32 @@ const getOrLoadRemote = (remote, shareScope, remoteFallbackUrl = undefined) => d.getElementsByTagName('head')[0].appendChild(script); } else { // no remote and no fallback exist, reject - reject(`Cannot Find Remote ${remote} to inject`); + reject(new Error(`Cannot Find Remote ${remote} to inject`)); } } else { // remote already instantiated, resolve - resolve(); + resolve((window as any)[remote]); } }); -const loadComponent = (remote, sharedScope, module, url) => async () => { - await getOrLoadRemote(remote, sharedScope, url); - const container = window[remote]; - const factory = await container.get(module); - const Module = factory(); - return Module; -}; +function loadComponent( + remote: string, + sharedScope: string, + module: string, + url: string, +): () => Promise<{ default: typeof GenericBlock }> { + return async (): Promise<{ default: typeof GenericBlock }> => { + await getOrLoadRemote(remote, sharedScope, url); + const container = (window as any)[remote]; + const factory = await container.get(module); + return factory(); + }; +} -export const ContextWrapper = ({ children, socket }) => { - const [blocks, setBlocks] = useState(null); +export const ContextWrapper = ({ children, socket }: { socket: AdminConnection; children: any }): React.JSX.Element => { + const [blocks, setBlocks] = useState<(typeof GenericBlock)[] | null>(null); const [onUpdate, setOnUpdate] = useState(false); - const [onDebugMessage, setOnDebugMessage] = useState(false); + const [onDebugMessage, setOnDebugMessage] = useState([]); const [enableSimulation, setEnableSimulation] = useState(false); useEffect(() => { @@ -100,36 +135,48 @@ export const ContextWrapper = ({ children, socket }) => { }, [onUpdate]); useEffect(() => { - (async () => { + void (async () => { const instances = await socket.getAdapterInstances(); const adapters = Object.keys(ADAPTERS).filter(adapter => - instances.find(obj => obj?.common?.name === adapter)); + instances.find(obj => obj?.common?.name === adapter), + ); - const adapterDynamicBlocksArray = []; + const adapterDynamicBlocksArray: (typeof GenericBlock)[] = []; // find all adapters, that have custom rule blocks + // @ts-expect-error javascriptRules in js-controller const dynamicRules = instances.filter(obj => obj.common.javascriptRules); - const alreadyCreated = []; - for (let k in dynamicRules) { + const alreadyCreated: string[] = []; + for (const k in dynamicRules) { const obj = dynamicRules[k]; if (alreadyCreated.includes(obj.common.name)) { continue; } let url; - if (obj.common.javascriptRules.url.startsWith('http:') || obj.common.javascriptRules.url.startsWith('https:')) { + if ( + // @ts-expect-error javascriptRules in js-controller + obj.common.javascriptRules.url.startsWith('http:') || + // @ts-expect-error javascriptRules in js-controller + obj.common.javascriptRules.url.startsWith('https:') + ) { + // @ts-expect-error javascriptRules in js-controller url = obj.common.javascriptRules.url; + // @ts-expect-error javascriptRules in js-controller } else if (obj.common.javascriptRules.url.startsWith('./')) { + // @ts-expect-error javascriptRules in js-controller url = `${window.location.protocol}//${window.location.host}${obj.common.javascriptRules.url.replace(/^\./, '')}`; } else { + // @ts-expect-error javascriptRules in js-controller url = `${window.location.protocol}//${window.location.host}/adapter/${obj.common.name}/${obj.common.javascriptRules.url}`; } + // @ts-expect-error javascriptRules in js-controller if (obj.common.javascriptRules.i18n === true) { // load i18n from files - const pos = url.lastIndexOf('/'); - let i18nURL; + const pos: number = url.lastIndexOf('/'); + let i18nURL: string; if (pos !== -1) { i18nURL = url.substring(0, pos); } else { @@ -147,21 +194,32 @@ export const ContextWrapper = ({ children, socket }) => { return fetch(`${i18nURL}/i18n/en.json`) .then(data => data.json()) .then(json => I18n.extendTranslations(json, lang)) - .catch(error => console.error(`Cannot load i18n "${file}": ${error}`)) - } else { - console.log(`Cannot load i18n "${file}": ${error}`) + .catch(error => console.error(`Cannot load i18n "${file}": ${error}`)); } + console.log(`Cannot load i18n "${file}": ${error}`); }); + // @ts-expect-error javascriptRules in js-controller } else if (obj.common.javascriptRules.i18n && typeof obj.common.javascriptRules.i18n === 'object') { try { + // @ts-expect-error javascriptRules in js-controller I18n.extendTranslations(obj.common.javascriptRules.i18n); } catch (error) { + // @ts-expect-error javascriptRules in js-controller console.error(`Cannot import i18n for "${obj.common.javascriptRules.name}": ${error}`); } } try { - const Component = (await loadComponent(obj.common.javascriptRules.name, 'default', `./${obj.common.javascriptRules.name}`, url)()).default; + const Component = ( + await loadComponent( + // @ts-expect-error javascriptRules in js-controller + obj.common.javascriptRules.name, + 'default', + // @ts-expect-error javascriptRules in js-controller + `./${obj.common.javascriptRules.name}`, + url, + )() + ).default; if (Component) { adapterDynamicBlocksArray.push(Component); @@ -169,27 +227,34 @@ export const ContextWrapper = ({ children, socket }) => { ADAPTERS[obj.common.name] = null; } } catch (e) { + // @ts-expect-error javascriptRules in js-controller console.error(`Cannot load component "${obj.common.javascriptRules.name}": ${e}`); } } - const adapterBlocksArray = adapters.filter(adapter => ADAPTERS[adapter]).map(adapter => ADAPTERS[adapter]); + const adapterBlocksArray: (typeof GenericBlock)[] = adapters + .filter(adapter => ADAPTERS[adapter]) + .map(adapter => ADAPTERS[adapter]) as (typeof GenericBlock)[]; setBlocks([...StandardBlocks, ...adapterBlocksArray, ...adapterDynamicBlocksArray]); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return - {children} - ; -}; \ No newline at end of file + return ( + + {children} + + ); +}; diff --git a/src-editor/src/Components/RulesEditor/components/CurrentItem/index.tsx b/src-editor/src/Components/RulesEditor/components/CurrentItem/index.tsx index 69233d40..51687cda 100644 --- a/src-editor/src/Components/RulesEditor/components/CurrentItem/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CurrentItem/index.tsx @@ -1,114 +1,161 @@ -import React, { - memo, useCallback, useContext, - useEffect, useMemo, useState, -} from 'react'; -import PropTypes from 'prop-types'; +import React, { memo, useCallback, useContext, useMemo, useState } from 'react'; +import type { AdminConnection, IobTheme, ThemeName, ThemeType } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import { deepCopy } from '../../helpers/deepCopy'; import { filterElement } from '../../helpers/filterElement'; import { ContextWrapperCreate } from '../ContextWrapper'; import { findElement } from '../../helpers/findElement'; -import GenericBlock from '../GenericBlock'; +import { GenericBlock, type GenericBlockProps } from '../GenericBlock'; +import type { BlockValue, RuleBlockConfig, RuleBlockType, RuleUserRules } from '../../types'; -// @iobroker/javascript-block - -const CurrentItem = memo(props => { - const { setUserRules, userRules, _id, id, blockValue, active, acceptedBy, isTourOpen, setTourStep, tourStep } = props; - const [anchorEl, setAnchorEl] = useState(null); - const { blocks, socket, onUpdate, setOnUpdate, onDebugMessage, enableSimulation } = useContext(ContextWrapperCreate); +interface CurrentItemProps { + setUserRules: (newRules: RuleUserRules) => void; + userRules: RuleUserRules; + _id: number; + id: string; + blockValue: BlockValue; + active?: boolean; + acceptedBy: RuleBlockType; + isTourOpen?: boolean; + setTourStep?: (step: number) => void; + tourStep?: number; + theme: IobTheme; + themeType: ThemeType; + themeName: ThemeName; + settings?: RuleBlockConfig; +} - useEffect(() => { - console.log(`New message !! ${JSON.stringify(onDebugMessage)}`); - }, [onDebugMessage]); +// @iobroker/javascript-block +// eslint-disable-next-line react/display-name +const CurrentItem = memo((props: CurrentItemProps) => { + const { setUserRules, userRules, _id, id, blockValue, active, acceptedBy, isTourOpen, setTourStep, tourStep } = + props; + const [anchorEl, setAnchorEl] = useState(null); + const { blocks, socket, onUpdate, setOnUpdate, onDebugMessage, enableSimulation } = + useContext(ContextWrapperCreate); - const findElementBlocks = useCallback(id => blocks.find(el => { - const staticData = el.getStaticData(); - return staticData.id === id; - }), [blocks]); + const findElementBlocks = useCallback( + (id: string) => + blocks?.find(el => { + const staticData = el.getStaticData(); + return staticData.id === id; + }), + [blocks], + ); - const onChange = useCallback(settings => { - let newUserRules = findElement(settings, userRules, blockValue); - newUserRules && setUserRules(newUserRules); + const onChange = useCallback( + (settings: RuleBlockConfig): void => { + const newUserRules = findElement(settings, userRules, blockValue); + newUserRules && setUserRules(newUserRules); + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, [userRules]); + [userRules], + ); - const handlePopoverOpen = event => - event.currentTarget !== anchorEl && setAnchorEl(event.currentTarget); + const handlePopoverOpen = (event: React.MouseEvent): void => { + if (event.currentTarget !== anchorEl) { + setAnchorEl(event.currentTarget); + } + }; - const handlePopoverClose = () => - setAnchorEl(null); + const handlePopoverClose = (): void => setAnchorEl(null); const blockInput = useMemo(() => { - const CustomBlock = findElementBlocks(id) || GenericBlock; - return ; + const CustomBlock: React.FC> = (findElementBlocks(id) || + GenericBlock) as unknown as React.FC>; + + return ( + + ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [userRules, onUpdate, onDebugMessage, enableSimulation]); const [isDelete, setIsDelete] = useState(false); - return
{ - if (el.ctrlKey) { - let newItem; - let newUserRules = deepCopy(acceptedBy, userRules, blockValue); - if (blockValue !== "triggers") { - newItem = newUserRules[acceptedBy][blockValue].find(el => el._id === _id); - } else { - newItem = newUserRules[acceptedBy].find(el => el._id === _id); - } - if (blockValue !== "triggers") { - newUserRules[acceptedBy][blockValue].splice(newUserRules[acceptedBy][blockValue].indexOf(newItem), 0, { ...newItem, _id: Date.now() }); - } else { - newUserRules[acceptedBy].splice(newUserRules[acceptedBy].indexOf(newItem), 0, { ...newItem, _id: Date.now() }); - } - setUserRules(newUserRules); - } - }} - id="height" - style={active ? { width: document.getElementById('width').clientWidth - 70 } : null} - className={`${cls.cardStyle} ${active ? cls.cardStyleActive : null} ${isDelete ? cls.isDelete : null}`}> -
- {blockInput} - {setUserRules &&
-
{ - let newItemsSwitches = deepCopy(acceptedBy, userRules, blockValue); - newItemsSwitches = filterElement(acceptedBy, newItemsSwitches, blockValue, _id); - setIsDelete(true); - setTimeout(() => { - if (acceptedBy === 'triggers') { - setOnUpdate(true); + return ( +
{ + if (el.ctrlKey) { + let newItem: RuleBlockConfig | undefined; + const newUserRules = deepCopy(acceptedBy, userRules, blockValue); + if (acceptedBy === 'conditions') { + newItem = newUserRules.conditions[blockValue as number].find(el => el._id === _id); + if (newItem) { + newUserRules.conditions[blockValue as number].splice( + newUserRules.conditions[blockValue as number].indexOf(newItem), + 0, + { ...newItem, _id: Date.now() }, + ); + } + } else if (acceptedBy === 'actions') { + newItem = newUserRules.actions[blockValue as 'then' | 'else'].find(el => el._id === _id); + if (newItem) { + newUserRules.actions[blockValue as 'then' | 'else'].splice( + newUserRules.actions[blockValue as 'then' | 'else'].indexOf(newItem), + 0, + { ...newItem, _id: Date.now() }, + ); + } + } else { + newItem = newUserRules.triggers.find(el => el._id === _id); + if (newItem) { + newUserRules.triggers.splice(newUserRules[acceptedBy].indexOf(newItem), 0, { + ...newItem, + _id: Date.now(), + }); + } } - setUserRules(newItemsSwitches); - }, 300); - }} className={cls.closeBtn} /> -
} -
; -}); - -CurrentItem.defaultProps = { - active: false -}; -CurrentItem.propTypes = { - name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) -}; + setUserRules(newUserRules); + } + }} + id="height" + style={active ? { width: (document.getElementById('width')?.clientWidth || 0) - 70 } : undefined} + className={`${cls.cardStyle} ${active ? cls.cardStyleActive : null} ${isDelete ? cls.isDelete : null}`} + > +
+ {blockInput} + {setUserRules && ( +
+
{ + let newItemsSwitches = deepCopy(acceptedBy, userRules, blockValue); + newItemsSwitches = filterElement(acceptedBy, newItemsSwitches, blockValue, _id); + setIsDelete(true); + setTimeout(() => { + if (acceptedBy === 'triggers') { + setOnUpdate(true); + } + setUserRules(newItemsSwitches); + }, 300); + }} + className={cls.closeBtn} + /> +
+ )} +
+ ); +}); export default CurrentItem; diff --git a/src-editor/src/Components/RulesEditor/components/CurrentItem/style.module.scss b/src-editor/src/Components/RulesEditor/components/CurrentItem/style.module.scss index 3b3aadd3..7dc5ea84 100644 --- a/src-editor/src/Components/RulesEditor/components/CurrentItem/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CurrentItem/style.module.scss @@ -11,7 +11,10 @@ align-items: center; background: #ffffff6b; border-radius: 4px; - box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12); + box-shadow: + 0 2px 1px -1px rgba(0, 0, 0, 0.2), + 0 1px 1px 0 rgba(0, 0, 0, 0.14), + 0 1px 3px 0 rgba(0, 0, 0, 0.12); } .cardStyleActive { width: 300px; @@ -34,7 +37,7 @@ margin: 5px auto; cursor: pointer; &:before { - content: "+"; + content: '+'; color: #f7060684; position: absolute; z-index: 2; @@ -46,7 +49,7 @@ transition: all 0.3s cubic-bezier(0.77, 0, 0.2, 0.85); } &:after { - content: ""; + content: ''; position: absolute; top: 0; left: 0; diff --git a/src-editor/src/Components/RulesEditor/components/CustomButton/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomButton/index.tsx index ea2c3d4b..7423964a 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomButton/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomButton/index.tsx @@ -1,38 +1,51 @@ import { Button } from '@mui/material'; import React from 'react'; -import PropTypes from 'prop-types'; import { Utils, Icon as CustomIcon } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const CustomButton = ({ fullWidth, size, onClick, style, className, value, square, icon }) => { - return ; +interface CustomButtonProps { + fullWidth?: boolean; + size?: 'small' | 'medium' | 'large'; + onClick?: () => void; + style?: React.CSSProperties; + className?: string; + value: string; + square?: boolean; + icon?: string; } -CustomButton.defaultProps = { - value: '', - className: null, - variant: 'standard', - size: 'medium', - fullWidth: false, - square: false +const CustomButton = ({ + fullWidth, + size, + onClick, + style, + className, + value, + square, + icon, +}: CustomButtonProps): React.JSX.Element => { + return ( + + ); }; -CustomButton.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - type: PropTypes.string, - style: PropTypes.object, -}; - -export default CustomButton; \ No newline at end of file +export default CustomButton; diff --git a/src-editor/src/Components/RulesEditor/components/CustomButton/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomButton/style.module.scss index 04c0ca8a..10b029c0 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomButton/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomButton/style.module.scss @@ -7,11 +7,11 @@ background-color: inherit !important; } } -.square{ +.square { min-width: auto !important; padding: 6px 16px !important; } .icon { width: 24px; height: 24px; -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/components/CustomCheckbox/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomCheckbox/index.tsx index 62b1e8f9..0c8d9453 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomCheckbox/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomCheckbox/index.tsx @@ -1,5 +1,4 @@ import React, { memo, useState } from 'react'; -import PropTypes from 'prop-types'; import { Checkbox } from '@mui/material'; @@ -7,53 +6,42 @@ import { Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const CustomCheckbox = ({ size, value, style, title, onChange, className, customValue, disabled }) => { - const [switchChecked, setSwitchChecked] = useState(false); - - return <> - { - customValue && setSwitchChecked(e.target.checked); - onChange(e.target.checked); - }} - size={size} - /> - {title || null} - ; +interface CustomCheckboxProps { + title?: string; + size?: 'small' | 'medium'; + value: boolean; + onChange?: (value: boolean) => void; + className?: string; + customValue?: boolean; + disabled?: boolean; } -CustomCheckbox.defaultProps = { - value: false, - disabled: false, - type: null, - error: '', - className: null, - table: false, - native: {}, - variant: 'standard', - size: 'medium', - component: null, - styleComponentBlock: null, - onChange: () => { }, - fullWidth: false, - autoComplete: '', - customValue: false, - label: 'all' -}; +const CustomCheckbox = ({ + size, + value, + title, + onChange, + className, + customValue, + disabled, +}: CustomCheckboxProps): React.JSX.Element => { + const [switchChecked, setSwitchChecked] = useState(false); -CustomCheckbox.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - type: PropTypes.string, - style: PropTypes.object, - native: PropTypes.object, - onChange: PropTypes.func, - component: PropTypes.object, - styleComponentBlock: PropTypes.object + return ( + <> + { + customValue && setSwitchChecked(e.target.checked); + onChange && onChange(e.target.checked); + }} + size={size || 'medium'} + /> + {title || null} + + ); }; export default memo(CustomCheckbox); diff --git a/src-editor/src/Components/RulesEditor/components/CustomDate/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomDate/index.tsx index 58b472fc..4ada8068 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomDate/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomDate/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { FormControl, MenuItem, Select } from '@mui/material'; @@ -7,7 +6,7 @@ import { I18n, Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const DAYS = [ +const DAYS: number[] = [ 31, // 1 29, // 2 31, // 3 @@ -19,13 +18,20 @@ const DAYS = [ 30, // 9 31, // 10 30, // 11 - 31 // 12 + 31, // 12 ]; -const CustomDate = ({ value, onChange, className, title, style }) => { - let [month, date] = (value || '01.01').toString().split('.'); - date = parseInt(date, 10) || 0; - month = parseInt(month, 10) || 0; +interface CustomDateProps { + value: string; + onChange: (value: string) => void; + className?: string; + style?: React.CSSProperties; +} + +const CustomDate = ({ value, onChange, className, style }: CustomDateProps): React.JSX.Element => { + const [monthStr, dateStr] = (value || '01.01').toString().split('.'); + let date = parseInt(dateStr, 10) || 0; + let month = parseInt(monthStr, 10) || 0; if (month > 12) { month = 12; } else if (month < 0) { @@ -38,72 +44,156 @@ const CustomDate = ({ value, onChange, className, title, style }) => { date = 0; } - let days = []; + const days: number[] = []; for (let i = 0; i < DAYS[month]; i++) { days.push(i + 1); } - return
- - - - - + + - onChange(`${month.toString().padStart(2, '0')}.${e.target.value.toString().padStart(2, '0')}`)} - value={date} + style={style} > - {I18n.t('Any')} - {days.map(i => {i})} - - -
; -} - -CustomDate.defaultProps = { - value: '', - className: null, -}; - -CustomDate.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - style: PropTypes.object, - onChange: PropTypes.func + + +
+ ); }; export default CustomDate; diff --git a/src-editor/src/Components/RulesEditor/components/CustomDate/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomDate/style.module.scss index 74b20b49..1fe742c9 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomDate/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomDate/style.module.scss @@ -4,10 +4,10 @@ * { color: var(--colorBlock) !important; } - [class*="MuiInputLabel-shrink"] { + [class*='MuiInputLabel-shrink'] { color: var(--colorBlock) !important; } - [class*="MuiInput-underline"] { + [class*='MuiInput-underline'] { &:after { border-bottom-color: var(--lineColor) !important; } diff --git a/src-editor/src/Components/RulesEditor/components/CustomDragLayer/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomDragLayer/index.tsx index 40206625..4781aefc 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomDragLayer/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomDragLayer/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { useDragLayer } from 'react-dnd'; +import { useDragLayer, type XYCoord } from 'react-dnd'; import CardMenu from '../CardMenu'; import CurrentItem from '../CurrentItem'; +import type { AdminConnection } from '@iobroker/adapter-react-v5'; -const layerStyles = { +const layerStyles: React.CSSProperties = { position: 'fixed', pointerEvents: 'none', zIndex: 100, @@ -14,16 +15,20 @@ const layerStyles = { height: '100%', }; -const snapToGrid = (x, y) => { - const snappedX = Math.round(x / 32) * 32 - const snappedY = Math.round(y / 32) * 32 - return [snappedX, snappedY] -} +const snapToGrid = (x: number, y: number): [number, number] => { + const snappedX = Math.round(x / 32) * 32; + const snappedY = Math.round(y / 32) * 32; + return [snappedX, snappedY]; +}; -const getItemStyles = (initialOffset, currentOffset, isSnapToGrid) => { +const getItemStyles = ( + initialOffset: XYCoord | null, + currentOffset: XYCoord | null, + isSnapToGrid?: boolean, +): React.CSSProperties => { if (!initialOffset || !currentOffset) { return { - display: 'none' + display: 'none', }; } let { x, y } = currentOffset; @@ -37,44 +42,54 @@ const getItemStyles = (initialOffset, currentOffset, isSnapToGrid) => { const transform = `translate(${x}px, ${y}px)`; return { transform, - WebkitTransform: transform + WebkitTransform: transform, }; +}; + +interface CustomDragLayerProps { + socket: AdminConnection; + allBlocks: any; } -export const CustomDragLayer = props => { - const { - itemType, - isDragging, - item, - initialOffset, - currentOffset, - targetIds - } = useDragLayer(monitor => ({ +export const CustomDragLayer = (props: CustomDragLayerProps): React.JSX.Element | null => { + const { itemType, isDragging, item, initialOffset, currentOffset, targetIds } = useDragLayer(monitor => ({ item: monitor.getItem(), itemType: monitor.getItemType(), initialOffset: monitor.getInitialSourceClientOffset(), currentOffset: monitor.getSourceClientOffset(), isDragging: monitor.isDragging(), - targetIds: monitor.getTargetIds() + // @ts-expect-error fix later + targetIds: monitor.getTargetIds(), })); - const renderItem = () => { + const renderItem = (): React.JSX.Element | null => { switch (itemType) { case 'box': - return targetIds.length ? : - ; + return targetIds.length ? ( + + ) : ( + + ); default: return null; } - } + }; if (!isDragging) { return null; } - return
-
- {renderItem()} + return ( +
+
{renderItem()}
-
; + ); }; diff --git a/src-editor/src/Components/RulesEditor/components/CustomHint/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomHint/index.tsx index 51362afb..6805ccae 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomHint/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomHint/index.tsx @@ -3,50 +3,53 @@ import React, { useState } from 'react'; import { Fab, Tooltip } from '@mui/material'; import { HelpOutlineOutlined as HelpOutlineOutlinedIcon } from '@mui/icons-material'; +import type { IobTheme } from '@iobroker/adapter-react-v5'; -const styles = { - tooltip: theme => ({ +const styles: Record = { + tooltip: (theme: IobTheme): React.CSSProperties => ({ backgroundColor: '#83469c9e', color: 'rgb(255 255 255 / 87%)', boxShadow: theme.shadows[1], fontSize: 11, - border: '1px solid #920b9e' + border: '1px solid #920b9e', }), }; -const CustomHint = ({ children }) => { - const [open, setOpen] = useState(false); - return setOpen(false)} - onOpen={() => setOpen(true)}> - setOpen(!open)} - > - - - +interface CustomHintProps { + children?: React.ReactNode; } -CustomHint.defaultProps = { - children: null +const CustomHint = ({ children }: CustomHintProps): React.JSX.Element => { + const [open, setOpen] = useState(false); + return ( + setOpen(false)} + onOpen={() => setOpen(true)} + > + setOpen(!open)} + > + + + + ); }; export default CustomHint; diff --git a/src-editor/src/Components/RulesEditor/components/CustomInput/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomInput/index.tsx index 4f6a998d..0f79c25e 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomInput/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomInput/index.tsx @@ -1,76 +1,108 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { - TextField, - InputAdornment, -} from '@mui/material'; +import { TextField, InputAdornment } from '@mui/material'; import { Utils, Icon as CustomIcon } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const CustomInput = ({ autoFocus, fullWidth, disabled, multiline, rows, autoComplete, label, error, size, variant, value, type, style, onChange, className, customValue, icon }) => { - const [inputText, setInputText] = useState(''); - return { - !customValue && setInputText(e.target.value); - onChange(e.target.value); - }} - slotProps={{ - input: { - endAdornment: icon ? - - : null, - }, - }} - margin="normal" - size={size} - />; +interface CustomInputProps { + autoFocus?: boolean; + fullWidth?: boolean; + disabled?: boolean; + multiline?: boolean; + rows?: number; + autoComplete?: string; + label?: string; + error?: string; + size?: 'small' | 'medium'; + variant?: 'standard' | 'filled' | 'outlined'; + value: string | number | undefined; + type?: string; + style?: React.CSSProperties; + onChange?: (value: string | number) => void; + className?: string; + customValue?: boolean; + icon?: string; } -CustomInput.defaultProps = { - value: '', - type: 'text', - error: '', - className: null, - table: false, - native: {}, - variant: 'standard', - size: 'medium', - component: null, - styleComponentBlock: null, - onChange: () => { }, - fullWidth: false, - autoComplete: '', - customValue: false, - autoFocus: false, - rows: 1 -}; +const CustomInput = (props: CustomInputProps): React.JSX.Element => { + const [inputText, setInputText] = useState(''); + const { + value, + type, + error, + className, + icon, + label, + style, + onChange, + fullWidth, + autoComplete, + customValue, + autoFocus, + rows, + size, + variant, + multiline, + disabled, + }: CustomInputProps = Object.assign( + { + value: '', + type: 'text', + error: '', + className: null, + table: false, + native: {}, + variant: 'standard', + size: 'medium', + component: null, + styleComponentBlock: null, + fullWidth: false, + autoComplete: '', + customValue: false, + autoFocus: false, + rows: 1, + }, + props, + ); -CustomInput.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - type: PropTypes.string, - style: PropTypes.object, - native: PropTypes.object, - onChange: PropTypes.func, - component: PropTypes.object, - styleComponentBlock: PropTypes.object + return ( + { + !customValue && setInputText(e.target.value); + onChange && onChange(e.target.value); + }} + slotProps={{ + input: { + endAdornment: icon ? ( + + + + ) : null, + }, + }} + margin="normal" + size={size} + /> + ); }; export default CustomInput; diff --git a/src-editor/src/Components/RulesEditor/components/CustomInput/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomInput/style.module.scss index 14bae4c7..9786e095 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomInput/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomInput/style.module.scss @@ -6,33 +6,32 @@ * { color: var(--colorInput) !important; } - [class*="MuiInputLabel-shrink"] { + [class*='MuiInputLabel-shrink'] { color: var(--colorBlock) !important; } - [class*="MuiInput-underline"] { + [class*='MuiInput-underline'] { :after { border-bottom-color: var(--lineColor) !important; } } &:hover { - [class*="MuiOutlinedInput-notchedOutline"] { + [class*='MuiOutlinedInput-notchedOutline'] { border-color: var(--lineColor) !important; } } - [class*="MuiOutlinedInput-notchedOutline"] { + [class*='MuiOutlinedInput-notchedOutline'] { border-color: var(--lineColor) !important; &:hover { border-color: var(--lineColor) !important; } - [class*="Mui-focused"] { + [class*='Mui-focused'] { border-color: var(--lineColor) !important; } - [class*="Mui-disabled"] { + [class*='Mui-disabled'] { border-color: var(--lineColor) !important; } } } .icon { - } diff --git a/src-editor/src/Components/RulesEditor/components/CustomInstance/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomInstance/index.tsx index e146dba0..b88cd3d6 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomInstance/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomInstance/index.tsx @@ -1,14 +1,10 @@ import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - FormControl, FormHelperText, - Input, MenuItem, Select, -} from '@mui/material'; +import { FormControl, FormHelperText, Input, MenuItem, Select } from '@mui/material'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5'; -const styles = { +const styles: Record = { formControl: { m: '10px 0', '& .MuiFormControl-marginNormal': { @@ -16,10 +12,10 @@ const styles = { mb: 0, }, '& > *': { - color: '#2d0440 !important' + color: '#2d0440 !important', }, '& .MuiSelect-icon': { - color: '#81688c' + color: '#81688c', }, '& label.Mui-focused': { color: '#81688c', @@ -36,60 +32,91 @@ const styles = { }, }; -const CustomInstance = ({ multiple, value, customValue, socket, title, attr, adapter, style, onChange, className, onInstanceHide }) => { +interface CustomInstanceProps { + multiple?: boolean; + value: string | string[]; + customValue: boolean; + socket: AdminConnection; + title?: string; + attr: string; + adapter: string; + style?: React.CSSProperties; + onChange: (value: string | string[]) => void; + onInstanceHide: (value: string) => void; +} + +const CustomInstance = ({ + multiple, + value, + customValue, + socket, + title, + attr, + adapter, + style, + onChange, + onInstanceHide, +}: CustomInstanceProps): React.JSX.Element => { const [inputText, setInputText] = useState(value || 'test1'); - const [options, setOptions] = useState([]); + const [options, setOptions] = useState<{ value: string; title: string }[]>([]); useEffect(() => { - socket && socket.getAdapterInstances(adapter) - .then(instances => { - const _options = instances.map(obj => ({value: obj._id.replace('system.adapter.', ''), title: obj._id.replace('system.adapter.', '')})); - if (_options.length === 1) { - onInstanceHide(_options[0].value); - } else { - _options.unshift({value: adapter, title: I18n.t('All')}); - } - setOptions(_options); - }); + void socket?.getAdapterInstances(adapter).then(instances => { + const _options = instances.map(obj => ({ + value: obj._id.replace('system.adapter.', ''), + title: obj._id.replace('system.adapter.', ''), + })); + if (_options.length === 1) { + onInstanceHide(_options[0].value); + } else { + _options.unshift({ value: adapter, title: I18n.t('All') }); + } + setOptions(_options); + }); }, [socket, adapter, onInstanceHide]); - return - : } + style={style} > - {options.map(item => - {I18n.t(item.title)}{item.title2 &&
{item.title2}
}
)} - - {I18n.t(title)} -
; -} - -CustomInstance.defaultProps = { - value: '', - table: false, - customValue: false -}; - -CustomInstance.propTypes = { - title: PropTypes.string, - socket: PropTypes.object, - attr: PropTypes.string, - adapter: PropTypes.string, - style: PropTypes.object, - onChange: PropTypes.func, + + ) : ( + + ) + } + > + {options.map(item => ( + + {I18n.t(item.title)} + + ))} + + {title ? {I18n.t(title)} : null} + + ); }; export default CustomInstance; diff --git a/src-editor/src/Components/RulesEditor/components/CustomModal/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomModal/index.tsx index 5e0327db..a9238d8f 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomModal/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomModal/index.tsx @@ -1,69 +1,80 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - Dialog, - DialogActions, - DialogContent, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent } from '@mui/material'; import { I18n } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import CustomInput from '../CustomInput'; -const CustomModal = ({ open, onClose, children, titleButtonApply, titleButtonClose, onApply, className, textInput, defaultValue}) => { - let [value, setValue] = useState(defaultValue); - - return - - {textInput && } - {!textInput && children} - - - - - - ; +interface CustomModalProps { + onClose: () => void; + children?: React.JSX.Element[] | React.JSX.Element | null; + titleButtonApply?: string; + titleButtonClose?: string; + onApply: (value: string | number | null) => void; + className?: string; + textInput?: boolean; + defaultValue?: string | number; } -CustomModal.defaultProps = { - open: false, - onApply: () => { }, - onClose: () => { }, - titleButtonClose: 'Cancel', - titleButtonApply: 'Ok' -}; +const CustomModal = ({ + onClose, + children, + titleButtonApply, + titleButtonClose, + onApply, + className, + textInput, + defaultValue, +}: CustomModalProps): React.JSX.Element => { + const [value, setValue] = useState(defaultValue || ''); + const [originalValue] = useState(defaultValue || ''); -CustomModal.propTypes = { - open: PropTypes.bool, - onClose: PropTypes.func, - children: PropTypes.any, - titleButtonClose: PropTypes.string, - titleButtonApply: PropTypes.string, - onApply: PropTypes.func + return ( + + + {textInput && ( + + )} + {!textInput && children} + + + + + + + ); }; export default CustomModal; diff --git a/src-editor/src/Components/RulesEditor/components/CustomModal/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomModal/style.module.scss index 8777b8ba..2de172d1 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomModal/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomModal/style.module.scss @@ -29,7 +29,7 @@ } .modalWrapper { /*position: relative;*/ - [class*="MuiPaper-root MuiDialog-paper MuiPaper-elevation24 MuiDialog-paperScrollPaper MuiDialog-paperWidthXl MuiPaper-elevation24 MuiPaper-rounded"] { + [class*='MuiPaper-root MuiDialog-paper MuiPaper-elevation24 MuiDialog-paperScrollPaper MuiDialog-paperWidthXl MuiPaper-elevation24 MuiPaper-rounded'] { background-color: #f6f6f6; } } @@ -51,7 +51,7 @@ &:before { position: absolute; left: 15px; - content: ""; + content: ''; height: 33px; width: 4px; background-color: #ff4f4f; @@ -60,7 +60,7 @@ &:after { position: absolute; left: 15px; - content: ""; + content: ''; height: 33px; width: 4px; background-color: #ff4f4f; diff --git a/src-editor/src/Components/RulesEditor/components/CustomSelect/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomSelect/index.tsx index 0fcee750..06b0dc41 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSelect/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomSelect/index.tsx @@ -1,101 +1,175 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { - FormControl, FormHelperText, - Input, MenuItem, Select, -} from '@mui/material'; +import { FormControl, FormHelperText, Input, MenuItem, Select } from '@mui/material'; import { I18n, Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import CustomCheckbox from '../CustomCheckbox'; -const CustomSelect = ({ multiple, value, customValue, title, attr, options, style, onChange, className, doNotTranslate, doNotTranslate2 }) => { +interface CustomSelectProps { + multiple?: boolean; + value?: string | string[] | number; + customValue?: boolean; + title?: string; + attr: string; + options: { title: string; titleShort?: string; title2?: string; value: string | number; only?: boolean }[]; + style?: React.CSSProperties; + onChange: (value: string | string[] | number, attr: string) => void; + className?: string; + doNotTranslate?: boolean; + doNotTranslate2?: boolean; +} + +const CustomSelect = ({ + multiple, + value, + customValue, + title, + attr, + options, + style, + onChange, + className, + doNotTranslate, + doNotTranslate2, +}: CustomSelectProps): React.JSX.Element => { const [inputText, setInputText] = useState(value === undefined ? options[0].value : value); const v = customValue ? value : inputText; const text = v === '' || v === null || v === undefined ? '_' : v; - return - { + if (multiple && Array.isArray(selected)) { + // sort + selected.sort(); + let pos = selected.indexOf('0'); + if (pos !== -1) { + selected.splice(pos, 1); + selected.push('0'); + } + pos = selected.indexOf('_'); + if (pos !== -1) { + selected.splice(pos, 1); + selected.unshift('_'); + } - const onlyItem = options.find(el => el.only); - if (selected.includes(onlyItem.value)) { - return onlyItem.titleShort ? (doNotTranslate ? onlyItem.titleShort : I18n.t(onlyItem.titleShort)) : (doNotTranslate ? onlyItem.title : I18n.t(onlyItem.title)) - } + const onlyItem = options.find(el => el.only); - const titles = selected - .map(sel => options.find(item => item.value === sel || (sel === '_' && item.value === '')) || sel) - .map(item => typeof item === 'object' ? (item.titleShort ? (doNotTranslate ? item.titleShort : I18n.t(item.titleShort)) : (doNotTranslate ? item.title : I18n.t(item.title))) : (doNotTranslate ? item : I18n.t(item))); + if (onlyItem && selected.includes(onlyItem.value as string)) { + return onlyItem.titleShort + ? doNotTranslate + ? onlyItem.titleShort + : I18n.t(onlyItem.titleShort) + : doNotTranslate + ? onlyItem.title + : I18n.t(onlyItem.title); + } - return titles.join(', '); - } else { - const item = options ? options.find(item => item.value === selected || (selected === '_' && item.value === '')) : null; + const titles = selected + .map( + sel => + options.find(item => item.value === sel || (sel === '_' && item.value === '')) || + sel, + ) + .map(item => + typeof item === 'object' + ? item.titleShort + ? doNotTranslate + ? item.titleShort + : I18n.t(item.titleShort) + : doNotTranslate + ? item.title + : I18n.t(item.title) + : doNotTranslate + ? item + : I18n.t(item), + ); + + return titles.join(', '); + } + const item = options + ? options.find(item => item.value === selected || (selected === '_' && item.value === '')) + : null; return item?.title ? (doNotTranslate ? item?.title : I18n.t(item?.title)) : selected; - } - }} - onChange={e => { - !customValue && setInputText(e.target.value); - if (multiple) { - const onlyItem = options.find(el => el.only); - if (onlyItem) { - const valueOnly = onlyItem.value; - if (e.target.value.length === options.length - 1 && e.target.value.includes(valueOnly)) { - return onChange(e.target.value.filter(el => el !== valueOnly), attr); - } - if (e.target.value.includes(valueOnly)) { - return onChange(options.map(el => el.value), attr); + }} + onChange={e => { + !customValue && setInputText(e.target.value); + if (multiple) { + const values = e.target.value as string[]; + const onlyItem = options.find(el => el.only); + + if (onlyItem) { + const valueOnly = onlyItem.value as string; + + if (values.length === options.length - 1 && values.includes(valueOnly)) { + return onChange( + values.filter(el => el !== valueOnly), + attr, + ); + } + + if (values.includes(valueOnly)) { + return onChange(options.map(el => el.value) as string[], attr); + } } } + onChange(e.target.value, attr); + }} + input={ + attr ? ( + + ) : ( + + ) } - onChange(e.target.value, attr); - }} - input={attr ? : } - > - {!multiple && options && options.map(item => {doNotTranslate ? item.title : I18n.t(item.title)}{item.title2 &&
{doNotTranslate2 ? item.title2 : I18n.t(item.title2)}
}
)} - {multiple && options && options.map(item => {doNotTranslate ? item.title : I18n.t(item.title)} )} - - {title ? {I18n.t(title)} : null} -
; -} - -CustomSelect.defaultProps = { - value: '', - className: null, - table: false, - customValue: false, - multiple: false -}; - -CustomSelect.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - options: PropTypes.array.isRequired, - style: PropTypes.object, - onChange: PropTypes.func + > + {!multiple && + options && + options.map(item => ( + + {doNotTranslate ? item.title : I18n.t(item.title)} + {item.title2 &&
{doNotTranslate2 ? item.title2 : I18n.t(item.title2)}
} +
+ ))} + {multiple && + options?.map(item => ( + + {doNotTranslate ? item.title : I18n.t(item.title)}{' '} + + + ))} + + {title ? {I18n.t(title)} : null} + + ); }; export default CustomSelect; diff --git a/src-editor/src/Components/RulesEditor/components/CustomSelect/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomSelect/style.module.scss index 47e3de54..fabb0475 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSelect/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomSelect/style.module.scss @@ -5,10 +5,10 @@ * { color: var(--colorBlock) !important; } - [class*="MuiInputLabel-shrink"] { + [class*='MuiInputLabel-shrink'] { color: var(--colorBlock) !important; } - [class*="MuiInput-underline"] { + [class*='MuiInput-underline'] { &:after { border-bottom-color: var(--lineColor) !important; } diff --git a/src-editor/src/Components/RulesEditor/components/CustomSlider/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomSlider/index.tsx index 1a6a3542..26a1da92 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSlider/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomSlider/index.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import { Slider } from '@mui/material'; @@ -7,7 +6,36 @@ import { Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const CustomSlider = ({ fullWidth, autoComplete, label, error, size, variant, value, type, style, onChange, className, customValue, min, max, step, unit }) => { +interface CustomSliderProps { + autoComplete?: string; + label?: string; + error?: string; + size?: 'small' | 'medium'; + variant?: 'standard' | 'filled' | 'outlined'; + value?: number; + type?: string; + style?: object; + onChange: (newValue: number) => void; + className?: string; + customValue?: boolean; + min?: number; + max?: number; + step?: number; + unit?: string; +} + +const CustomSlider = ({ + size, + value, + style, + onChange, + className, + customValue, + min, + max, + step, + unit, +}: CustomSliderProps): React.JSX.Element => { const [inputText, setInputText] = useState(0); min = min !== undefined ? min : 0; max = max !== undefined ? max : 0; @@ -24,61 +52,39 @@ const CustomSlider = ({ fullWidth, autoComplete, label, error, size, variant, va }, ]; - return { - !customValue && setInputText(newValue); - onChange(newValue); - }} - margin="normal" - size={size} - />; -} - -CustomSlider.defaultProps = { - value: '', - type: 'text', - error: '', - className: null, - table: false, - native: {}, - variant: 'standard', - size: 'medium', - component: null, - styleComponentBlock: null, - onChange: () => { }, - fullWidth: false, - autoComplete: '', - customValue: false -}; - -CustomSlider.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - type: PropTypes.string, - style: PropTypes.object, - native: PropTypes.object, - onChange: PropTypes.func, - component: PropTypes.object, - styleComponentBlock: PropTypes.object + return ( + { + if (Array.isArray(newValue)) { + !customValue && setInputText(newValue[0]); + onChange(newValue[0]); + } else { + !customValue && setInputText(newValue); + onChange(newValue); + } + }} + // margin="normal" + size={size || 'medium'} + /> + ); }; -export default CustomSlider; \ No newline at end of file +export default CustomSlider; diff --git a/src-editor/src/Components/RulesEditor/components/CustomSlider/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomSlider/style.module.scss index d08abf75..9d79168c 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSlider/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomSlider/style.module.scss @@ -1,18 +1,18 @@ .root { color: var(--lineColorActive) !important; height: 8px !important; - [class*="MuiSlider-rail"] { + [class*='MuiSlider-rail'] { height: 8px !important; border-radius: 4px; } - [class*="MuiSlider-track"] { + [class*='MuiSlider-track'] { height: 8px !important; border-radius: 4px; } - [class*="MuiSlider-valueLabel"] { + [class*='MuiSlider-valueLabel'] { left: calc(-50% + 4px); } - [class*="MuiSlider-thumb"] { + [class*='MuiSlider-thumb'] { height: 24px; width: 24px; background-color: var(--colorBlock); @@ -21,7 +21,7 @@ margin-left: -12px; &:focus, &:hover, - &[class*="MuiSlider-active"] { + &[class*='MuiSlider-active'] { box-shadow: inherit !important; } } @@ -29,4 +29,3 @@ background-color: #00000000 !important; } } - diff --git a/src-editor/src/Components/RulesEditor/components/CustomSwitch/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomSwitch/index.tsx index 5e44f44c..e084cf50 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSwitch/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomSwitch/index.tsx @@ -1,56 +1,49 @@ -import PropTypes from 'prop-types'; import React, { memo, useState } from 'react'; import { FormControlLabel, Switch } from '@mui/material'; import cls from './style.module.scss'; -const CustomSwitch = ({ label, size, value, style, onChange, className, customValue }) => { - const [switchChecked, setSwitchChecked] = useState(false); - return { - if (!customValue) setSwitchChecked(e.target.checked); - onChange(e.target.checked); - }} - size={size} - /> - } - label={label} - />; +interface CustomSwitchProps { + label: string; + size?: 'small' | 'medium'; + value: boolean; + style?: React.CSSProperties; + onChange: (value: boolean) => void; + className?: string; + customValue?: boolean; } -CustomSwitch.defaultProps = { - value: false, - type: 'text', - error: '', - className: null, - table: false, - native: {}, - variant: 'standard', - size: 'medium', - component: null, - styleComponentBlock: null, - onChange: () => { }, - fullWidth: false, - autoComplete: '', - customValue: false, - label: 'all' -}; - -CustomSwitch.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - type: PropTypes.string, - style: PropTypes.object, - native: PropTypes.object, - onChange: PropTypes.func, - component: PropTypes.object, - styleComponentBlock: PropTypes.object +const CustomSwitch = ({ + label, + size, + value, + style, + onChange, + className, + customValue, +}: CustomSwitchProps): React.JSX.Element => { + const [switchChecked, setSwitchChecked] = useState(false); + return ( + { + if (!customValue) { + setSwitchChecked(e.target.checked); + } + onChange(e.target.checked); + }} + size={size || 'medium'} + /> + } + label={label || 'all'} + /> + ); }; export default memo(CustomSwitch); diff --git a/src-editor/src/Components/RulesEditor/components/CustomSwitch/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomSwitch/style.module.scss index 743bb792..1457d989 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomSwitch/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomSwitch/style.module.scss @@ -2,10 +2,10 @@ * { color: var(--colorBlock) !important; } - [class*="Mui-checked"] { + [class*='Mui-checked'] { color: var(--lineColor) !important; } - [class*="Mui-checked"] + [class*="MuiSwitch-track"] { + [class*='Mui-checked'] + [class*='MuiSwitch-track'] { background-color: var(--lineColor) !important; } } diff --git a/src-editor/src/Components/RulesEditor/components/CustomTime/index.tsx b/src-editor/src/Components/RulesEditor/components/CustomTime/index.tsx index e90b2b39..d5f622c4 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomTime/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/CustomTime/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { TextField } from '@mui/material'; @@ -7,38 +6,34 @@ import { Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; -const CustomTime = ({ value, style, onChange, className }) => { - return onChange(e.currentTarget.value)} - value={value} - className={Utils.clsx(cls.root, className)} - fullWidth - style={style} - slotProps={{ - htmlInput: { - step: 300, // 5 min - }, - inputLabel: { - shrink: true, - }, - }} - />; +interface CustomTimeProps { + value: string; + style?: React.CSSProperties; + onChange: (value: string) => void; + className?: string; } -CustomTime.defaultProps = { - value: '', - className: null, - table: false -}; - -CustomTime.propTypes = { - title: PropTypes.string, - attr: PropTypes.string, - style: PropTypes.object, - onChange: PropTypes.func +const CustomTime = ({ value, style, onChange, className }: CustomTimeProps): React.JSX.Element => { + return ( + onChange(e.currentTarget.value)} + value={value} + className={Utils.clsx(cls.root, className)} + fullWidth + style={style} + slotProps={{ + htmlInput: { + step: 300, // 5 min + }, + inputLabel: { + shrink: true, + }, + }} + /> + ); }; export default CustomTime; diff --git a/src-editor/src/Components/RulesEditor/components/CustomTime/style.module.scss b/src-editor/src/Components/RulesEditor/components/CustomTime/style.module.scss index 74b20b49..1fe742c9 100644 --- a/src-editor/src/Components/RulesEditor/components/CustomTime/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/CustomTime/style.module.scss @@ -4,10 +4,10 @@ * { color: var(--colorBlock) !important; } - [class*="MuiInputLabel-shrink"] { + [class*='MuiInputLabel-shrink'] { color: var(--colorBlock) !important; } - [class*="MuiInput-underline"] { + [class*='MuiInput-underline'] { &:after { border-bottom-color: var(--lineColor) !important; } diff --git a/src-editor/src/Components/RulesEditor/components/DragWrapper/index.tsx b/src-editor/src/Components/RulesEditor/components/DragWrapper/index.tsx index e6ac88d2..3632e786 100644 --- a/src-editor/src/Components/RulesEditor/components/DragWrapper/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/DragWrapper/index.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; import { useDrag, useDrop } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; @@ -8,15 +7,43 @@ import { filterElement } from '../../helpers/filterElement'; import { findCard, moveCard } from '../../helpers/cardSort'; import { ContextWrapperCreate } from '../ContextWrapper'; import cls from './style.module.scss'; +import type { BlockValue, RuleBlockConfig, RuleBlockDescription, RuleBlockType, RuleUserRules } from '../../types'; -const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, userRules, children, _id, blockValue }) => { +interface DragWrapperProps { + typeBlock?: RuleBlockType; + allProperties: RuleBlockDescription | RuleBlockConfig; + id: string; + _id?: number; + isActive?: boolean; + setUserRules: (newRules: RuleUserRules) => void; + userRules: RuleUserRules; + children: React.ReactNode; + blockValue?: BlockValue; +} + +const DragWrapper = ({ + typeBlock, + allProperties, + id, + isActive, + setUserRules, + userRules, + children, + _id, + blockValue, +}: DragWrapperProps): React.JSX.Element => { const { setOnUpdate } = useContext(ContextWrapperCreate); const [{ opacity }, drag, preview] = useDrag({ type: 'box', - item: () => ({ ...allProperties, id, isActive, _id }), + item: (): Omit & { isActive?: boolean; _id?: number } => ({ + ...allProperties, + id, + isActive, + _id, + }), end: (item, monitor) => { - let { acceptedBy } = item; - let dropResult = monitor.getDropResult(); + const { acceptedBy } = item; + const dropResult: { blockValue: BlockValue } | null = monitor.getDropResult(); let newUserRules; if (!dropResult) { if (typeof _id === 'number' && !monitor.getTargetIds().length) { @@ -27,30 +54,33 @@ const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, us return null; } if (dropResult.blockValue !== blockValue) { - let idNumber = typeof _id === 'number' ? _id : Date.now(); + const idNumber = typeof _id === 'number' ? _id : Date.now(); newUserRules = deepCopy(acceptedBy, userRules, dropResult.blockValue); - const newItem = { id: item.id, acceptedBy: item.acceptedBy } + const newItem = { id: item.id, acceptedBy: item.acceptedBy }; switch (acceptedBy) { case 'actions': if (blockValue) { - newUserRules = filterElement(acceptedBy, newUserRules, blockValue, _id); + newUserRules = filterElement('actions', newUserRules, blockValue, idNumber); } - newUserRules = filterElement(acceptedBy, newUserRules, dropResult.blockValue, _id); - newUserRules[acceptedBy][dropResult.blockValue].push({ ...newItem, _id: idNumber }); + newUserRules = filterElement('actions', newUserRules, dropResult.blockValue, idNumber); + newUserRules.actions[dropResult.blockValue as 'then' | 'else'].push({ + ...newItem, + _id: idNumber, + }); return setUserRules(newUserRules); case 'conditions': if (typeof blockValue === 'number') { - newUserRules = filterElement(acceptedBy, newUserRules, blockValue, _id); + newUserRules = filterElement('conditions', newUserRules, blockValue, idNumber); } - newUserRules = filterElement(acceptedBy, newUserRules, dropResult.blockValue, _id); - newUserRules[acceptedBy][dropResult.blockValue].push({ ...newItem, _id: idNumber }); + newUserRules = filterElement('conditions', newUserRules, dropResult.blockValue, idNumber); + newUserRules.conditions[dropResult.blockValue as number].push({ ...newItem, _id: idNumber }); return setUserRules(newUserRules); default: setOnUpdate(true); - newUserRules = filterElement(acceptedBy, newUserRules, dropResult.blockValue, _id); - newUserRules[acceptedBy].push({ ...newItem, _id: idNumber }); + newUserRules = filterElement('triggers', newUserRules, dropResult.blockValue, idNumber); + newUserRules.triggers.push({ ...newItem, _id: idNumber }); return setUserRules(newUserRules); } } @@ -60,29 +90,32 @@ const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, us isDragging: monitor.isDragging(), }), }); - const ref = useRef(null) + + const ref = useRef(null); + const [, drop] = useDrop({ accept: 'box', canDrop: () => false, - hover({ _id: draggedId, acceptedBy }, monitor) { + hover({ _id: draggedId, acceptedBy }: { _id: number; acceptedBy: RuleBlockType }, monitor) { if (!ref.current) { return; } - if (typeBlocks !== acceptedBy) { - return + if (typeBlock !== acceptedBy) { + return; } const hoverBoundingRect = ref.current?.getBoundingClientRect(); const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; const clientOffset = monitor.getClientOffset(); - const hoverClientY = clientOffset.y - hoverBoundingRect.top; + const hoverClientY = (clientOffset?.y || 0) - hoverBoundingRect.top; - if (!!_id && draggedId !== _id) { + if (_id && draggedId !== _id) { switch (acceptedBy) { case 'actions': if (blockValue === 'then' || blockValue === 'else') { - const { index: overIndexActions } = findCard(_id, userRules[acceptedBy][blockValue]); + const { index: overIndexActions } = findCard(_id, userRules.actions[blockValue]); if (overIndexActions !== draggedId) { - moveCard(draggedId, + moveCard( + draggedId, overIndexActions, userRules[acceptedBy][blockValue], setUserRules, @@ -90,7 +123,7 @@ const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, us acceptedBy, blockValue, hoverClientY, - hoverMiddleY + hoverMiddleY, ); } } @@ -99,7 +132,8 @@ const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, us if (typeof blockValue === 'number') { const { index: overIndexConditions } = findCard(_id, userRules[acceptedBy][blockValue]); if (overIndexConditions !== draggedId) { - moveCard(draggedId, + moveCard( + draggedId, overIndexConditions, userRules[acceptedBy][blockValue], setUserRules, @@ -107,49 +141,55 @@ const DragWrapper = ({ typeBlocks, allProperties, id, isActive, setUserRules, us acceptedBy, blockValue, hoverClientY, - hoverMiddleY + hoverMiddleY, ); } } return; - default: + default: { const { index: overIndex } = findCard(_id, userRules[acceptedBy]); if (overIndex !== draggedId) { - moveCard(draggedId, + moveCard( + draggedId, overIndex, userRules[acceptedBy], setUserRules, userRules, acceptedBy, - null, + undefined, hoverClientY, - hoverMiddleY + hoverMiddleY, ); } return; + } } } - } + }, }); + useEffect(() => { preview(getEmptyImage(), { captureDraggingState: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); drag(drop(ref)); - const isMobile = window.innerWidth < 600; - return
{children}
; -} -DragWrapper.defaultProps = { - name: '', - active: false, - id: '', - _id: null -}; + const isMobile = window.innerWidth < 600; -DragWrapper.propTypes = { - name: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + return ( +
+
+ {children} +
+ ); }; export default DragWrapper; diff --git a/src-editor/src/Components/RulesEditor/components/GenericBlock/index.tsx b/src-editor/src/Components/RulesEditor/components/GenericBlock/index.tsx index 97a917a3..01043694 100644 --- a/src-editor/src/Components/RulesEditor/components/GenericBlock/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/GenericBlock/index.tsx @@ -1,19 +1,18 @@ import React, { PureComponent, Fragment } from 'react'; import cls from './style.module.scss'; -import { - Menu, - MenuItem, - IconButton, -} from '@mui/material'; +import { Menu, MenuItem, IconButton } from '@mui/material'; import { HelpOutline as IconHelp } from '@mui/icons-material'; -import { getSelectIdIcon } from '@iobroker/adapter-react-v5/Components/Icon'; import { - I18n, Utils, + getSelectIdIcon, + I18n, + Utils, SelectID as DialogSelectID, Error as DialogError, Message as DialogMessage, + type AdminConnection, + type IobTheme, } from '@iobroker/adapter-react-v5'; import CustomButton from '../CustomButton'; @@ -28,21 +27,130 @@ import CustomTime from '../CustomTime'; import CustomDate from '../CustomDate'; import MaterialDynamicIcon from '../../helpers/MaterialDynamicIcon'; -import utils from '../../helpers/utils'; +import { getName } from '../../helpers/utils'; import { STEPS } from '../../helpers/Tour'; +import type { + RuleBlockConfig, + RuleBlockDescription, + RuleContext, + RuleInputAny, + RuleInputButton, + RuleInputCheckbox, + RuleInputColor, + RuleInputNameText, + RuleInputNumber, + RuleInputSlider, + RuleInputSwitch, + RuleInputText, + RuleInputAll, + RuleTagCard, + RuleTagCardTitle, + RuleUserRules, + RuleInputObjectID, + RuleInputTime, + RuleInputSelect, + RuleInputInstance, + RuleInputDialog, + RuleInputModalInput, + RuleInputDate, + RuleInputCron, + RuleInputWizard, + DebugMessage, + RuleBlockConfigTriggerState, +} from '../../types'; + +export interface GenericBlockProps { + _id: number; + name?: string; + icon?: string; + adapter?: string; + socket: AdminConnection; + userRules?: RuleUserRules; + classes?: { + valueAck: string; + valueNotAck: string; + }; + settings?: Settings; + onChange: (settings: Settings) => void; + onDebugMessage?: DebugMessage[]; + enableSimulation: boolean; + theme: IobTheme; + className?: string; + style?: React.CSSProperties; + inputs?: RuleInputAny[]; + notFound?: boolean; + isTourOpen?: boolean; + tourStep?: number; + setTourStep?: (step: number) => void; + setOnUpdate?: (value: boolean) => void; + helpDialog?: string; + acceptedBy?: string; + onUpdate?: boolean; +} + +export interface GenericBlockState { + inputs: RuleInputAny[]; + name: string; + icon: string; + adapter: string; + helpDialog: string; + tagCardArray: (RuleTagCard | RuleTagCardTitle)[]; + openTagMenu: any; + openModal: boolean; + iconTag: boolean; + error: string; + helpText: string; + instanceSelectionOptions: any[]; + instanceSelectionDef: string; + hideAttributes: string[]; + settings: Settings; + debugMessage: any; + enableSimulation: boolean; +} + +export abstract class GenericBlock< + Settings extends RuleBlockConfig = RuleBlockConfig, + TState extends GenericBlockState = GenericBlockState, +> extends PureComponent, TState> { + private debugHideTimeout: ReturnType | null = null; + + private lastObjectIdChange: number = 0; + private enableSimulationProcessing = false; + private lastDebugMessage = 0; + private debugMessageTimeout: ReturnType | null = null; -class GenericBlock extends PureComponent { - constructor(props, item) { + static getStaticData(): RuleBlockDescription { + return { + acceptedBy: 'actions', + name: 'Not found', + id: 'ActionEmpty', + icon: 'Shuffle', + }; + } + + static compile(_config: RuleBlockConfig, _context: RuleContext): string { + return ''; + } + + protected constructor(props: GenericBlockProps, item: RuleBlockDescription) { super(props); item = item || {}; - let settings = props.settings || { - tagCard: item.tagCardArray ? typeof item.tagCardArray[0] !== 'string' ? item.tagCardArray[0].title : item.tagCardArray[0] : '', - }; + const settings: Settings = + props.settings || + ({ + tagCard: item.tagCardArray + ? typeof item.tagCardArray[0] !== 'string' + ? item.tagCardArray[0].title + : item.tagCardArray[0] + : '', + } as Settings); if (!settings.tagCard && item.tagCardArray) { - settings.tagCard = typeof item.tagCardArray[0] !== 'string' ? item.tagCardArray[0].title : item.tagCardArray[0]; + settings.tagCard = + typeof item.tagCardArray[0] !== 'string' ? item.tagCardArray[0].title : item.tagCardArray[0]; } + // @ts-expect-error fix later this.state = { inputs: item.inputs || props.inputs || [], name: item.name || props.name || '', @@ -58,7 +166,6 @@ class GenericBlock extends PureComponent { error: '', helpText: '', - oid: {}, instanceSelectionOptions: [], instanceSelectionDef: '', @@ -67,61 +174,61 @@ class GenericBlock extends PureComponent { settings, debugMessage: null, enableSimulation: this.props.enableSimulation, - }; - - this.debugHideTimeout = null; + } satisfies GenericBlockState; } - UNSAFE_componentWillReceiveProps(nextProps) { + static getDerivedStateFromProps( + nextProps: GenericBlockProps, + state: GenericBlockState, + ): Partial> | null { if (!nextProps || !nextProps.settings) { console.log(JSON.stringify(nextProps)); - return; - } - - const settings = JSON.parse(JSON.stringify(nextProps.settings)); - if (!settings.tagCard && this.state.tagCardArray && this.state.tagCardArray.length) { - settings.tagCard = typeof this.state.tagCardArray[0] !== 'string' ? this.state.tagCardArray[0].title : this.state.tagCardArray[0]; - } - - let newState = null; - - if (nextProps.onDebugMessage && nextProps.onDebugMessage.blockId === this.props._id) { - newState = {}; - newState.debugMessage = JSON.parse(JSON.stringify(nextProps.onDebugMessage)); - this.debugHideTimeout && clearTimeout(this.debugHideTimeout); - this.debugHideTimeout = setTimeout(() => - this.setState({ debugMessage: null }), - nextProps.onDebugMessage.hideTimeout || 5000); + return null; } - if (JSON.stringify(settings) !== JSON.stringify(this.state.settings)) { - newState = newState || {}; - newState.settings = settings; + const settings: any = JSON.parse(JSON.stringify(nextProps.settings)); + if (!settings.tagCard && state.tagCardArray && state.tagCardArray.length) { + settings.tagCard = + typeof state.tagCardArray[0] !== 'string' ? state.tagCardArray[0].title : state.tagCardArray[0]; } - if (this.state.enableSimulation !== nextProps.enableSimulation) { - newState = newState || {}; - newState.enableSimulation = nextProps.enableSimulation; + if (JSON.stringify(settings) !== JSON.stringify(state.settings)) { + return { settings }; } - newState && this.setState(newState); + return null; } - componentWillUnmount() { - this.debugHideTimeout && clearTimeout(this.debugHideTimeout); - this.debugHideTimeout = null; + componentWillUnmount(): void { + if (this.debugMessageTimeout) { + clearTimeout(this.debugMessageTimeout); + this.debugMessageTimeout = null; + } + if (this.debugHideTimeout) { + clearTimeout(this.debugHideTimeout); + this.debugHideTimeout = null; + } } // called every time, the tagCard changes or at start - onTagChange(tagCard, cb) { + onTagChange( + _tagCard?: RuleTagCardTitle | null, + cb?: () => void, + _value?: any, + _toggle?: boolean, + _useTrigger?: boolean, + ): void { // analyse inputs and fill the attributes with default values let changed = false; - let settings = JSON.parse(JSON.stringify(this.state.settings)); + const settings: Settings = JSON.parse(JSON.stringify(this.state.settings)); this.state.inputs.forEach(input => { - if (input.attr && input.defaultValue !== undefined) { - if (settings[input.attr] === undefined) { + const attr: string | undefined = (input as RuleInputAll).attr; + const defaultValue: any = (input as RuleInputAll).defaultValue; + + if (attr && defaultValue !== undefined) { + if (attr && (settings as Record)[attr] === undefined) { changed = true; - settings[input.attr] = input.defaultValue; + (settings as Record)[attr] = defaultValue; } } }); @@ -134,172 +241,251 @@ class GenericBlock extends PureComponent { } // called if trigger added or removed - onUpdate() { + // eslint-disable-next-line class-methods-use-this + onUpdate(): void { // do nothing, but blocks can overwrite it } // called every time if some attribute changes - onValueChanged(value, attr) { + // eslint-disable-next-line class-methods-use-this + onValueChanged(_value: any, _attr: string): void { // do nothing, but blocks can overwrite it } - renderText = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderText = (input: RuleInputText, value: string, onChange: (value: string) => void): React.JSX.Element => { const { className } = this.props; const { attr, frontText, backText, nameBlock, name, doNotTranslate, doNotTranslateBack } = input; - return -
+ return ( + +
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + void} + customValue + /> + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ {nameBlock &&
{I18n.t(nameBlock)}
} +
+ ); + }; + + // eslint-disable-next-line react/no-unused-class-component-methods + renderSwitch = (input: RuleInputSwitch, value: boolean, onChange: (value: boolean) => void): React.JSX.Element => { + const { className } = this.props; + const { attr, frontText, backText, nameBlock, doNotTranslate, doNotTranslateBack } = input; + return ( +
+
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ {nameBlock &&
{I18n.t(nameBlock)}
} +
+ ); + }; + + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderNameText = ( + { attr, signature, doNotTranslate, defaultValue }: RuleInputNameText, + value: string, + ): React.JSX.Element => ( +
+ {value ? (doNotTranslate ? value : I18n.t(value)) : doNotTranslate ? defaultValue : I18n.t(defaultValue)} +
+ ); + + // eslint-disable-next-line react/no-unused-class-component-methods + renderNumber = ( + input: RuleInputNumber, + value: number, + onChange: (value: number | string) => void, + ): React.JSX.Element | null => { + const { className } = this.props; + const { settings } = this.state; + const { attr, backText, frontText, openCheckbox, doNotTranslate, doNotTranslateBack } = input; + let visibility = true; + if (openCheckbox) { + visibility = + typeof (settings as Record).offset === 'boolean' + ? (settings as Record).offset + : true; + } + return visibility ? ( +
{frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
}
- {nameBlock &&
{I18n.t(nameBlock)}
} - ; - } + ) : null; + }; - renderSwitch = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderColor = (input: RuleInputColor, value: string, onChange: (value: string) => void): React.JSX.Element => { const { className } = this.props; - const { attr, frontText, backText, nameBlock, doNotTranslate, doNotTranslateBack } = input; - return
-
+ const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input; + return ( +
{frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - void} /> {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
}
- {nameBlock &&
{I18n.t(nameBlock)}
} -
; - } - - renderNameText = ({ attr, signature, doNotTranslate, defaultValue }, value) =>
- {value ? (doNotTranslate ? value : I18n.t(value)) : (doNotTranslate ? defaultValue : I18n.t(defaultValue))} -
; - - renderNumber = (input, value, onChange) => { - const { className } = this.props; - const { settings } = this.state; - const { attr, backText, frontText, openCheckbox, doNotTranslate, doNotTranslateBack } = input; - let visibility = true; - if (openCheckbox) { - visibility = typeof settings['offset'] === 'boolean' ? settings['offset'] : true; - } - return visibility ?
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
: null; - } - - renderColor = (input, value, onChange) => { - const { className } = this.props; - const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input; - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; - } + ); + }; - renderCheckbox = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderCheckbox = ( + input: RuleInputCheckbox, + value: boolean, + onChange: (value: boolean) => void, + ): React.JSX.Element => { const { className } = this.props; const { settings } = this.state; const { attr, backText, frontText, defaultValue, doNotTranslate, doNotTranslateBack } = input; - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
onChange(typeof settings[attr] === 'boolean' ? !settings[attr] : !defaultValue)} className={cls.backText}>{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; - } - - renderSlider = (input, value, onChange) => { - const { className } = this.props; - const { attr, frontText, backText, nameBlock, min, max, step, unit, doNotTranslate, doNotTranslateBack } = input; - return
-
+ return ( +
{frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - { - console.log(val); - onChange(val); - }} + value={ + typeof (settings as Record)[attr] === 'boolean' + ? !!(settings as Record)[attr] + : !!defaultValue + } + customValue + onChange={onChange} /> - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} + {backText && ( +
+ onChange( + typeof (settings as Record)[attr] === 'boolean' + ? !(settings as Record)[attr] + : !defaultValue, + ) + } + className={cls.backText} + > + {doNotTranslateBack ? backText : I18n.t(backText)} +
+ )}
- {nameBlock &&
{I18n.t(nameBlock)}
} -
; + ); }; - renderButton = (input, value, onClick) => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderSlider = (input: RuleInputSlider, value: number, onChange: (value: number) => void): React.JSX.Element => { const { className } = this.props; - const { attr, frontText, backText, buttonText, doNotTranslate, doNotTranslateBack } = input; - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + const { attr, frontText, backText, nameBlock, min, max, step, unit, doNotTranslate, doNotTranslateBack } = + input; + return ( +
+
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + { + console.log(val); + onChange(val); + }} + /> + {backText && ( +
+ {doNotTranslateBack ? backText : I18n.t(backText)} +
+ )} +
+ {nameBlock &&
{I18n.t(nameBlock)}
} +
+ ); }; - findIcon = obj => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderButton = (input: RuleInputButton, value: boolean, onChange: (bValue: boolean) => void): React.JSX.Element => { + const { className } = this.props; + const { attr, frontText, backText, doNotTranslate, doNotTranslateBack } = input; + return ( +
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + onChange(value)} + /> + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ ); + }; + + findIcon(obj: ioBroker.Object | null | undefined): Promise { if (!obj) { return Promise.resolve(null); } @@ -313,251 +499,358 @@ class GenericBlock extends PureComponent { parts.pop(); const newId = parts.join('.'); - return this.props.socket.getObject(newId) + return this.props.socket + .getObject(newId) .then(o => this.findIcon(o)) .catch(() => null); } - }; + return Promise.resolve(null); + } - renderObjectID = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods + renderObjectID = ( + input: RuleInputObjectID, + value: string, + onChange: (value: Record, cb: () => void) => void, + ): React.JSX.Element | null => { const { attr, openCheckbox, checkReadOnly } = input; const { settings } = this.state; - const showSelectId = this.state[`showSelectId${attr}`]; + const showSelectId = (this.state as Record)[`showSelectId${attr}`]; const { className, socket, style } = this.props; let visibility = true; if (openCheckbox) { - visibility = typeof settings['offset'] === 'boolean' ? settings['offset'] : true; + visibility = + typeof (settings as Record).offset === 'boolean' + ? (settings as Record).offset + : true; } - if (settings[attr] && !this.state[settings[attr]]) { - setTimeout(() => { - socket.getObject(value) - .then(obj => { - this.findIcon(obj) - .then(icon => this.setState({ - [settings[attr]]: obj, - [`${settings[attr]}___icon`]: icon, - error: checkReadOnly && this.lastObjectIdChange && Date.now() - this.lastObjectIdChange < 1000 && obj?.common?.write === false ? - I18n.t('Read only ID selected: %s', settings[attr]) : '', - })) - }); - }, 0); + const oid: string | undefined = (settings as Record)[attr]; + const iobObj: ioBroker.Object | null | undefined | false = oid + ? (this.state as Record)[oid] + : undefined; + + if (oid && !iobObj && iobObj !== false) { + setTimeout( + async (_attrStr: string): Promise => { + const obj = await socket.getObject(value); + const icon = await this.findIcon(obj); + const newState: Partial = { + [_attrStr]: obj || false, + [`${_attrStr}___icon`]: icon, + error: + checkReadOnly && + this.lastObjectIdChange && + Date.now() - this.lastObjectIdChange < 1000 && + obj?.common?.write === false + ? I18n.t('Read only ID selected: %s', (settings as Record)[_attrStr]) + : '', + } as Partial; + + this.setState(newState as TState); + }, + 0, + oid, + ); } // return null - return visibility ?
-
- {input.title ?
{I18n.t(input.title)}
: null} - - { - const settings = {}; - settings[`showSelectId${attr}`] = true; - this.setState(settings); - }} - /> -
- {this.state[this.state.settings[input.attr]] &&
{Utils.getObjectNameFromObj(this.state[settings[attr]], I18n.getLanguage())}
} - {showSelectId ? { - const settings = {}; - settings[`showSelectId${attr}`] = false; - this.setState(settings); - }} - onOk={(selected, name, common) => { - const settings = {}; - settings[`showSelectId${attr}`] = false; - this.setState(settings, () => - // read type of object - socket.getObject(selected) - .then(obj => { + return visibility ? ( +
+
+ {input.title ?
{I18n.t(input.title)}
: null} + + )[`${oid}___icon`]} + square + style={{ ...(style || undefined), marginLeft: 7 }} + value="..." + className={className} + onClick={() => { + const settings: Partial = {}; + (settings as Record)[`showSelectId${attr}`] = true; + this.setState(settings as TState); + }} + /> +
+ {iobObj ? ( +
+ {Utils.getObjectNameFromObj(iobObj, I18n.getLanguage())} +
+ ) : null} + {showSelectId ? ( + { + const settings: Partial = {}; + (settings as Record)[`showSelectId${attr}`] = false; + this.setState(settings as TState); + }} + onOk={(selected: string | string[] | undefined, _name: string): void => { + const settings: Partial = {}; + (settings as Record)[`showSelectId${attr}`] = false; + const oid = Array.isArray(selected) ? selected[0] : selected; + + this.setState(settings as TState, async () => { + // read type of object + const obj = oid ? await socket.getObject(oid) : undefined; this.lastObjectIdChange = Date.now(); - onChange({ - [attr]: selected, - [`${attr}Role`]: obj.common.role, - [`${attr}Type`]: obj.common.type, - [`${attr}Unit`]: obj.common.unit, - [`${attr}States`]: obj.common.states, - [`${attr}Min`]: obj.common.min, - [`${attr}Max`]: obj.common.max, - [`${attr}Step`]: obj.common.step, - [`${attr}Def`]: obj.common.def, - [`${attr}Write`]: obj.common.write, - [`${attr}Read`]: obj.common.read, - }, null, () => - this.props.setOnUpdate && this.props.setOnUpdate(true)); - })); - }} - /> : null} -
: null; + onChange( + { + [attr]: selected, + [`${attr}Role`]: obj?.common?.role, + [`${attr}Type`]: obj?.common?.type, + [`${attr}Unit`]: obj?.common?.unit, + [`${attr}States`]: obj?.common?.states, + [`${attr}Min`]: obj?.common?.min, + [`${attr}Max`]: obj?.common?.max, + [`${attr}Step`]: obj?.common?.step, + [`${attr}Def`]: obj?.common?.def, + [`${attr}Write`]: obj?.common?.write, + [`${attr}Read`]: obj?.common?.read, + }, + () => this.props.setOnUpdate && this.props.setOnUpdate(true), + ); + }); + }} + /> + ) : null} +
+ ) : null; }; - renderIconTag = () => { - return
{ - if (this.state.settings.tagCard) { - if (this.state.tagCardArray.length < 3) { - this.onChangeTag(); - } else { - this.setState({ openTagMenu: e.currentTarget }) + renderIconTag = (): React.JSX.Element => { + return ( +
{ + if (this.state.settings.tagCard) { + if (this.state.tagCardArray.length < 3) { + this.onChangeTag(); + } else { + this.setState({ openTagMenu: e.currentTarget }); + } } - } - }}> - {this.state.settings.tagCard} -
; + }} + > + {this.state.settings.tagCard} +
+ ); }; - renderTime = (input, value, onChange) => { - const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderTime = (input: RuleInputTime, value: string, onChange: (value: string) => void): React.JSX.Element => { + const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input; + return ( +
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ ); }; - renderSelect = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderSelect = ( + input: RuleInputSelect, + value: any, + onChange: (value: any, attr: string) => void, + ): React.JSX.Element => { const { className, style } = this.props; - const { name, options, frontText, backText, attr, multiple, doNotTranslate, doNotTranslate2, doNotTranslateBack } = input; - return
- {frontText &&
{I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + const { + name, + options, + frontText, + backText, + attr, + multiple, + doNotTranslate, + doNotTranslate2, + doNotTranslateBack, + } = input; + return ( +
+ {frontText &&
{I18n.t(frontText)}
} + + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ ); }; - renderInstance = (input, value, onChange) => { - const { className, socket } = this.props; - const { name, options, frontText, backText, attr, adapter, doNotTranslate, doNotTranslateBack } = input; + // eslint-disable-next-line react/no-unused-class-component-methods + renderInstance = ( + input: RuleInputInstance, + value: string, + onChange: (value: string) => void, + ): React.JSX.Element | null => { + const { socket } = this.props; + const { name, frontText, backText, attr, adapter, doNotTranslate, doNotTranslateBack } = input; if (this.state.hideAttributes.includes(attr)) { return null; } - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - this.setState({ hideAttributes: [...this.state.hideAttributes, attr] }, () => onChange(value))} // hide instance if only exactly one exists - /> - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + return ( +
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + { + onChange(Array.isArray(value) ? value[0] : value); + }} + customValue + onInstanceHide={value => + this.setState({ hideAttributes: [...this.state.hideAttributes, attr] }, () => onChange(value)) + } // hide instance if only exactly one exists + /> + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ ); }; - renderDialog = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderDialog = (input: RuleInputDialog): React.JSX.Element => { const { onShowDialog, frontText, backText, attr, icon, doNotTranslate, doNotTranslateBack } = input; - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - onShowDialog && onShowDialog()} - /> - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + return ( +
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + onShowDialog && onShowDialog()} + /> + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ ); }; - renderModalInput = (input, value, onChange) => { + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderModalInput = ( + input: RuleInputModalInput, + value: string | number, + onChange: (value: string | number) => void, + ): React.JSX.Element => { const { openModal } = this.state; const { className } = this.props; - const { attr, nameBlock, frontText, backText, noTextEdit, doNotTranslate, doNotTranslateBack} = input; - return
-
+ const { attr, nameBlock, frontText, backText, noTextEdit, doNotTranslate, doNotTranslateBack } = input; + return ( +
+
+ {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} + + this.setState({ openModal: true })} + /> + {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} +
+ {openModal ? ( + + this.setState( + { openModal: false }, + () => val !== null && val !== undefined && onChange(val), + ) + } + onClose={() => this.setState({ openModal: false })} + defaultValue={value} + textInput + /> + ) : null} + {nameBlock &&
{I18n.t(nameBlock)}
} +
+ ); + }; + + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + renderDate = (input: RuleInputDate, value: string, onChange: (value: string) => void): React.JSX.Element => { + const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input; + return ( +
{frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - this.setState({ openModal: true })} /> {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
}
- {openModal ? - this.setState({ openModal: false }, () => - val !== null && val !== undefined && onChange(val))} - onClose={() => this.setState({ openModal: false })} - defaultValue={value} - textInput - /> : null} - {nameBlock &&
{I18n.t(nameBlock)}
} -
; - }; - - renderDate = (input, value, onChange) => { - const { attr, backText, frontText, doNotTranslate, doNotTranslateBack } = input - return
- {frontText &&
{doNotTranslate ? frontText : I18n.t(frontText)}
} - - {backText &&
{doNotTranslateBack ? backText : I18n.t(backText)}
} -
; + ); }; - static getReplacesInText(context) { + static getReplacesInText(context: RuleContext): string { let value = ''; - if (context.trigger?.oidType) { - value = '.replace(/%s/g, obj.state.val).replace(/%id/g, obj.id).replace(/%name/g, obj.common && obj.common.name).replace(/%old/g, obj.oldState.val)'; + if ((context.trigger as RuleBlockConfigTriggerState)?.oidType) { + value = + '.replace(/%s/g, obj.state.val).replace(/%id/g, obj.id).replace(/%name/g, obj.common && obj.common.name).replace(/%old/g, obj.oldState.val)'; } else if (context.conditionsStates.length) { value = `.replace(/%s/g, ${context.conditionsStates[0].name}).replace(/%id/g, "${context.conditionsStates[0].id}")`; } @@ -565,71 +858,120 @@ class GenericBlock extends PureComponent { } ///////////////////////////// - renderTags = () => { - let { tagCardArray, openTagMenu } = this.state; - let { tagCard } = this.state.settings; - let result = tagCard !== '=' && tagCard !== '<>' && tagCard !== '>=' && tagCard !== '()' && tagCard !== '.' && tagCard !== '<=' && tagCard !== '<' && tagCard !== '>' ? I18n.t(tagCard) : tagCard; + renderTags(): React.JSX.Element | string | undefined { + const { tagCardArray, openTagMenu } = this.state; + const { tagCard } = this.state.settings; + let result: React.JSX.Element | string | undefined = + tagCard !== '=' && + tagCard !== '<>' && + tagCard !== '>=' && + tagCard !== '()' && + tagCard !== '.' && + tagCard !== '<=' && + tagCard !== '<' && + tagCard !== '>' && + tagCard + ? I18n.t(tagCard) + : tagCard; + if (tagCardArray.length >= 3) { - result =
-
{ - this.setState({ openTagMenu: e.currentTarget }, () => { - this.props.isTourOpen && - this.props.tourStep === STEPS.openTagsMenu && - setTimeout(() => this.props.setTourStep(STEPS.selectIntervalTag), 300); - }); - }}>{result}
- this.setState({ openTagMenu: null })} - > - {tagCardArray.map(el => { - let tag = el; - if (typeof el !== 'string') { - tag = el.title; - } - return ( - { - const settings = { ...this.state.settings, tagCard: tag }; - this.setState({ openTagMenu: null, settings }, () => { - this.props.onChange(settings); - this.onTagChange(tag); - }); - (this.props.isTourOpen && - (this.props.tourStep === STEPS.openTagsMenu || - this.props.tourStep === STEPS.selectIntervalTag) && - tag === 'interval' && - setTimeout(() => this.props.setTourStep(STEPS.selectActions), 500)); - - }}>{tag.search(/>|<|<>|<=|>=|=/) !== -1 ? tag : I18n.t(tag)}{typeof el !== 'string' && el.title2 &&
{I18n.t(el.title2)}
} -
- ); - })} -
-
; + result = ( +
+
{ + this.setState({ openTagMenu: e.currentTarget }, () => { + this.props.isTourOpen && + this.props.tourStep === STEPS.openTagsMenu && + setTimeout( + () => this.props.setTourStep && this.props.setTourStep(STEPS.selectIntervalTag), + 300, + ); + }); + }} + > + {result} +
+ this.setState({ openTagMenu: null })} + > + {tagCardArray.map((el, i) => { + let tag: RuleTagCardTitle; + if (typeof el !== 'string') { + tag = el.title; + } else { + tag = el; + } + return ( + { + const settings = { ...this.state.settings, tagCard: tag }; + this.setState({ openTagMenu: null, settings }, () => { + this.props.onChange(settings); + this.onTagChange(tag); + }); + this.props.isTourOpen && + (this.props.tourStep === STEPS.openTagsMenu || + this.props.tourStep === STEPS.selectIntervalTag) && + tag === 'interval' && + setTimeout( + () => + this.props.setTourStep && + this.props.setTourStep(STEPS.selectActions), + 500, + ); + }} + > + {tag.search(/>|<|<>|<=|>=|=/) !== -1 ? tag : I18n.t(tag)} + {typeof el !== 'string' && el.title2 && ( +
{I18n.t(el.title2)}
+ )} +
+ ); + })} +
+
+ ); } return result; - }; + } - onChangeTag = () => { - const { tagCardArray, settings, settings: { tagCard } } = this.state; - let newTagCardArray = [...tagCardArray] - if (typeof newTagCardArray[0] !== 'string') { - newTagCardArray = newTagCardArray.map(el => el.title); + // will be overwritten + // eslint-disable-next-line react/no-unused-class-component-methods,class-methods-use-this + getData(): RuleBlockDescription { + return { + acceptedBy: 'triggers', + name: '', + id: '', + }; + } + + onChangeTag = (): void => { + const { + tagCardArray, + settings, + settings: { tagCard }, + } = this.state; + let newTagCardArray: RuleTagCardTitle[]; + if (typeof tagCardArray[0] !== 'string') { + newTagCardArray = (tagCardArray as RuleTagCard[]).map(el => el.title); + } else { + newTagCardArray = [...(tagCardArray as RuleTagCardTitle[])]; } if (tagCard && newTagCardArray.length < 3) { const newSettings = { ...settings }; - const newTagCard = newTagCardArray[(newTagCardArray.indexOf(tagCard) + 1) % newTagCardArray.length] + const newTagCard = newTagCardArray[(newTagCardArray.indexOf(tagCard) + 1) % newTagCardArray.length]; newSettings.tagCard = newTagCard; this.setState({ settings: newSettings }, () => { this.props.onChange(newSettings); @@ -638,98 +980,329 @@ class GenericBlock extends PureComponent { } }; - componentDidMount = () => { + componentDidMount = (): void => { this.onTagChange(); // detect changes }; - componentDidUpdate = prevProps => { + componentDidUpdate(): void { if (this.props.acceptedBy !== 'triggers' && this.props.onUpdate) { setTimeout(() => this.onUpdate(), 0); } } - onChangeInput = attribute => { - return (value, attr, cb) => { + onChangeInput = (attribute: string): ((value: any, attr?: string | (() => void), cb?: () => void) => void) => { + return (value: any, attr?: string | (() => void), cb?: () => void): void => { const settings = JSON.parse(JSON.stringify(this.state.settings)); if (typeof value === 'object' && (!attr || typeof attr === 'function')) { - Object.keys(value).forEach(_attr => settings[_attr] = value[_attr]); + Object.keys(value).forEach(_attr => (settings[_attr] = value[_attr])); + if (typeof attr === 'function') { + cb = attr; + attr = undefined; + } } else { - settings[attr || attribute] = value; + settings[(attr as string) || attribute] = value; } + settings.id = this.getData().id; settings._id = this.props._id; this.setState({ settings }, () => { - this.onValueChanged(value, attr || attribute); + this.onValueChanged(value, (attr as string) || attribute); this.props.onChange(settings); cb && cb(); }); - } - } + }; + }; - renderSpecific() { + // eslint-disable-next-line class-methods-use-this + renderSpecific(): React.JSX.Element | null { return null; // it can be overloaded } - renderDebugInfo() { + // eslint-disable-next-line class-methods-use-this + renderDebug(_message?: any): React.JSX.Element | string { + return ''; + } + + renderDebugInfo(): React.JSX.Element | null { if (this.state.debugMessage) { - return
- {this.renderDebug ? this.renderDebug(this.state.debugMessage) : I18n.t('executed')} -
; - } else { - return null; + return ( +
+ {this.renderDebug ? this.renderDebug(this.state.debugMessage) : I18n.t('executed')} +
+ ); } + return null; + } + + // eslint-disable-next-line class-methods-use-this + renderCron( + _input: RuleInputCron, + _value: string, + _onChange: (value: string, attr?: string, cb?: () => void) => void, + ): React.JSX.Element | null { + return null; + } + + // eslint-disable-next-line class-methods-use-this + renderWizard( + _input: RuleInputWizard, + _value: string, + _onChange: (newData: Record | string) => void, + ): React.JSX.Element | null { + return null; + } + + // eslint-disable-next-line class-methods-use-this + renderWriteState(): React.JSX.Element[] | null { + return null; } - render = () => { - const { inputs, name, icon, iconTag, settings, adapter, settings: { tagCard }, helpDialog } = this.state; + renderInputElement(input: RuleInputAny, index: number): React.JSX.Element | React.JSX.Element[] | null { + const { nameRender, defaultValue, attr } = input as RuleInputAll; + const { settings } = this.state; + let value: any = attr ? (settings as Record)[attr] : undefined; + if (value === undefined) { + value = defaultValue; + } + + switch (nameRender) { + case 'renderTime': + if (attr) { + return this.renderTime(input as RuleInputTime, value as string, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderTime')}
; + + case 'renderNameText': + return this.renderNameText(input as RuleInputNameText, value); + + case 'renderSelect': + if (attr) { + return this.renderSelect(input as RuleInputSelect, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderSelect')}
; + case 'renderModalInput': + if (attr) { + return this.renderModalInput(input as RuleInputModalInput, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderModalInput')}
; + case 'renderObjectID': + if (attr) { + return this.renderObjectID(input as RuleInputObjectID, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderObjectID')}
; + case 'renderDialog': + if (attr) { + return this.renderDialog(input as RuleInputDialog); + } + return
{I18n.t('Invalid renderDialog')}
; + case 'renderInstance': + if (attr) { + return this.renderInstance(input as RuleInputInstance, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderInstance')}
; + case 'renderText': + if (attr) { + return this.renderText(input as RuleInputText, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderText')}
; + case 'renderSlider': + if (attr) { + return this.renderSlider(input as RuleInputSlider, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderSlider')}
; + case 'renderCheckbox': + if (attr) { + return this.renderCheckbox(input as RuleInputCheckbox, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderCheckbox')}
; + case 'renderButton': + if (attr) { + return this.renderButton(input as RuleInputButton, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderButton')}
; + case 'renderColor': + if (attr) { + return this.renderColor(input as RuleInputColor, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderColor')}
; + case 'renderSwitch': + if (attr) { + return this.renderSwitch(input as RuleInputSwitch, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderSwitch')}
; + case 'renderDate': + if (attr) { + return this.renderDate(input as RuleInputDate, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderDate')}
; + case 'renderCron': + if (attr) { + return this.renderCron(input as RuleInputCron, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderCron')}
; + case 'renderWizard': + if (attr) { + return this.renderWizard(input as RuleInputWizard, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderWizard')}
; + case 'renderWriteState': + return this.renderWriteState(); + case 'renderNumber': + if (attr) { + return this.renderNumber(input as RuleInputNumber, value, this.onChangeInput(attr)); + } + return
{I18n.t('Invalid renderNumber')}
; + default: + if (this[nameRender]) { + // @ts-expect-error ignore error as it is special case + return this[nameRender](input, value, attr ? this.onChangeInput(attr) : null); + } + return
{I18n.t('Invalid input type: %s', nameRender)}
; + } + } + + render(): React.JSX.Element { + const { + inputs, + name, + icon, + iconTag, + settings, + adapter, + settings: { tagCard }, + helpDialog, + } = this.state; const { socket, notFound } = this.props; - return - {iconTag ? this.renderIconTag() : - { - if (tagCard) { - if (this.state.tagCardArray.length < 3) { - this.onChangeTag(); - } else { - this.setState({ openTagMenu: e.currentTarget }) - } + // Detect changing of simulation + if (this.state.enableSimulation !== this.props.enableSimulation && !this.enableSimulationProcessing) { + this.enableSimulationProcessing = true; + setTimeout(() => { + this.setState({ enableSimulation: this.props.enableSimulation }, () => { + this.enableSimulationProcessing = false; + }); + }, 50); + } + + // Try to find latest message for this block + let debugMsg; + if (this.props.onDebugMessage) { + for (let d = this.props.onDebugMessage.length - 1; d >= 0; d--) { + const msg = this.props.onDebugMessage[d]; + if (msg.blockId === this.props._id && msg.ts > this.lastDebugMessage && msg.ts > Date.now() - 1000) { + debugMsg = msg; + break; + } + } + } + + if (debugMsg) { + // Get the last message + this.lastDebugMessage = debugMsg.ts; + if (this.debugMessageTimeout) { + clearTimeout(this.debugMessageTimeout); + } + if (this.debugHideTimeout) { + clearTimeout(this.debugHideTimeout); + this.debugHideTimeout = null; + } + this.debugMessageTimeout = setTimeout( + (debugMessageStr: string): void => { + const debugMessage: DebugMessage = JSON.parse(debugMessageStr); + const hideTimeout: number = debugMessage.hideTimeout || 5000; + this.debugMessageTimeout = null; + this.setState({ debugMessage }, () => { + if (this.debugHideTimeout) { + clearTimeout(this.debugHideTimeout); } - }} - />} -
- - {I18n.t(name)} - {!!notFound ? I18n.t(`%s not found`, settings.id) : ''} - {helpDialog ? this.setState({ helpText: I18n.t(helpDialog) })}> : null} - - {inputs.filter(({ nameRender }) => this[nameRender]) - .map(input => { - const { nameRender, defaultValue, attr, options } = input; - return this[nameRender]( - input, - settings[attr] !== undefined ? settings[attr] : defaultValue, - this.onChangeInput(attr), - options || [] - ); - })} -
- {tagCard &&
-
this.onChangeTag()} className={Utils.clsx(cls.tagCard, 'tag-card')}>{this.renderTags()}
-
} - {this.renderDebugInfo()} - {this.state.error ? this.setState({ error: '' })} /> : null} - {this.state.helpText ? this.setState({ helpText: '' })} /> : null} - {this.renderSpecific()} -
; - }; -} + this.debugHideTimeout = setTimeout(() => { + this.debugHideTimeout = null; + this.setState({ debugMessage: null }); + }, hideTimeout); + }); + }, + 50, + JSON.stringify(debugMsg), + ); + } -export default GenericBlock; + return ( + + {iconTag ? ( + this.renderIconTag() + ) : ( + { + if (tagCard) { + if (this.state.tagCardArray.length < 3) { + this.onChangeTag(); + } else { + this.setState({ openTagMenu: e.currentTarget }); + } + } + }} + /> + )} +
+ + {I18n.t(name)} + {notFound ? I18n.t(`%s not found`, settings.id) : ''} + {helpDialog ? ( + this.setState({ helpText: I18n.t(helpDialog) })} + > + + + ) : null} + + {inputs.map((input, index) => this.renderInputElement(input, index))} +
+ {tagCard && ( +
+
this.onChangeTag()} + className={Utils.clsx(cls.tagCard, 'tag-card')} + > + {this.renderTags()} +
+
+ )} + {this.renderDebugInfo()} + {this.state.error ? ( + this.setState({ error: '' })} + /> + ) : null} + {this.state.helpText ? ( + this.setState({ helpText: '' })} + /> + ) : null} + {this.renderSpecific()} +
+ ); + } +} diff --git a/src-editor/src/Components/RulesEditor/components/GenericBlock/style.module.scss b/src-editor/src/Components/RulesEditor/components/GenericBlock/style.module.scss index f51690fb..a617e6cf 100644 --- a/src-editor/src/Components/RulesEditor/components/GenericBlock/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/GenericBlock/style.module.scss @@ -2,17 +2,14 @@ 0% { opacity: 0; } - 1% { opacity: 0; } - 100% { opacity: 1; } } - .cardStyle { cursor: pointer; position: relative; @@ -26,7 +23,9 @@ align-items: center; background: var(--backgroundBlock); border-radius: 4px; - box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), + box-shadow: + 0 2px 1px -1px rgba(0, 0, 0, 0.2), + 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 1px 3px 0 rgba(0, 0, 0, 0.12); } .nameCard { @@ -83,7 +82,7 @@ margin: 5px auto; cursor: pointer; &:before { - content: "+"; + content: '+'; color: #f7060684; position: absolute; z-index: 2; @@ -95,7 +94,7 @@ transition: all 0.3s cubic-bezier(0.77, 0, 0.2, 0.85); } &:after { - content: ""; + content: ''; position: absolute; top: 0; left: 0; @@ -121,7 +120,10 @@ left: 0; width: 100%; overflow: hidden; - transition: opacity 0.5s, height 0.5s, top 0.5s; + transition: + opacity 0.5s, + height 0.5s, + top 0.5s; } .debugInfo { font-size: 12px; diff --git a/src-editor/src/Components/RulesEditor/components/HamburgerMenu/hamburgerMenu.module.scss b/src-editor/src/Components/RulesEditor/components/HamburgerMenu/hamburgerMenu.module.scss index 8436adfb..cf6bf88e 100644 --- a/src-editor/src/Components/RulesEditor/components/HamburgerMenu/hamburgerMenu.module.scss +++ b/src-editor/src/Components/RulesEditor/components/HamburgerMenu/hamburgerMenu.module.scss @@ -6,56 +6,64 @@ $bar-spacing: 7px; outline: 0; outline-offset: 0; margin-top: 12px; - cursor: pointer; + cursor: pointer; } .hamburgerMenu, .hamburgerMenu:after, .hamburgerMenu:before { - width: $bar-width; - height: $bar-height; + width: $bar-width; + height: $bar-height; } .hamburgerMenu { - position: relative; - transform: translateY($bar-spacing); - background: var(--lineColorActive); - transition: all 0ms 300ms; - - &.animate { - background: #dfbdec00; - } + position: relative; + transform: translateY($bar-spacing); + background: var(--lineColorActive); + transition: all 0ms 300ms; + + &.animate { + background: #dfbdec00; + } } .hamburgerMenu:before { - content: ""; - position: absolute; - left: 0; - bottom: $bar-spacing; - background: var(--lineColorActive); - transition: bottom 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms cubic-bezier(0.23, 1, 0.32, 1); + content: ''; + position: absolute; + left: 0; + bottom: $bar-spacing; + background: var(--lineColorActive); + transition: + bottom 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), + transform 300ms cubic-bezier(0.23, 1, 0.32, 1); } .hamburgerMenu:after { - content: ""; - position: absolute; - left: 0; - top: $bar-spacing; - background: var(--lineColorActive); - transition: top 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms cubic-bezier(0.23, 1, 0.32, 1); + content: ''; + position: absolute; + left: 0; + top: $bar-spacing; + background: var(--lineColorActive); + transition: + top 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1), + transform 300ms cubic-bezier(0.23, 1, 0.32, 1); } .hamburgerMenu.animate:after { - top: 0; - transform: rotate(45deg); - transition: top 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1);; + top: 0; + transform: rotate(45deg); + transition: + top 300ms cubic-bezier(0.23, 1, 0.32, 1), + transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1); } .hamburgerMenu.animate:before { - bottom: 0; - transform: rotate(-45deg); - transition: bottom 300ms cubic-bezier(0.23, 1, 0.32, 1), transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1);; + bottom: 0; + transform: rotate(-45deg); + transition: + bottom 300ms cubic-bezier(0.23, 1, 0.32, 1), + transform 300ms 300ms cubic-bezier(0.23, 1, 0.32, 1); } -.menu_conatiner_wrapper{ +.menu_conatiner_wrapper { display: none; } diff --git a/src-editor/src/Components/RulesEditor/components/HamburgerMenu/index.tsx b/src-editor/src/Components/RulesEditor/components/HamburgerMenu/index.tsx index 8a9ac013..80b0de75 100644 --- a/src-editor/src/Components/RulesEditor/components/HamburgerMenu/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/HamburgerMenu/index.tsx @@ -1,18 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; import cls from './hamburgerMenu.module.scss'; -const HamburgerMenu = ({ boolean }) => { - return
+interface HamburgerMenuProps { + bool: boolean; } -HamburgerMenu.defaultProps = { - boolean: false -}; - -HamburgerMenu.propTypes = { - boolean: PropTypes.bool -}; +function HamburgerMenu({ bool }: HamburgerMenuProps): React.JSX.Element { + return
; +} export default HamburgerMenu; diff --git a/src-editor/src/Components/RulesEditor/components/Menu/index.tsx b/src-editor/src/Components/RulesEditor/components/Menu/index.tsx index b31e24b0..e1f228ea 100644 --- a/src-editor/src/Components/RulesEditor/components/Menu/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/Menu/index.tsx @@ -1,5 +1,4 @@ import React, { Fragment, useContext, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { AppBar, ClickAwayListener, Tab, Tabs } from '@mui/material'; @@ -14,137 +13,197 @@ import { useStateLocal } from '../../hooks/useStateLocal'; import { ContextWrapperCreate } from '../ContextWrapper'; import MaterialDynamicIcon from '../../helpers/MaterialDynamicIcon'; import { STEPS } from '../../helpers/Tour'; +import type { RuleBlockDescription, RuleBlockType, RuleUserRules } from '@/Components/RulesEditor/types'; +import type { GenericBlock } from '@/Components/RulesEditor/components/GenericBlock'; -const Menu = ({ addClass, setAllBlocks, allBlocks, userRules, onChangeBlocks, setTourStep, tourStep, isTourOpen }) => { - // eslint-disable-next-line no-unused-vars - const { blocks, socket } = useContext(ContextWrapperCreate); - const [hamburgerOnOff, setHamburgerOnOff] = useStateLocal(false, 'hamburgerOnOff'); - const [filter, setFilter] = useStateLocal({ - text: '', - type: 'triggers', - index: 0 - }, 'filterControlPanel'); +interface MenuProps { + addClass: Record; + setAllBlocks: (blocks: (typeof GenericBlock)[]) => void; + allBlocks: (typeof GenericBlock)[]; + userRules: RuleUserRules; + onChangeBlocks: (newRules: RuleUserRules) => void; + setTourStep: (step: number) => void; + tourStep: number; + isTourOpen: boolean; +} - const handleChange = (event, newValue) => { - isTourOpen && (newValue === 0 && tourStep === STEPS.selectTriggers) && setTourStep(STEPS.addScheduleByDoubleClick); - isTourOpen && (newValue === 2 && tourStep === STEPS.selectActions) && setTourStep(STEPS.addActionPrintText); - setFilter({ - ...filter, - index: newValue, - type: ['triggers', 'conditions', 'actions'][newValue] - }); - setBlocksFunc(filter.text, ['triggers', 'conditions', 'actions'][newValue]); - }; +const Menu = ({ + addClass, + setAllBlocks, + allBlocks, + userRules, + onChangeBlocks, + setTourStep, + tourStep, + isTourOpen, +}: MenuProps): React.JSX.Element => { + const { blocks, socket } = useContext(ContextWrapperCreate); + const [hamburgerOnOff, setHamburgerOnOff] = useStateLocal(false, 'hamburgerOnOff'); + const [filter, setFilter] = useStateLocal<{ + text: string; + type: RuleBlockType; + index: number; + }>( + { + text: '', + type: 'triggers', + index: 0, + }, + 'filterControlPanel', + ); - const setBlocksFunc = (text = filter.text, typeFunc = filter.type) => { + const setBlocksFunc = (text = filter.text, typeFunc = filter.type): void => { if (!blocks) { return; } - let newAllBlocks = [...blocks]; + let newAllBlocks: (typeof GenericBlock)[] = [...blocks]; newAllBlocks = newAllBlocks.filter(el => { if (!text) { return true; } - const { name } = el.getStaticData(); + const { name }: RuleBlockDescription = el.getStaticData(); return name && I18n.t(name).toLowerCase().includes(text.toLowerCase()); }); newAllBlocks = newAllBlocks.filter(el => typeFunc === el.getStaticData().acceptedBy); setAllBlocks(newAllBlocks); }; - const a11yProps = index => ({ + const handleChange = (_event: React.SyntheticEvent, newValue: number): void => { + if (isTourOpen && newValue === 0 && tourStep === STEPS.selectTriggers) { + setTourStep(STEPS.addScheduleByDoubleClick); + } + + if (isTourOpen && newValue === 2 && tourStep === STEPS.selectActions) { + setTourStep(STEPS.addActionPrintText); + } + + setFilter({ + ...filter, + index: newValue, + type: (['triggers', 'conditions', 'actions'] as RuleBlockType[])[newValue], + }); + + setBlocksFunc(filter.text, (['triggers', 'conditions', 'actions'] as RuleBlockType[])[newValue]); + }; + + const a11yProps = (index: number): { id: string; 'aria-controls': string } => ({ id: `scrollable-force-tab-${index}`, - 'aria-controls': `scrollable-force-tabpanel-${index}` + 'aria-controls': `scrollable-force-tabpanel-${index}`, }); useEffect(() => { setBlocksFunc(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [blocks]); - return setHamburgerOnOff(true)} - > -
-
setHamburgerOnOff(!hamburgerOnOff)}> -
-
-
- - - } - {...a11yProps(0)} /> - } - {...a11yProps(1)} /> - } - {...a11yProps(2)} /> - - + + return ( + setHamburgerOnOff(true)} + > +
+
setHamburgerOnOff(!hamburgerOnOff)} + > +
-
- - {allBlocks.map(el => { - const { name, id, icon, adapter } = el.getStaticData(); - return - setHamburgerOnOff(true)} - setTourStep={setTourStep} - tourStep={tourStep} - isTourOpen={isTourOpen} - allProperties={el.getStaticData()} - name={name} - icon={icon} - adapter={adapter} - socket={socket} - userRules={userRules} - setUserRules={onChangeBlocks} - isActive={false} - id={id} +
+
+ + + } + {...a11yProps(0)} + /> + } + {...a11yProps(1)} /> - ; - })} - {allBlocks.length === 0 &&
- {I18n.t('Nothing found')}... -
{ - setFilter({ - ...filter, - text: '' - }); - setBlocksFunc(''); - }}>{I18n.t('reset search')}
-
} - + } + {...a11yProps(2)} + /> +
+
+
+
+ + {allBlocks.map(el => { + const { name, id, icon, adapter } = el.getStaticData(); + return ( + + setHamburgerOnOff(true)} + setTourStep={setTourStep} + setUserRules={onChangeBlocks} + socket={socket} + tourStep={tourStep} + userRules={userRules} + /> + + ); + })} + {!allBlocks.length && ( +
+ {I18n.t('Nothing found')}... +
{ + setFilter({ + ...filter, + text: '', + }); + setBlocksFunc(''); + }} + > + {I18n.t('reset search')} +
+
+ )} +
+
+
+ { + setFilter({ ...filter, text: value as string }); + setBlocksFunc(value as string); + }} + />
-
- { - setFilter({ ...filter, text: value }); - setBlocksFunc(value); - }} - />
-
- ; -} - -Menu.propTypes = { - onChange: PropTypes.func, - code: PropTypes.string + + ); }; export default Menu; diff --git a/src-editor/src/Components/RulesEditor/components/Menu/style.module.scss b/src-editor/src/Components/RulesEditor/components/Menu/style.module.scss index 36a288be..4b1f79d1 100644 --- a/src-editor/src/Components/RulesEditor/components/Menu/style.module.scss +++ b/src-editor/src/Components/RulesEditor/components/Menu/style.module.scss @@ -7,7 +7,12 @@ padding: 10px; opacity: 1; overflow-x: hidden; - transition: width 0.5s, opacity 0.2s, padding 0.5s, background 0.2s, border-right 0.2s; + transition: + width 0.5s, + opacity 0.2s, + padding 0.5s, + background 0.2s, + border-right 0.2s; } .switchesRenderWrapper { overflow-x: hidden; @@ -31,7 +36,7 @@ transition: color 0.2s; &::after, &::before { - content: ""; + content: ''; flex: 1; border-bottom: 1px solid; } @@ -61,7 +66,13 @@ top: 30px; border: 1px solid var(--lineColor); cursor: pointer; - transition: left 0.5s, border-radius 0.7s, width 0.7s, height 0.7s, background 0.2s, border 0.2s; + transition: + left 0.5s, + border-radius 0.7s, + width 0.7s, + height 0.7s, + background 0.2s, + border 0.2s; z-index: 10; } .hamburgerOff { @@ -82,7 +93,9 @@ color: var(--lineColor); cursor: pointer; margin: 10px 0; - transition: color 0.3s, font-size 0.3s; + transition: + color 0.3s, + font-size 0.3s; &:hover { color: var(--lineColorHover); font-size: 22px; @@ -102,13 +115,13 @@ background: none; box-shadow: none; } - [class*="Mui-selected"] { + [class*='Mui-selected'] { color: var(--lineColorActive) !important; } - [class*="MuiTabs-indicator"] { + [class*='MuiTabs-indicator'] { background-color: var(--lineColorActive); } - [class*="Mui-disabled"] { + [class*='Mui-disabled'] { color: #210025cc !important; } } diff --git a/src-editor/src/Components/RulesEditor/components/StandardBlocks/index.tsx b/src-editor/src/Components/RulesEditor/components/StandardBlocks/index.tsx index b89b2a6b..7d3fca06 100644 --- a/src-editor/src/Components/RulesEditor/components/StandardBlocks/index.tsx +++ b/src-editor/src/Components/RulesEditor/components/StandardBlocks/index.tsx @@ -12,8 +12,9 @@ import ActionPause from '../Blocks/ActionPause'; import ActionFunction from '../Blocks/ActionFunction'; import ActionSetStateDelayed from '../Blocks/ActionSetStateDelayed'; import ActionOperateStates from '../Blocks/ActionOperateStates'; +import type { GenericBlock } from '@/Components/RulesEditor/components/GenericBlock'; -const StandardBlocks = [ +const StandardBlocks: (typeof GenericBlock)[] = [ TriggerSchedule, TriggerScriptSave, TriggerState, @@ -30,4 +31,4 @@ const StandardBlocks = [ ActionOperateStates, ]; -export default StandardBlocks; \ No newline at end of file +export default StandardBlocks; diff --git a/src-editor/src/Components/RulesEditor/helpers/Compile.tsx b/src-editor/src/Components/RulesEditor/helpers/Compile.tsx index f90143c2..10e5fec1 100644 --- a/src-editor/src/Components/RulesEditor/helpers/Compile.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/Compile.tsx @@ -1,6 +1,7 @@ -// eslint-disable-next-line no-unused-vars +import type { RuleBlockConfig, RuleContext, RuleUserConditionsSaved, RuleUserRules } from '../types'; +import type { GenericBlock } from '../components/GenericBlock'; -const STANDARD_FUNCTION_STATE = `async function (obj) { +export const STANDARD_FUNCTION_STATE = `async function (obj) { "__%%DEBUG_TRIGGER%%__"; __%%CONDITIONS_VARS%%__ const _cond = __%%CONDITION%%__; @@ -13,7 +14,7 @@ __%%THEN%%__ __%%ELSE%%__ } }`; -const STANDARD_FUNCTION_STATE_ONCHANGE = `async function (obj) { +export const STANDARD_FUNCTION_STATE_ONCHANGE = `async function (obj) { "__%%DEBUG_TRIGGER%%__"; __%%CONDITIONS_VARS%%__ const _cond = __%%CONDITION%%__; @@ -28,8 +29,7 @@ __%%THEN%%__ __%%ELSE%%__ } }`; -const STANDARD_FUNCTION = -`async function () { +export const STANDARD_FUNCTION = `async function () { "__%%DEBUG_TRIGGER%%__"; __%%CONDITIONS_VARS%%__ const _cond = __%%CONDITION%%__; @@ -43,8 +43,7 @@ __%%ELSE%%__ } }`; -const STANDARD_FUNCTION_ONCHANGE = -`async function () { +export const STANDARD_FUNCTION_ONCHANGE = `async function () { "__%%DEBUG_TRIGGER%%__"; __%%CONDITIONS_VARS%%__ const _cond = __%%CONDITION%%__; @@ -60,7 +59,7 @@ __%%ELSE%%__ } }`; -const NO_FUNCTION = `"__%%DEBUG_TRIGGER%%__"; +export const NO_FUNCTION = `"__%%DEBUG_TRIGGER%%__"; __%%CONDITIONS_VARS%%__ const _cond = __%%CONDITION%%__; @@ -72,34 +71,38 @@ __%%THEN%%__ __%%ELSE%%__ }`; -const DEFAULT_RULE = { +const DEFAULT_RULE: RuleUserRules = { triggers: [], conditions: [[]], justCheck: false, actions: { then: [], - 'else': [] - } + else: [], + }, }; -function compileTriggers(json, context, blocks) { - const triggers = []; +function compileTriggers( + json: RuleUserRules, + _context: RuleContext | null, + blocks: (typeof GenericBlock)[], +): string { + const triggers: string[] = []; let jsonTriggers = json.triggers; if (!jsonTriggers.length) { - jsonTriggers = [{id: 'TriggerScriptSave'}]; + jsonTriggers = [{ id: 'TriggerScriptSave' } as RuleBlockConfig]; } - const vars = []; - let prelines = []; - let hist = json.conditions.find(conds => conds.find(cond => cond.tagCard === '()')); + const vars: string[] = []; + const prelines: string[] = []; + const hist = json.conditions.find(conds => conds.find(cond => cond.tagCard === '()')); jsonTriggers.forEach((trigger, i) => { const found = findBlock(trigger.id, blocks); if (found) { - const _context = { + const _context: RuleContext = { trigger, - condition: {}, - justCheck: hist ? false : (json.justCheck || (!json.conditions.length || !json.conditions[0].length)), + condition: { index: 0 }, + justCheck: hist ? false : json.justCheck || !json.conditions.length || !json.conditions[0].length, conditionsDebug: [], conditionsVars: [], conditionsStates: [], @@ -112,13 +115,13 @@ function compileTriggers(json, context, blocks) { // find indent vars.push(`cond${i}`); - if (_context.prelines && _context.prelines.length) { + if (_context.prelines?.length) { _context.prelines.forEach(line => prelines.push(line)); } if (text.includes(' __%%CONDITIONS_VARS%%__')) { - _context.conditionsVars = _context.conditionsVars.map((v, i) => i ? ` ${v}` : v); - _context.conditionsDebug = _context.conditionsDebug.map((v, i) => i ? ` ${v}` : v); + _context.conditionsVars = _context.conditionsVars.map((v, i) => (i ? ` ${v}` : v)); + _context.conditionsDebug = _context.conditionsDebug.map((v, i) => (i ? ` ${v}` : v)); } triggers.push( @@ -128,7 +131,7 @@ function compileTriggers(json, context, blocks) { .replace('__%%CONDITION%%__', conditions) .replace('__%%THEN%%__', then || '// ignore') .replace('__%%ELSE%%__', _else || '// ignore') - .replace(/__%%STATE%%__/g, 'cond' + i) + .replace(/__%%STATE%%__/g, `cond${i}`), ); } }); @@ -145,99 +148,82 @@ function compileTriggers(json, context, blocks) { return text; } -function findBlock(type, blocks) { +function findBlock(type: string, blocks: (typeof GenericBlock)[]): typeof GenericBlock | undefined { return blocks.find(block => block.getStaticData && block.getStaticData().id === type); } -function compileActions(actions, context, blocks) { - let result = []; - actions && actions.forEach(action => { +function compileActions( + actions: RuleBlockConfig[], + context: RuleContext, + blocks: (typeof GenericBlock)[], +): string { + const result: string[] = []; + actions?.forEach(action => { const found = findBlock(action.id, blocks); if (found) { result.push(found.compile(action, context)); } }); - return `\t\t${result.join('\n\n\t\t')}` || ''; + return `\t\t${result.join('\n\n\t\t')}`; } -function compileConditions(conditions, context, blocks) { - let result = []; +function compileConditions( + conditions: RuleUserConditionsSaved, + context: RuleContext, + blocks: (typeof GenericBlock)[], +): string { + const result: string[] = []; let i = 0; - conditions && conditions.forEach(ors => { - if (ors.hasOwnProperty('length') && ors.length) { - const _ors = []; - _ors && ors.forEach(block => { - const found = findBlock(block.id, blocks); - if (found) { - context.condition.index = i++; - _ors.push(found.compile(block, context)); - } - }); - result.push(`(${_ors.join(') &&\n (')})`); - } else { - const found = findBlock(ors.id, blocks); + conditions?.forEach(ors => { + const _ors: string[] = []; + ors?.forEach(block => { + const found = findBlock(block.id, blocks); if (found) { context.condition.index = i++; - result.push(found.compile(ors, context)); + _ors.push(found.compile(block, context)); } - } + }); + result.push(`(${_ors.join(') &&\n (')})`); }); if (!result.length) { return 'true'; - } else + } if (result.length === 1) { return result[0] || 'true'; - } else { - return `(${result.join(') || (')})`; } + return `(${result.join(') || (')})`; } -function compile(json, blocks) { +export function compile(json: RuleUserRules, blocks: (typeof GenericBlock)[]): string { return compileTriggers(json, null, blocks); } // eslint-disable-next-line no-unused-vars -function code2json(code) { +export function code2json(code: string): RuleUserRules { if (!code) { return DEFAULT_RULE; - } else { - const lines = code.split('\n'); - try { - let json = lines.pop().replace(/^\/\//, ''); - json = JSON.parse(json); - if (!json.triggers) { - json = DEFAULT_RULE; - } - return json; - } catch (e) { - return DEFAULT_RULE; + } + const lines = code.split('\n'); + try { + const jsonStr = (lines.pop() || '').replace(/^\/\//, ''); + let json: RuleUserRules = JSON.parse(jsonStr); + if (!json.triggers) { + json = DEFAULT_RULE; } + return json; + } catch { + return DEFAULT_RULE; } } -// eslint-disable-next-line no-unused-vars -function json2code(json, blocks) { +export function json2code(json: RuleUserRules, blocks: (typeof GenericBlock)[]): string { let code = ''; const compiled = compile(json, blocks); code += compiled; - code += `\n/*\nconst demo = ${JSON.stringify(json, null, 2) - .replace(/\*\//g, '* /')};\n*/\n`; + code += `\n/*\nconst demo = ${JSON.stringify(json, null, 2).replace(/\*\//g, '* /')};\n*/\n`; return `${code}\n//${JSON.stringify(json)}`; } - -const Compile = { - code2json, - json2code, - compile, - STANDARD_FUNCTION, - STANDARD_FUNCTION_ONCHANGE, - STANDARD_FUNCTION_STATE, - STANDARD_FUNCTION_STATE_ONCHANGE, - NO_FUNCTION, -}; - -export default Compile; \ No newline at end of file diff --git a/src-editor/src/Components/RulesEditor/helpers/MaterialDynamicIcon.tsx b/src-editor/src/Components/RulesEditor/helpers/MaterialDynamicIcon.tsx index fc58cafd..f5edd4b0 100644 --- a/src-editor/src/Components/RulesEditor/helpers/MaterialDynamicIcon.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/MaterialDynamicIcon.tsx @@ -1,39 +1,93 @@ -import { useState, useEffect } from 'react'; -import * as Icons from '@mui/icons-material/'; +import React, { useState, useEffect } from 'react'; +import { + Shuffle, + Apps, + Functions, + Language, + AddBox, + Pause, + Subject, + PlayForWork, + Brightness3, + HelpOutline, + Storage, + AccessTime, + PlayArrow, + FlashOn, + Help, + type SvgIconComponent, +} from '@mui/icons-material'; +import type { AdminConnection } from '@iobroker/adapter-react-v5'; -const ICON_CACHE = {}; +const ICON_CACHE: Record> = {}; -const MaterialDynamicIcon = ({ iconName, style, adapter, socket, onClick, className }) => { - let [url, setUrl] = useState(''); +const objIcon: Record = { + Shuffle, + Apps, + Functions, + Language, + AddBox, + Pause, + Subject, + PlayForWork, + Brightness3, + HelpOutline, + Storage, + AccessTime, + PlayArrow, + FlashOn, +}; + +interface MaterialDynamicIconProps { + iconName: string | undefined; + className?: string; + adapter?: string; + socket?: AdminConnection | null; + onClick?: (e: React.MouseEvent) => void; + style?: React.CSSProperties; +} + +function MaterialDynamicIcon({ + iconName, + className, + adapter, + socket, + onClick, + style, +}: MaterialDynamicIconProps): React.JSX.Element { + const [url, setUrl] = useState(''); useEffect(() => { if (adapter && socket) { - ICON_CACHE[adapter] = ICON_CACHE[adapter] || socket.getObject(`system.adapter.${adapter}`); - ICON_CACHE[adapter].then(obj => - obj?.common?.icon && setUrl(`../../adapter/${adapter}/${obj.common.icon}`)); + if (!(ICON_CACHE[adapter] instanceof Promise)) { + ICON_CACHE[adapter] = socket.getObject(`system.adapter.${adapter}`); + } + void ICON_CACHE[adapter].then( + obj => obj?.common?.icon && setUrl(`../../adapter/${adapter}/${obj.common.icon}`), + ); } }, [adapter, socket]); if (adapter) { - return onClick && onClick(e)} - src={url || ''} - style={style} - className={className} - alt="" - />; + return ( + onClick && onClick(e)} + src={url || ''} + className={className} + style={style} + alt="" + /> + ); } + const Element = (iconName && objIcon[iconName]) || Help; - const Element = Icons[iconName || 'Help']; - return onClick && onClick(e)} - />; + return ( + onClick && onClick(e)} + /> + ); } -MaterialDynamicIcon.defaultProps = { - style: null, - iconName: 'Help' -}; - export default MaterialDynamicIcon; diff --git a/src-editor/src/Components/RulesEditor/helpers/Tour.tsx b/src-editor/src/Components/RulesEditor/helpers/Tour.tsx index b37322f8..b99f208d 100644 --- a/src-editor/src/Components/RulesEditor/helpers/Tour.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/Tour.tsx @@ -10,45 +10,54 @@ const STEPS = { saveTheScript: 8, }; -const steps = [ - { // 0 +const steps: { selector: string; content: string }[] = [ + { + // 0 selector: '.blocks-triggers', content: 'Select triggers', }, - { // 1 + { + // 1 selector: '.block-TriggerScheduleBlock', content: 'Double click to add the block', }, - { // 2 + { + // 2 selector: '.tag-card', content: 'Open drop down menu', }, - { // 3 + { + // 3 selector: '.tag-card-interval', content: 'Select interval', }, - { // 4 + { + // 4 selector: '.blocks-actions', content: 'Select action blocks', }, - { // 5 + { + // 5 selector: '.block-ActionPrintText', content: 'Double click to add the block', }, - { // 6 + { + // 6 selector: '.button-js-code', content: 'Check the script', }, - { // 7 + { + // 7 selector: '.button-js-code', content: 'Switch back to rules', }, - { // 8 + { + // 8 selector: '.button-save', content: 'Save the script', - } + }, ]; -export {STEPS}; +export { STEPS }; -export default steps; \ No newline at end of file +export default steps; diff --git a/src-editor/src/Components/RulesEditor/helpers/cardSort.tsx b/src-editor/src/Components/RulesEditor/helpers/cardSort.tsx index edb2113c..bd305eb2 100644 --- a/src-editor/src/Components/RulesEditor/helpers/cardSort.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/cardSort.tsx @@ -1,20 +1,28 @@ -import _ from "lodash"; +import _ from 'lodash'; +import type { BlockValue, RuleBlockConfig, RuleBlockType, RuleUserRules } from '../types'; -const funcSet = _.throttle( - (setCards, userRules) => setCards(userRules) - , 0); +const funcSet = _.throttle((setCards, userRules) => setCards(userRules), 0); -const moveCard = ( - id, - atIndex, - cards, - setCards, - userRules, - acceptedBy, - additionally, - hoverClientY, - hoverMiddleY) => { +export function findCard(id: number, cards: RuleBlockConfig[]): { card: RuleBlockConfig | undefined; index: number } { + const card = cards.find(c => c._id === id); + return { + card, + index: card ? cards.indexOf(card) : -1, + }; +} + +export function moveCard( + id: number, + atIndex: number, + cards: RuleBlockConfig[], + setCards: (newRules: RuleUserRules) => void, + userRules: RuleUserRules, + acceptedBy: RuleBlockType, + additionally: BlockValue, + hoverClientY: number, + hoverMiddleY: number, +): void { const { card, index } = findCard(id, cards); if (index < atIndex && hoverClientY < hoverMiddleY) { return; @@ -26,29 +34,22 @@ const moveCard = ( const copyCard = _.clone(cards); copyCard.splice(index, 1); copyCard.splice(atIndex, 0, card); - const newTriggers = _.clone(userRules); + const newUserRules = _.clone(userRules); switch (acceptedBy) { case 'actions': - newTriggers[acceptedBy][additionally] = copyCard; - funcSet(setCards, newTriggers); + newUserRules.actions[additionally as 'else' | 'then'] = copyCard; + funcSet(setCards, newUserRules); return; + case 'conditions': - newTriggers[acceptedBy][additionally] = copyCard; - funcSet(setCards, newTriggers); + newUserRules.conditions[additionally as number] = copyCard; + funcSet(setCards, newUserRules); return; + default: - newTriggers[acceptedBy] = copyCard; - funcSet(setCards, newTriggers); + newUserRules.triggers = copyCard; + funcSet(setCards, newUserRules); return; } } -}; -const findCard = (id, cards) => { - const card = cards.find((c) => c._id === id); - return { - card, - index: cards.indexOf(card), - }; -}; - -export { moveCard, findCard }; \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/helpers/deepCopy.tsx b/src-editor/src/Components/RulesEditor/helpers/deepCopy.tsx index 43f8677d..d743fb2e 100644 --- a/src-editor/src/Components/RulesEditor/helpers/deepCopy.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/deepCopy.tsx @@ -1,21 +1,53 @@ -export function deepCopy(name, userRules, additionalParameter) { - let newItemsSwitches; +import type { BlockValue, RuleBlockType, RuleUserRules } from '../types'; + +export function deepCopy( + name: RuleBlockType, + userRules: RuleUserRules, + additionalParameter?: BlockValue, +): RuleUserRules { + let newItemsSwitches: RuleUserRules; + switch (name) { case 'actions': + if (additionalParameter === 'else') { + newItemsSwitches = { + ...userRules, + actions: { + ...userRules[name], + else: [...userRules[name].else], + }, + }; + return newItemsSwitches; + } + if (additionalParameter === 'then') { + newItemsSwitches = { + ...userRules, + actions: { + ...userRules[name], + then: [...userRules[name].then], + }, + }; + return newItemsSwitches; + } + + console.error(`Unknown additionalParameter: ${additionalParameter}`); + throw new Error(`Unknown additionalParameter: ${additionalParameter}`); + + case 'triggers': newItemsSwitches = { ...userRules, - [name]: { - ...userRules[name], - [additionalParameter]: [...userRules[name][additionalParameter]] - } + triggers: [...userRules.triggers], }; return newItemsSwitches; - default: + case 'conditions': newItemsSwitches = { ...userRules, - [name]: [...userRules[name]] + conditions: [...userRules.conditions], }; return newItemsSwitches; + + default: + throw new Error(`Unknown name: ${name}`); } -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/helpers/filterElement.tsx b/src-editor/src/Components/RulesEditor/helpers/filterElement.tsx index 9cb51750..e465eace 100644 --- a/src-editor/src/Components/RulesEditor/helpers/filterElement.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/filterElement.tsx @@ -1,13 +1,27 @@ -export function filterElement(name, userRules, additionalParameter, _id) { +import type { BlockValue, RuleUserRules } from '@/Components/RulesEditor/types'; + +export function filterElement( + name: string, + userRules: RuleUserRules, + additionalParameter: BlockValue, + _id: number, +): RuleUserRules { switch (name) { case 'actions': - userRules[name][additionalParameter] = userRules[name][additionalParameter].filter(el => el._id !== _id); + userRules.actions[additionalParameter as 'then' | 'else'] = userRules.actions[ + additionalParameter as 'then' | 'else' + ].filter(el => el._id !== _id); return userRules; + case 'conditions': - userRules[name][additionalParameter] = userRules[name][additionalParameter].filter(el => el._id !== _id); + userRules.conditions[additionalParameter as number] = userRules.conditions[ + additionalParameter as number + ]?.filter(el => el._id !== _id); return userRules; + + case 'triggers': default: - userRules[name] = userRules[name].filter(el => el._id !== _id); + userRules.triggers = userRules.triggers.filter(el => el._id !== _id); return userRules; } -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/helpers/findElement.tsx b/src-editor/src/Components/RulesEditor/helpers/findElement.tsx index eb1ced7a..a81199ae 100644 --- a/src-editor/src/Components/RulesEditor/helpers/findElement.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/findElement.tsx @@ -1,36 +1,45 @@ -export function findElement(settings, userRules, additionalParameter) { +import type { BlockValue, RuleBlockConfig, RuleUserRules } from '@/Components/RulesEditor/types'; + +export function findElement( + settings: RuleBlockConfig, + userRules: RuleUserRules, + additionalParameter: BlockValue, +): RuleUserRules { const { _id, acceptedBy } = settings; let block; if (!acceptedBy || !userRules[acceptedBy]) { - console.warn('Cannot find ' + acceptedBy); + console.warn(`Cannot find ${acceptedBy}`); return userRules; } switch (acceptedBy) { case 'actions': - block = userRules[acceptedBy][additionalParameter].find(el => el._id === _id); + block = userRules.actions[additionalParameter as 'else' | 'then'].find(el => el._id === _id); if (!block) { - console.warn('Cannot find ' + _id); + console.warn(`Cannot find ${_id}`); } else { - userRules[acceptedBy][additionalParameter][userRules[acceptedBy][additionalParameter].indexOf(block)] = settings; + const pos = userRules.actions[additionalParameter as 'else' | 'then'].indexOf(block); + userRules.actions[additionalParameter as 'else' | 'then'][pos] = settings; } return userRules; case 'conditions': - block = userRules[acceptedBy][additionalParameter].find(el => el._id === _id); + block = userRules.conditions[additionalParameter as number].find(el => el._id === _id); if (!block) { - console.warn('Cannot find ' + _id); + console.warn(`Cannot find ${_id}`); } else { - userRules[acceptedBy][additionalParameter][userRules[acceptedBy][additionalParameter].indexOf(block)] = settings; + const pos = userRules.conditions[additionalParameter as number].indexOf(block); + userRules.conditions[additionalParameter as number][pos] = settings; } return userRules; default: - block = userRules[acceptedBy].find(el => el._id === _id); + block = userRules.triggers.find(el => el._id === _id); if (!block) { - console.warn('Cannot find ' + _id); + console.warn(`Cannot find ${_id}`); } else { - userRules[acceptedBy][userRules[acceptedBy].indexOf(block)] = settings; + const pos = userRules.triggers.indexOf(block); + userRules.triggers[pos] = settings; } return userRules; } -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/helpers/stylesVariables.scss b/src-editor/src/Components/RulesEditor/helpers/stylesVariables.scss index 8dd5f6d2..b19d75ab 100644 --- a/src-editor/src/Components/RulesEditor/helpers/stylesVariables.scss +++ b/src-editor/src/Components/RulesEditor/helpers/stylesVariables.scss @@ -10,7 +10,7 @@ $themeStandard: ( --backgroundBlock: #c9e7ffab, --backgroundGlobalColor: #ffffff00, --backgroundMobile: #041c35d4, - --debugColor: #c6511b + --debugColor: #c6511b, ); $themeGreen: ( --backgroundColor: #3081333b, @@ -24,7 +24,7 @@ $themeGreen: ( --backgroundBlock: #c9ffcfab, --backgroundGlobalColor: #51ff001b, --backgroundMobile: #3081333b, - --debugColor: #c6511b + --debugColor: #c6511b, ); $themeSilver: ( --backgroundColor: rgba(31, 31, 31, 0.23), @@ -38,7 +38,7 @@ $themeSilver: ( --backgroundBlock: #c1c1c1ab, --backgroundGlobalColor: rgba(28, 28, 28, 0.93), --backgroundMobile: #040303eb, - --debugColor: #c6511b + --debugColor: #c6511b, ); $themeLight: ( --backgroundColor: #3131313b, @@ -52,7 +52,7 @@ $themeLight: ( --backgroundBlock: #000000, --backgroundGlobalColor: #ffffff40, --backgroundMobile: #b4b2c7bf, - --debugColor: #c6511b + --debugColor: #c6511b, ); @mixin spread-map($map: ()) { @each $key, $value in $map { @@ -70,4 +70,4 @@ $themeLight: ( } :root.light { @include spread-map($themeLight); -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/helpers/utils.tsx b/src-editor/src/Components/RulesEditor/helpers/utils.tsx index 0ade0f9c..8c1c14cc 100644 --- a/src-editor/src/Components/RulesEditor/helpers/utils.tsx +++ b/src-editor/src/Components/RulesEditor/helpers/utils.tsx @@ -1,16 +1,27 @@ import { I18n } from '@iobroker/adapter-react-v5'; -let lang; -const getName = obj => { +let lang: ioBroker.Languages | undefined; +export function getName(obj: undefined | ioBroker.StringOrTranslated | null): string { lang = lang || I18n.getLanguage(); - if (typeof obj === 'object') { + if (obj && typeof obj === 'object') { return obj[lang] || obj.en; } - return obj; -}; + return obj || ''; +} -const utils = { - getName, -}; +export function renderValue(val: any): string { + if (val === null) { + return 'null'; + } + if (val === undefined) { + return 'undefined'; + } + if (Array.isArray(val)) { + return val.join(', '); + } + if (typeof val === 'object') { + return JSON.stringify(val); + } -export default utils; + return val.toString(); +} diff --git a/src-editor/src/Components/RulesEditor/hooks/useStateLocal.tsx b/src-editor/src/Components/RulesEditor/hooks/useStateLocal.tsx index b0f11d99..0e0ac058 100644 --- a/src-editor/src/Components/RulesEditor/hooks/useStateLocal.tsx +++ b/src-editor/src/Components/RulesEditor/hooks/useStateLocal.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -export function useStateLocal(events, nameEvents) { +export function useStateLocal(value: T, valueName: string): [T, (newHeadCells: T) => void, boolean] { const [state, setState] = React.useState( - localStorage.getItem(nameEvents) ? JSON.parse(localStorage.getItem(nameEvents)) : events + window.localStorage.getItem(valueName) ? JSON.parse(window.localStorage.getItem(valueName) || '') : value, ); - const eventsToInstall = (newHeadCells) => { - localStorage.setItem(nameEvents, JSON.stringify(newHeadCells)); - setState(newHeadCells); + const eventsToInstall = (newValue: T): void => { + window.localStorage.setItem(valueName, JSON.stringify(newValue)); + setState(newValue); }; - return [state, eventsToInstall, localStorage.getItem(nameEvents) ? true : false]; + return [state, eventsToInstall, !!window.localStorage.getItem(valueName)]; } diff --git a/src-editor/src/Components/RulesEditor/index.tsx b/src-editor/src/Components/RulesEditor/index.tsx index 6349bebb..aa71623a 100644 --- a/src-editor/src/Components/RulesEditor/index.tsx +++ b/src-editor/src/Components/RulesEditor/index.tsx @@ -1,79 +1,145 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; +import { I18n, type IobTheme, type ThemeName, type ThemeType, Utils } from '@iobroker/adapter-react-v5'; import cls from './style.module.scss'; import { CustomDragLayer } from './components/CustomDragLayer'; import ContentBlockItems from './components/ContentBlockItems'; import { ContextWrapperCreate } from './components/ContextWrapper'; -import Compile from './helpers/Compile'; +import { code2json, json2code } from './helpers/Compile'; import Menu from './components/Menu'; import './helpers/stylesVariables.scss'; import DialogExport from '../../Dialogs/Export'; import DialogImport from '../../Dialogs/Import'; +import type { DebugMessage, RuleUserRules } from './types'; +import type { GenericBlock } from '@/Components/RulesEditor/components/GenericBlock'; -const RulesEditor = ({ code, onChange, themeName, themeType, theme, setTourStep, tourStep, isTourOpen, command, scriptId, changed, running }) => { - // eslint-disable-next-line no-unused-vars +interface RulesEditorProps { + onChange: (code: string) => void; + code: string; + scriptId: string; + setTourStep: (step: number) => void; + tourStep: number; + command: string; + themeType: ThemeType; + themeName: ThemeName; + theme: IobTheme; + searchText: string; + resizing: boolean; + isTourOpen: boolean; + changed: boolean; + running: boolean; +} + +let gDebugMessages: DebugMessage[] = []; + +const RulesEditor = ({ + code, + onChange, + themeName, + themeType, + theme, + setTourStep, + tourStep, + isTourOpen, + command, + scriptId, + changed, + running, +}: RulesEditorProps): React.JSX.Element | null => { const { blocks, socket, setOnUpdate, setOnDebugMessage, setEnableSimulation } = useContext(ContextWrapperCreate); - const [allBlocks, setAllBlocks] = useState([]); - const [userRules, setUserRules] = useState(Compile.code2json(code)); + const [allBlocks, setAllBlocks] = useState<(typeof GenericBlock)[]>([]); + const [userRules, setUserRules] = useState(code2json(code)); const [importExport, setImportExport] = useState(''); const [modal, setModal] = useState(false); - //const [jsAlive, setJsAlive] = useState(false); - //const [jsInstance, setJsInstance] = useState(false); useEffect(() => { - let _jsInstance; - let _jsAlive; - const handler = (id, obj) => { - if (id === _jsInstance + '.alive') { - if (_jsAlive !== obj?.val) { - _jsAlive = obj?.val; + let _jsInstance: string | undefined; + let _jsAlive: boolean; + const aliveHandler = (id: string, state: ioBroker.State | null | undefined): void => { + if (id === `${_jsInstance}.alive`) { + if (_jsAlive !== state?.val) { + _jsAlive = !!state?.val; //setJsAlive(_jsAlive); - _jsAlive && socket.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOn', scriptId); + _jsAlive && + _jsInstance && + void socket?.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOn', scriptId); + } + } + }; + + const handler = (_id: string, obj: ioBroker.Object | null | undefined): void => { + if (!socket) { + return; + } + if (_jsInstance !== (obj as ioBroker.ScriptObject)?.common?.engine) { + if (_jsInstance) { + socket.unsubscribeState(`${_jsInstance}.alive`, aliveHandler); + if (_jsAlive) { + void socket.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOn', scriptId); + } } - } else { - if (_jsInstance !== obj?.common?.engine) { - _jsInstance && socket.unsubscribeState(`${_jsInstance}.alive`, handler); - _jsAlive && socket.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOn', scriptId); - _jsInstance = obj?.common?.engine; - //setJsInstance(_jsInstance); - _jsInstance && socket.subscribeState(`${_jsInstance}.alive`, handler); + _jsInstance = obj?.common?.engine; + if (_jsInstance) { + _jsInstance && void socket.subscribeState(`${_jsInstance}.alive`, aliveHandler); } } }; - const handlerStatus = (id, state) => { + const handlerStatus = (_id: string, state: ioBroker.State | null | undefined): void => { if (state) { try { - let msg = JSON.parse(state.val); + const msg: DebugMessage = JSON.parse(state.val as string); + const now = Date.now(); // if not from previous session - if (msg.ruleId === scriptId && Date.now() - msg.ts < 1000) { - setOnDebugMessage({blockId: msg.blockId, data: msg.data, ts: msg.ts}); + if (msg.ruleId === scriptId && now - msg.ts < 1000) { + const messages = [...gDebugMessages, { blockId: msg.blockId, data: msg.data, ts: msg.ts }]; + // Delete all messages older than 5 seconds and if the length is bigger than 200 + if (messages.length > 200) { + messages.splice(0, 200 - messages.length); + } + for (let m = messages.length - 1; m >= 0; m--) { + if (messages[m].ts < now - 5000) { + messages.splice(0, m); + break; + } + } + console.log(`Debug1: ${JSON.stringify(gDebugMessages)}`); + console.log(`Debug2: ${JSON.stringify(messages)}`); + + gDebugMessages = messages; + setOnDebugMessage(messages); } - } catch (e) { - console.error('Cannot parse: ' + state.val); + } catch { + console.error(`Cannot parse: ${state.val}`); } } }; - socket.getObject(scriptId) - .then(obj => { - _jsInstance = obj?.common?.engine; - //setJsInstance(_jsInstance); - socket.subscribeObject(scriptId, handler); - _jsInstance && socket.subscribeState(`${_jsInstance}.alive`, handler); - _jsInstance && socket.subscribeState(_jsInstance.replace(/^system\.adapter\./, '') + '.debug.rules', handlerStatus); - }); + void socket?.getObject(scriptId).then(obj => { + _jsInstance = obj?.common?.engine; + void socket.subscribeObject(scriptId, handler); + if (_jsInstance) { + // setJsInstance(_jsInstance); + void socket.subscribeState(`${_jsInstance}.alive`, aliveHandler); + void socket.subscribeState( + `${_jsInstance.replace(/^system\.adapter\./, '')}.debug.rules`, + handlerStatus, + ); + } + }); return function cleanup() { - _jsInstance && socket.unsubscribeObject(`${_jsInstance}.alive`, handler); - socket.unsubscribeState(scriptId, handler); - _jsAlive && _jsInstance && socket.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOff', scriptId); - _jsInstance && socket.unsubscribeState(_jsInstance.replace(/^system\.adapter\./, '') + '.debug.rules', handlerStatus); + socket?.unsubscribeState(scriptId, aliveHandler); + if (_jsInstance) { + void socket?.unsubscribeObject(`${_jsInstance}.alive`, handler); + if (_jsAlive) { + void socket?.sendTo(_jsInstance.replace(/^system\.adapter\./, ''), 'rulesOff', scriptId); + } + socket?.unsubscribeState(`${_jsInstance.replace(/^system\.adapter\./, '')}.debug.rules`, handlerStatus); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -83,7 +149,7 @@ const RulesEditor = ({ code, onChange, themeName, themeType, theme, setTourStep, }, [changed, running, setEnableSimulation]); useEffect(() => { - if (!!command) { + if (command) { setImportExport(command); if (!modal) { setModal(true); @@ -93,7 +159,7 @@ const RulesEditor = ({ code, onChange, themeName, themeType, theme, setTourStep, }, [command]); useEffect(() => { - const newUserRules = Compile.code2json(code); + const newUserRules = code2json(code); if (JSON.stringify(newUserRules) !== JSON.stringify(userRules)) { setUserRules(newUserRules); setOnUpdate(true); @@ -105,12 +171,17 @@ const RulesEditor = ({ code, onChange, themeName, themeType, theme, setTourStep, document.getElementsByTagName('HTML')[0].className = themeName || 'blue'; }, [themeName]); - const onChangeBlocks = useCallback(json => { - setUserRules(json); - onChange(Compile.json2code(json, blocks)); - }, [blocks, onChange]); + const onChangeBlocks = useCallback( + (json: RuleUserRules): void => { + setUserRules(json); + if (blocks) { + onChange(json2code(json, blocks)); + } + }, + [blocks, onChange], + ); - const ref = useRef({ clientWidth: 0 }); + const ref = useRef(null); const [addClass, setAddClass] = useState({ 835: false, 1035: false }); useEffect(() => { if (ref.current) { @@ -125,104 +196,107 @@ const RulesEditor = ({ code, onChange, themeName, themeType, theme, setTourStep, } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ref.current.clientWidth]) + }, [ref.current?.clientWidth || 0]); - if (!blocks) { + if (!blocks || !socket) { return null; } - return
- - {importExport === "export" ? - setModal(false)} - open={modal} - text={JSON.stringify(userRules, null, 2)} /> : - { - setModal(false); - if (text) { - onChangeBlocks(JSON.parse(text)); - } - }} />} - {
- + - - - -
} -
; -} - -RulesEditor.propTypes = { - onChange: PropTypes.func, - code: PropTypes.string, - scriptId: PropTypes.string, - setTourStep: PropTypes.func, - tourStep: PropTypes.number, - command: PropTypes.string, - themeType: PropTypes.string, - themeName: PropTypes.string, - theme: PropTypes.object, - searchText: PropTypes.string, - resizing: PropTypes.bool, - + {modal ? ( + importExport === 'export' ? ( + setModal(false)} + text={JSON.stringify(userRules, null, 2)} + /> + ) : ( + { + setModal(false); + if (text) { + onChangeBlocks(JSON.parse(text)); + } + }} + /> + ) + ) : null} + { +
+ + + + +
+ } +
+ ); }; export default RulesEditor; diff --git a/src-editor/src/Components/RulesEditor/style.module.scss b/src-editor/src/Components/RulesEditor/style.module.scss index 0d194d6b..f4dc32a0 100644 --- a/src-editor/src/Components/RulesEditor/style.module.scss +++ b/src-editor/src/Components/RulesEditor/style.module.scss @@ -1,5 +1,6 @@ .wrapperRules { - background: linear-gradient(0deg, var(--backgroundGlobalColor), var(--backgroundGlobalColor)), url("../assets/back.jpg"); + background: linear-gradient(0deg, var(--backgroundGlobalColor), var(--backgroundGlobalColor)), + url('../assets/back.jpg'); background-repeat: no-repeat; background-size: cover; height: 100%; diff --git a/src-editor/src/Components/ScriptEditor.tsx b/src-editor/src/Components/ScriptEditor.tsx index 971991d4..f8eaea1d 100644 --- a/src-editor/src/Components/ScriptEditor.tsx +++ b/src-editor/src/Components/ScriptEditor.tsx @@ -1,98 +1,145 @@ import React from 'react'; -import PropTypes from 'prop-types'; import MonacoEditor from 'react-monaco-editor'; +import type * as monacoEditor from 'monaco-editor'; -class ScriptEditor extends React.Component { - constructor(props) { +interface ScriptEditorProps { + onChange?: (newValue: string) => void; + onInserted?: () => void; + isDark?: boolean; + readOnly?: boolean; + code?: string; + language?: 'javascript' | 'typescript'; + searchText?: string; + insert?: string; +} + +interface ScriptEditorState { + isDark: boolean; + language: 'javascript' | 'typescript'; + readOnly: boolean; + forceUpdate: boolean; + originalCode: string; + insertText: string; +} + +class ScriptEditor extends React.Component { + private editor: monacoEditor.editor.IStandaloneCodeEditor | null = null; + + private monaco: typeof monacoEditor | null = null; + + private updating = false; + + constructor(props: ScriptEditorProps) { super(props); this.state = { isDark: props.isDark || false, language: props.language || 'javascript', readOnly: props.readOnly || false, + forceUpdate: false, + originalCode: props.code || '', + insertText: '', }; - this.editor = null; - this.monaco = null; - this.insert = ''; - this.originalCode = props.code || ''; } - UNSAFE_componentWillReceiveProps(nextProps) { - if (this.originalCode !== nextProps.code) { - this.forceUpdate(); - this.originalCode = nextProps.code || ''; - } else if (this.state.language !== (nextProps.language || 'javascript')) { - this.setState({ language: nextProps.language || 'javascript' }); - } else if (this.state.readOnly !== (nextProps.readOnly || false)) { - this.setState({ readOnly: nextProps.readOnly || false }); - } else if (this.state.isDark !== (nextProps.isDark || false)) { - this.setState({ isDark: nextProps.isDark || false }); + static getDerivedStateFromProps( + props: ScriptEditorProps, + state: ScriptEditorState, + ): Partial | null { + let newState: Partial | null = null; + if (props.code !== state.originalCode) { + newState = { originalCode: props.code || '', forceUpdate: true }; } - - if (this.insert !== nextProps.insert) { - this.insert = nextProps.insert; - nextProps.insert && this.insertTextIntoEditor(nextProps.insert); - if (nextProps.insert) { - setTimeout(() => this.props.onInserted && this.props.onInserted(), 100); - } + if (props.language !== state.language) { + newState = newState || {}; + newState.language = props.language || 'javascript'; + } + if (props.isDark !== state.isDark) { + newState = newState || {}; + newState.isDark = props.isDark || false; + } + if (props.readOnly !== state.readOnly) { + newState = newState || {}; + newState.readOnly = props.readOnly || false; + } + if (props.insert !== state.insertText) { + newState = newState || {}; + newState.insertText = props.insert || ''; } + + return newState; } /** * Inserts some text into the given editor - * @param {string} text The text to add + * + * @param text The text to add */ - insertTextIntoEditor(text) { - const selection = this.editor.getSelection(); - const range = new this.monaco.Range( - selection.startLineNumber, - selection.startColumn, - selection.endLineNumber, - selection.endColumn, - ); + insertTextIntoEditor(text: string): void { + if (this.editor && this.monaco) { + const selection = this.editor.getSelection(); + if (selection) { + const range = new this.monaco.Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn, + ); - this.editor.executeEdits('', [{ range, text, forceMoveMarkers: true }]); + this.editor.executeEdits('', [{ range, text, forceMoveMarkers: true }]); + } + } } - editorDidMount(editor, monaco) { + editorDidMount(editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor): void { this.monaco = monaco; this.editor = editor; - //editor.focus(); + // editor.focus(); } - onChange(newValue, e) { + onChange(newValue: string): void { this.props.onChange && this.props.onChange(newValue); } - render() { + render(): React.JSX.Element { const options = { selectOnLineNumbers: true, scrollBeyondLastLine: false, automaticLayout: true, readOnly: this.state.readOnly, }; - return this.onChange(newValue)} - editorDidMount={(editor, monaco) => this.editorDidMount(editor, monaco)} - />; + + if (this.state.forceUpdate && !this.updating) { + this.updating = true; + setTimeout(() => { + this.updating = false; + this.setState({ forceUpdate: false }); + }, 50); + } + + if (this.state.insertText) { + setTimeout(() => { + this.insertTextIntoEditor(this.state.insertText); + this.setState({ insertText: '' }, () => this.props.onInserted && this.props.onInserted()); + }, 50); + } + + return ( + this.onChange(newValue)} + editorDidMount={(editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor) => + this.editorDidMount(editor, monaco) + } + /> + ); } } -ScriptEditor.propTypes = { - onChange: PropTypes.func, - onInserted: PropTypes.func, - isDark: PropTypes.bool, - readOnly: PropTypes.bool, - code: PropTypes.string, - language: PropTypes.string, - searchText: PropTypes.string, -}; - export default ScriptEditor; diff --git a/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx b/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx index c9bbb73c..1954c104 100644 --- a/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx +++ b/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx @@ -1,18 +1,81 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import type * as monacoEditor from 'monaco-editor'; import { Fab } from '@mui/material'; import { MdGTranslate as IconNoCheck } from 'react-icons/md'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5'; +import type { DebuggerLocation, SetBreakpointParameterType } from './Debugger/types'; -function isIdOfGlobalScript(id) { +function isIdOfGlobalScript(id: string): boolean { return /^script\.js\.global\./.test(id); } + let index = 0; -class ScriptEditor extends React.Component { - constructor(props) { + +interface ScriptEditorProps { + adapterName: string; + socket: AdminConnection; + runningInstances: Record; + name: string; + onChange?: (code: string) => void; + onForceSave?: () => void; + onInserted?: () => void; + isDark?: boolean; + readOnly?: boolean; + code?: string; + language?: 'javascript' | 'typescript'; + onRegisterSelect?: (cb: (() => string | undefined) | null) => void; + searchText?: string; + checkJs?: boolean; + changed?: boolean; + insert?: string; + style?: React.CSSProperties; + + breakpoints?: SetBreakpointParameterType[]; + location?: DebuggerLocation | null; + onToggleBreakpoint?: (lineNumber: number) => void; +} + +interface ScriptEditorState { + name: string; + isDark: boolean; + language: 'javascript' | 'typescript'; + readOnly: boolean; + alive: boolean; + check: boolean; + searchText: string; + typingsLoaded: boolean; +} + +class ScriptEditor extends React.Component { + private readonly monacoDiv: React.RefObject | null = null; + + private editor: monacoEditor.editor.IStandaloneCodeEditor | null = null; + + private monaco: typeof monacoEditor | null = (window as any).monaco as typeof monacoEditor | null; + + private insert: string = ''; + + private originalCode: string; + + private runningInstancesStr: string; + + private monacoCounter: number = 0; + + private location: DebuggerLocation | undefined; + + private breakpoints: SetBreakpointParameterType[] | undefined; + + private lastSearch: string = ''; + + // TypeScript declarations + private typings: Record = {}; + + private decorations: string[] = []; + + constructor(props: ScriptEditorProps) { super(props); this.state = { name: 'current', @@ -25,21 +88,15 @@ class ScriptEditor extends React.Component { typingsLoaded: false, }; this.runningInstancesStr = JSON.stringify(this.props.runningInstances); - this.monacoDiv = null; //ref - this.editor = null; - this.monaco = window.monaco; - this.insert = ''; this.originalCode = props.code || ''; - this.typings = {}; // TypeScript declarations - this.lastSearch = ''; + this.monacoDiv = React.createRef(); } - waitForMonaco(cb) { - let monacoLoaded = this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; + waitForMonaco(cb: () => void): void { + let monacoLoaded = !!this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; if (!monacoLoaded || !this.props.runningInstances) { - this.monaco = window.monaco; - monacoLoaded = this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; - this.monacoCounter = this.monacoCounter || 0; + this.monaco = (window as any).monaco as typeof monacoEditor | null; + monacoLoaded = !!this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; this.monacoCounter++; if (!monacoLoaded && this.monacoCounter < 20) { console.log('wait for monaco loaded'); @@ -54,15 +111,18 @@ class ScriptEditor extends React.Component { } } - loadTypings(runningInstances) { + loadTypings(runningInstances?: Record): void { if (!this.editor) { return; } runningInstances = runningInstances || this.props.runningInstances; - const scriptAdapterInstance = runningInstances && Object.keys(runningInstances).find(id => runningInstances[id]); + const scriptAdapterInstance = + runningInstances && Object.keys(runningInstances).find(id => runningInstances && runningInstances[id]); + if (scriptAdapterInstance) { - this.props.socket.sendTo(scriptAdapterInstance.replace('system.adapter.', ''), 'loadTypings', null) + void this.props.socket + .sendTo(scriptAdapterInstance.replace('system.adapter.', ''), 'loadTypings', null) .then(result => { this.setState({ alive: true, check: true, typingsLoaded: true }); this.setTypeCheck(true); @@ -76,10 +136,11 @@ class ScriptEditor extends React.Component { } } - componentDidMount() { - const monacoLoaded = this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; + componentDidMount(): void { + let monacoLoaded = !!this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; if (!monacoLoaded || !this.props.runningInstances) { - this.monaco = window.monaco; + this.monaco = (window as any).monaco as typeof monacoEditor | null; + monacoLoaded = !!this.monaco?.languages?.typescript?.typescriptDefaults?.getCompilerOptions; if (!monacoLoaded) { console.log('wait for monaco loaded...'); this.waitForMonaco(() => this.componentDidMount()); @@ -87,48 +148,68 @@ class ScriptEditor extends React.Component { return; } } - if (!this.editor && monacoLoaded) { + if (!this.editor && monacoLoaded && this.monaco) { console.log('Init editor'); - this.props.onRegisterSelect && this.props.onRegisterSelect(() => this.editor.getModel().getValueInRange(this.editor.getSelection())); + if (this.props.onRegisterSelect) { + this.props.onRegisterSelect((): string | undefined => { + if (this.editor) { + const selection = this.editor.getSelection(); + if (selection) { + return this.editor.getModel()?.getValueInRange(selection); + } + } + return undefined; + }); + } // For some reason, we have to get the original compiler options // and assign new properties one by one - const compilerOptions = this.monaco.languages.typescript.typescriptDefaults['getCompilerOptions'](); - compilerOptions.target = this.monaco.languages.typescript.ScriptTarget.ES2015; + const compilerOptions = this.monaco.languages.typescript.typescriptDefaults.getCompilerOptions(); + // compilerOptions.target = this.monaco.languages.typescript.ScriptTarget.ES2020; compilerOptions.allowJs = true; compilerOptions.checkJs = this.props.checkJs !== false; compilerOptions.noLib = true; compilerOptions.lib = []; compilerOptions.useUnknownInCatchVariables = false; compilerOptions.moduleResolution = this.monaco.languages.typescript.ModuleResolutionKind.NodeJs; + compilerOptions.target = this.monaco.languages.typescript.ScriptTarget.ESNext; + compilerOptions.module = this.monaco.languages.typescript.ModuleKind.ESNext; + compilerOptions.allowNonTsExtensions = true; + this.monaco.languages.typescript.typescriptDefaults.setCompilerOptions(compilerOptions); this.setTypeCheck(false); - // Create the editor instances - this.editor = this.monaco.editor.create(this.monacoDiv, { - lineNumbers: 'on', - scrollBeyondLastLine: false, - automaticLayout: true, - glyphMargin: !!this.props.breakpoints, - }); + if (this.monacoDiv?.current) { + // Create the editor instances + this.editor = this.monaco.editor.create(this.monacoDiv?.current, { + lineNumbers: 'on', + scrollBeyondLastLine: false, + automaticLayout: true, + glyphMargin: !!this.props.breakpoints, + colorDecorators: true, + }); - this.editor.onDidChangeModelContent(() => - this.onChange(this.editor.getValue())); + this.editor.onDidChangeModelContent(() => this.onChange()); - // Load typings for the JS editor - /** @type {string} */ - this.loadTypings(); + // Load typings for the JS editor + this.loadTypings(); - this.editor.addCommand(this.monaco.KeyMod.CtrlCmd | this.monaco.KeyCode.KEY_S, () => - this.onForceSave()); + if (this.props.onForceSave) { + this.editor.addCommand( + this.monaco.KeyMod.CtrlCmd | this.monaco.KeyCode.KeyS, + () => this.props.onForceSave && this.props.onForceSave(), + ); + } - setTimeout(() => { - this.highlightText(this.state.searchText); - this.location = this.props.location; - this.breakpoints = this.props.breakpoints; - this.showDecorators(); - }); + setTimeout(() => { + this.highlightText(this.state.searchText); + this.location = this.props.location || undefined; + this.breakpoints = this.props.breakpoints; + this.showDecorators(); + }); + } } + const options = { selectOnLineNumbers: true, scrollBeyondLastLine: false, @@ -139,48 +220,68 @@ class ScriptEditor extends React.Component { }; this.setEditorOptions(options); - this.editor.focus(); - this.editor.setValue(this.originalCode); + if (this.editor) { + this.editor.focus(); + this.editor.setValue(this.originalCode); - if (this.props.onToggleBreakpoint) { - // add onMouseDown listener to toggle breakpoints - this.editor.onMouseDown(e => { - if (e.target.detail && e.target.detail.glyphMarginLeft !== undefined) { - this.props.onToggleBreakpoint(e.target.position.lineNumber - 1); - } - }); - } else { - // remove onMouseDown listener - this.editor.onMouseDown(() => { /* nop */ }); + if (this.props.onToggleBreakpoint) { + // add onMouseDown listener to toggle breakpoints + this.editor.onMouseDown((e: monacoEditor.editor.IEditorMouseEvent) => { + const target: monacoEditor.editor.IMouseTargetMargin = + e.target as monacoEditor.editor.IMouseTargetMargin; + if ( + this.props.onToggleBreakpoint && + target.detail?.glyphMarginLeft !== undefined && + target.position + ) { + this.props.onToggleBreakpoint(target.position.lineNumber - 1); + } + }); + } else { + // remove onMouseDown listener + this.editor.onMouseDown(() => { + /* nop */ + }); + } } } /** * Sets some options of the code editor - * @param {object} options The editor options to change - * @param {Partial<{readOnly: boolean, lineWrap: boolean, language: EditorLanguage, typeCheck: boolean}>} options + * + * @param options The editor options to change */ - setEditorOptions(options) { + setEditorOptions( + options: Partial<{ + readOnly: boolean; + lineWrap: boolean; + language: 'javascript' | 'typescript'; + typeCheck: boolean; + isDark: boolean; + }>, + ): void { if (options) { if (options.language) { this.setEditorLanguage(options.language); } - if (options.readOnly !== undefined) { - this.editor.updateOptions({ readOnly: options.readOnly }); - } - if (options.lineWrap !== undefined) { - this.editor.updateOptions({ wordWrap: options.lineWrap ? 'on' : 'off' }); + if (this.editor) { + if (options.readOnly !== undefined) { + this.editor.updateOptions({ readOnly: options.readOnly }); + } + if (options.lineWrap !== undefined) { + this.editor.updateOptions({ wordWrap: options.lineWrap ? 'on' : 'off' }); + } } if (options.typeCheck !== undefined) { this.setTypeCheck(options.typeCheck); } if (options.isDark !== undefined) { - this.monaco.editor.setTheme(options.isDark ? 'vs-dark' : 'vs'); + this.monaco?.editor.setTheme(options.isDark ? 'vs-dark' : 'vs'); } } } - componentWillUnmount() { + componentWillUnmount(): void { if (this.editor) { this.props.onRegisterSelect && this.props.onRegisterSelect(null); this.editor.dispose(); @@ -188,60 +289,69 @@ class ScriptEditor extends React.Component { } } - /** @typedef {"javascript" | "typescript"} EditorLanguage */ - /** * Sets the language of the code editor - * @param {EditorLanguage} language */ - setEditorLanguage(language) { + setEditorLanguage(language: 'javascript' | 'typescript'): void { // we need to recreate the model when changing languages, // so remember its settings + if (!this.editor) { + return; + } const model = this.editor.getModel(); - const code = model.getValue(); - const uri = model.uri.path; + if (model) { + const code = model.getValue(); + const uri = model.uri.path; - const filenameWithoutExtension = - typeof uri === 'string' && uri.includes('.') - ? uri.substr(0, uri.lastIndexOf('.')) - : 'index'; + const filenameWithoutExtension = + typeof uri === 'string' && uri.includes('.') ? uri.substring(0, uri.lastIndexOf('.')) : 'index'; - const extension = - language === 'javascript' ? 'js' - : (language === 'typescript' ? 'ts' : language); + const extension = language === 'javascript' ? 'js' : language === 'typescript' ? 'ts' : language; - // get rid of the original model - model.dispose(); + // get rid of the original model + model.dispose(); - // Both JS and TS need the model to work in TypeScript as the script type - // is inferred from the file extension - const newLanguage = (language === 'javascript' || language === 'typescript') ? 'typescript' : language; + // Both JS and TS need the model to work in TypeScript as the script type + // is inferred from the file extension + const newLanguage = language === 'javascript' || language === 'typescript' ? 'typescript' : language; - const newModel = this.monaco.editor.createModel( - code, - newLanguage, - this.monaco.Uri.from({ path: `${filenameWithoutExtension}${index++}.${extension}` }), - ); + const newModel = this.monaco?.editor.createModel( + code, + newLanguage, + this.monaco.Uri.from({ + scheme: window.location.protocol.replace(':', ''), + path: `${filenameWithoutExtension}${index++}.${extension}`, + }), + ); - this.editor.setModel(newModel); + if (newModel) { + this.editor.setModel(newModel); + } + } } /** * Enables or disables the type checking in the editor - * @param {boolean} enabled - Whether type checking is enabled or not + * + * @param enabled - Whether type checking is enabled or not */ - setTypeCheck(enabled) { + setTypeCheck(enabled: boolean): void { const options = { noSemanticValidation: !this.state.alive || !enabled, // toggle the type checking - noSyntaxValidation: !this.state.alive // always check the syntax + noSyntaxValidation: !this.state.alive, // always check the syntax }; - this.monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(options); + this.monaco?.languages.typescript.typescriptDefaults.setDiagnosticsOptions(options); + + this.monaco?.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: !this.state.alive || !enabled, + noSyntaxValidation: !this.state.alive, + }); } /** - * @param {string} [currentScriptName] The name of the current script + * @param currentScriptName The name of the current script */ - setEditorTypings(currentScriptName = '') { + setEditorTypings(currentScriptName = ''): void { const isGlobalScript = isIdOfGlobalScript(currentScriptName); // The filename of the declarations this script can see if it is a global script const partialDeclarationsPath = `${currentScriptName}.d.ts`; @@ -271,8 +381,8 @@ class ScriptEditor extends React.Component { } else if (this.monaco?.languages?.typescript?.typescriptDefaults?.addExtraLib) { const existingLibs = this.monaco.languages.typescript.typescriptDefaults.getExtraLibs(); wantedTypings.forEach(lib => { - if (!existingLibs[lib.filePath]) { - this.monaco.languages.typescript.typescriptDefaults.addExtraLib(lib, lib.filePath); + if (!existingLibs[lib.filePath] && this.monaco) { + this.monaco.languages.typescript.typescriptDefaults.addExtraLib(lib.content, lib.filePath); } }); } @@ -280,38 +390,57 @@ class ScriptEditor extends React.Component { /** * Inserts some text into the given editor - * @param {string} text The text to add + * + * @param text The text to add */ - insertTextIntoEditor(text) { + insertTextIntoEditor(text: string): void { + if (!this.editor || !this.monaco) { + return; + } const selection = this.editor.getSelection(); - const range = new this.monaco.Range( - selection.startLineNumber, - selection.startColumn, - selection.endLineNumber, - selection.endColumn, - ); - this.editor.executeEdits('', [{ range, text, forceMoveMarkers: true }]); + if (selection) { + const range = new this.monaco.Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn, + ); + this.editor.executeEdits('', [{ range, text, forceMoveMarkers: true }]); + } this.editor.focus(); } - highlightText(text) { - const range = text && this.editor.getModel().findMatches(text); - if (range && range.length) { - range.forEach(r => this.editor.setSelection(r.range)); + highlightText(text: string): void { + if (!this.editor || !this.monaco) { + return; + } + + const range: monacoEditor.editor.FindMatch[] | undefined = text + ? this.editor.getModel()?.findMatches(text, true, false, false, null, true) + : undefined; + if (range?.length) { + range.forEach(r => this.editor?.setSelection(r.range)); this.editor.revealLine(range[0].range.startLineNumber); - } else if (this.editor) { - const row = this.editor.getPosition().lineNumber; - const col = this.editor.getPosition().column; - this.editor.setSelection(new this.monaco.Range(row, col, row, col)); + } else { + const pos = this.editor.getPosition(); + if (pos) { + const row = pos.lineNumber; + const col = pos.column; + this.editor.setSelection(new this.monaco.Range(row, col, row, col)); + } } } - showDecorators() { - this.decorations = this.decorations || []; + showDecorators(): void { const decorations = []; - if (this.location) { + if (this.location && this.monaco) { decorations.push({ - range: new this.monaco.Range(this.location.lineNumber + 1, this.location.columnNumber + 1, this.location.lineNumber + 1, 1000), + range: new this.monaco.Range( + this.location.lineNumber + 1, + (this.location.columnNumber || 0) + 1, + this.location.lineNumber + 1, + 1000, + ), options: { isWholeLine: false, className: this.props.isDark ? 'monacoCurrentLineDark' : 'monacoCurrentLine', @@ -326,8 +455,8 @@ class ScriptEditor extends React.Component { }); } - if (this.breakpoints) { - this.breakpoints.forEach(bp => { + this.breakpoints?.forEach(bp => { + if (this.monaco) { decorations.push({ range: new this.monaco.Range(bp.location.lineNumber + 1, 0, bp.location.lineNumber + 1, 100), options: { @@ -335,16 +464,21 @@ class ScriptEditor extends React.Component { glyphMarginClassName: this.props.isDark ? 'monacoBreakPointDark' : 'monacoBreakPoint', }, }); - }); + } + }); + if (this.editor) { + const editorModel = this.editor.getModel(); + if (editorModel) { + this.decorations = editorModel.deltaDecorations(this.decorations, decorations); + // this.decorations = this.editor.createDecorationsCollection(decorations); + } } - this.editor && (this.decorations = - this.editor.deltaDecorations(this.decorations, decorations)); } - initNewScript(name, code) { + initNewScript(name: string, code: string | undefined): void { this.setState({ name }); this.originalCode = code || ''; - this.editor && this.editor.setValue(code); + this.editor?.setValue(code || ''); this.highlightText(this.lastSearch); this.showDecorators(); // this.setEditorLanguage(); @@ -353,17 +487,29 @@ class ScriptEditor extends React.Component { this.setEditorTypings(name); } - scrollToLineIfNeeded(lineNumber) { + scrollToLineIfNeeded(lineNumber: number): void { if (this.editor) { const ranges = this.editor.getVisibleRanges(); - if (!ranges || !ranges[0] || ranges[0].startLineNumber > lineNumber || lineNumber > ranges[0].endLineNumber) { + if ( + !ranges || + !ranges[0] || + ranges[0].startLineNumber > lineNumber || + lineNumber > ranges[0].endLineNumber + ) { this.editor.revealLineInCenter(lineNumber); } } } - UNSAFE_componentWillReceiveProps(nextProps) { - const options = {}; + // TODO + UNSAFE_componentWillReceiveProps(nextProps: ScriptEditorProps): void { + const options: Partial<{ + readOnly: boolean; + lineWrap: boolean; + language: 'javascript' | 'typescript'; + typeCheck: boolean; + isDark: boolean; + }> = {}; if (this.state.name !== nextProps.name) { // A different script was selected this.initNewScript(nextProps.name, nextProps.code); @@ -378,22 +524,27 @@ class ScriptEditor extends React.Component { } // if the code not yet changed, update the new code - if (this.editor && !nextProps.changed && (nextProps.code !== this.originalCode || nextProps.code !== this.editor.getValue())) { - this.originalCode = nextProps.code; + if ( + this.editor && + !nextProps.changed && + (nextProps.code !== this.originalCode || nextProps.code !== this.editor.getValue()) + ) { + this.originalCode = nextProps.code || ''; this.editor.setValue(this.originalCode); this.showDecorators(); this.location && this.scrollToLineIfNeeded(this.location.lineNumber + 1); } if (nextProps.searchText !== this.lastSearch) { - this.lastSearch = nextProps.searchText; + this.lastSearch = nextProps.searchText || ''; this.highlightText(this.lastSearch); } - if (JSON.stringify(nextProps.location) !== JSON.stringify(this.location) && + if ( + JSON.stringify(nextProps.location) !== JSON.stringify(this.location) && JSON.stringify(nextProps.breakpoints) !== JSON.stringify(this.breakpoints) ) { - this.location = nextProps.location; + this.location = nextProps.location || undefined; this.breakpoints = nextProps.breakpoints; this.showDecorators(); this.editor && this.location && this.scrollToLineIfNeeded(this.location.lineNumber + 1); @@ -402,7 +553,7 @@ class ScriptEditor extends React.Component { this.breakpoints = nextProps.breakpoints; this.showDecorators(); } else if (JSON.stringify(nextProps.location) !== JSON.stringify(this.location)) { - this.location = nextProps.location; + this.location = nextProps.location || undefined; this.showDecorators(); this.editor && this.location && this.scrollToLineIfNeeded(this.location.lineNumber + 1); // this.editor && this.location && this.editor.setPosition(this.location.lineNumber + 1, this.location.columnNumber + 1); @@ -422,65 +573,62 @@ class ScriptEditor extends React.Component { this.setEditorOptions(options); if (this.insert !== nextProps.insert) { - this.insert = nextProps.insert; + this.insert = nextProps.insert || ''; if (this.insert) { console.log(`Insert text: ${this.insert}`); - setTimeout(insert => { - this.insertTextIntoEditor(insert); - setTimeout(() => this.props.onInserted && this.props.onInserted(), 100); - }, 100, this.insert); + setTimeout( + insert => { + this.insertTextIntoEditor(insert); + setTimeout(() => this.props.onInserted && this.props.onInserted(), 100); + }, + 100, + this.insert, + ); } } } - onChange(newValue, e) { - if (!this.props.readOnly) { + onChange(): void { + if (!this.props.readOnly && this.editor) { this.props.onChange && this.props.onChange(this.editor.getValue()); } } - render() { + render(): React.JSX.Element | null { if (!this.monaco?.languages?.typescript?.typescriptDefaults || !this.props.runningInstances) { setTimeout(() => { - this.monaco = window.monaco; + this.monaco = (window as any).monaco as typeof monacoEditor | null; this.forceUpdate(); }, 200); return null; } - return
this.monacoDiv = el} style={{width: '100%', height: '100%', overflow: 'hidden', position: 'relative'}}> - {!this.state.check && - - } -
; + {!this.state.check && ( + + + + )} +
+ ); } } -ScriptEditor.propTypes = { - adapterName: PropTypes.string.isRequired, - socket: PropTypes.object, - runningInstances: PropTypes.object, - name: PropTypes.string, - onChange: PropTypes.func, - onForceSave: PropTypes.func, - onInserted: PropTypes.func, - isDark: PropTypes.bool, - readOnly: PropTypes.bool, - code: PropTypes.string, - language: PropTypes.string, - onRegisterSelect: PropTypes.func, - searchText: PropTypes.string, - checkJs: PropTypes.bool, - changed: PropTypes.bool, - - breakpoints: PropTypes.array, - location: PropTypes.object, - onToggleBreakpoint: PropTypes.func, -}; - export default ScriptEditor; diff --git a/src-editor/src/Dialogs/AdapterDebug.tsx b/src-editor/src/Dialogs/AdapterDebug.tsx index 2dbc8c5c..d0cf0e64 100644 --- a/src-editor/src/Dialogs/AdapterDebug.tsx +++ b/src-editor/src/Dialogs/AdapterDebug.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Button, @@ -9,23 +8,19 @@ import { Dialog, ListItemIcon, List, - ListItem, Grid2, ListItemText, Input, InputAdornment, IconButton, + ListItemButton, } from '@mui/material'; -import { - Check as IconOk, - Cancel as IconCancel, - Close as IconClose, -} from '@mui/icons-material'; +import { Check as IconOk, Cancel as IconCancel, Close as IconClose } from '@mui/icons-material'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5'; -const styles = { +const styles: Record = { buttonIcon: { marginRight: 8, }, @@ -42,11 +37,31 @@ const styles = { title: { fontWeight: 'bold', marginTop: 16, - } + }, }; -class DialogAdapterDebug extends React.Component { - constructor(props) { +interface DialogAdapterDebugProps { + socket: AdminConnection; + onDebug: (instance: string, adapterToDebug: string) => void; + onClose: () => void; + title?: string; +} +interface DialogAdapterDebugState { + instances: { + id: string; + enabled: boolean; + host: string; + icon: string; + }[]; + jsInstance: string; + filter: string; + showAskForStop: boolean; + jsInstanceHost: string; + adapterToDebug: string; +} + +class DialogAdapterDebug extends React.Component { + constructor(props: DialogAdapterDebugProps) { super(props); this.state = { instances: [], @@ -58,10 +73,16 @@ class DialogAdapterDebug extends React.Component { }; } - componentDidMount() { - this.props.socket.getAdapterInstances() - .then(instances => { - instances = instances.filter(i => i && !i.common?.onlyWWW).map(item => { + componentDidMount(): void { + void this.props.socket.getAdapterInstances().then(oInstances => { + const instances: { + id: string; + enabled: boolean; + host: string; + icon: string; + }[] = oInstances + .filter(i => i && !i.common?.onlyWWW) + .map(item => { const name = item._id.replace(/^system\.adapter\./, ''); const [adapter] = name.split('.'); return { @@ -71,144 +92,186 @@ class DialogAdapterDebug extends React.Component { icon: item.common?.icon ? `../../adapter/${adapter}/${item.common.icon}` : '', }; }); - instances.sort((a, b) => a.id > b.id ? 1 : (a.id < b.id ? -1 : 0)); - let jsInstance = this.state.jsInstance || ''; - let jsInstanceObj = this.state.jsInstance && instances.find(item => item.id === this.state.jsInstance); - let jsInstanceHost; - - // check if selected instance is in the list - if (!this.state.jsInstance || !jsInstanceObj) { - jsInstance = instances.find(item => item.id.startsWith('javascript.')); // take the first one - jsInstanceHost = jsInstance ? jsInstance.host : ''; - jsInstance = jsInstance ? jsInstance.id : ''; - } else { - jsInstanceHost = jsInstanceObj ? jsInstanceObj.host : ''; - } + instances.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); + let jsInstance: string = this.state.jsInstance || ''; + const jsInstanceObj = this.state.jsInstance + ? instances.find(item => item.id === this.state.jsInstance) + : null; + let jsInstanceHost: string; - let adapterToDebug = this.state.adapterToDebug || ''; - if (adapterToDebug && !instances.find(item => item.id === adapterToDebug)) { - adapterToDebug = ''; - } + // check if selected instance is in the list + if (!this.state.jsInstance || !jsInstanceObj) { + const oJsInstance = instances.find(item => item.id.startsWith('javascript.')); // take the first one + jsInstanceHost = oJsInstance?.host || ''; + jsInstance = oJsInstance?.id || ''; + } else { + jsInstanceHost = jsInstanceObj?.host || ''; + } - this.setState({instances, jsInstance, adapterToDebug, jsInstanceHost}); - }); + let adapterToDebug = this.state.adapterToDebug || ''; + if (adapterToDebug && !instances.find(item => item.id === adapterToDebug)) { + adapterToDebug = ''; + } + + this.setState({ instances, jsInstance, adapterToDebug, jsInstanceHost }); + }); } - handleOk = () => { + handleOk = (): void => { // TODO - if (this.state.instances.find(item => item.id === this.state.adapterToDebug).enabled) { - return this.props.socket.getObject(`system.adapter.${this.state.adapterToDebug}`) - .then(obj => { + if (this.state.instances.find(item => item.id === this.state.adapterToDebug)?.enabled) { + void this.props.socket.getObject(`system.adapter.${this.state.adapterToDebug}`).then(obj => { + if (obj) { obj.common.enabled = false; - this.props.socket.setObject(obj._id, obj) - .then(() => - this.props.onDebug(this.state.jsInstance, this.state.adapterToDebug)); - }) - } else { - this.props.onDebug(this.state.jsInstance, this.state.adapterToDebug); + void this.props.socket + .setObject(obj._id, obj) + .then(() => this.props.onDebug(this.state.jsInstance, this.state.adapterToDebug)); + } + }); + return; } + this.props.onDebug(this.state.jsInstance, this.state.adapterToDebug); }; - - renderJavascriptList() { + renderJavascriptList(): React.JSX.Element | null { const js = this.state.instances.filter(item => item.id.startsWith('javascript.')); if (js.length < 2) { return null; } - return -
{I18n.t('Host')}
- - {js.map(item => this.setState({ jsInstance: item.id, jsInstanceHost: item.host })} - > - - {item.id} - - - )} - -
; + return ( + +
{I18n.t('Host')}
+ + {js.map(item => ( + this.setState({ jsInstance: item.id, jsInstanceHost: item.host })} + > + + {item.id} + + + + ))} + +
+ ); } - renderInstances() { + renderInstances(): React.JSX.Element { if (!this.state.jsInstance) { return ; } - const instances = this.state.instances.filter(item => - item.id !== this.state.jsInstance && item.host === this.state.jsInstanceHost && (!this.state.filter || item.id.includes(this.state.filter.toLowerCase()) )); - - return -
{I18n.t('Instances')}
- - {instances.map(item => this.setState({adapterToDebug: item.id}, () => this.handleOk())} - onClick={() => this.setState({adapterToDebug: item.id})} - > - - {item.id} - - - )} - -
; + const instances = this.state.instances.filter( + item => + item.id !== this.state.jsInstance && + item.host === this.state.jsInstanceHost && + (!this.state.filter || item.id.includes(this.state.filter.toLowerCase())), + ); + + return ( + +
{I18n.t('Instances')}
+ + {instances.map(item => ( + this.setState({ adapterToDebug: item.id }, () => this.handleOk())} + onClick={() => this.setState({ adapterToDebug: item.id })} + > + + {item.id} + + + + ))} + +
+ ); } - render() { - return false} - aria-labelledby="confirmation-dialog-title" - > - {this.props.title || I18n.t('Debug instance')} - - - - { - this.setState({filter: e.target.value}); - window.localStorage.setItem('javascript.debug.filter', e.target.value); - }} - endAdornment={ - {this.state.filter ? this.setState({ filter: '' })} - > - - : ''} - } - /> - - - - {this.renderJavascriptList()} - {this.renderInstances()} + render(): React.JSX.Element { + return ( + false} + aria-labelledby="confirmation-dialog-title" + > + {this.props.title || I18n.t('Debug instance')} + + + + { + this.setState({ filter: e.target.value }); + window.localStorage.setItem('javascript.debug.filter', e.target.value); + }} + endAdornment={ + + {this.state.filter ? ( + this.setState({ filter: '' })} + > + + + ) : ( + '' + )} + + } + /> + + + + {this.renderJavascriptList()} + {this.renderInstances()} + - - - - - - - - ; + + + + + + + ); } } -DialogAdapterDebug.propTypes = { - socket: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired, - onDebug: PropTypes.func.isRequired, -}; - export default DialogAdapterDebug; diff --git a/src-editor/src/Dialogs/AddNewScript.tsx b/src-editor/src/Dialogs/AddNewScript.tsx index 7b2a29c2..5ed779b9 100644 --- a/src-editor/src/Dialogs/AddNewScript.tsx +++ b/src-editor/src/Dialogs/AddNewScript.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Button, @@ -18,12 +17,13 @@ import { Cancel as IconCancel } from '@mui/icons-material'; import { I18n } from '@iobroker/adapter-react-v5'; +import type { ScriptType } from '@/types'; import ImgJS from '../assets/tileJS.png'; import ImgTS from '../assets/tileTS.png'; import ImgBlockly from '../assets/tileBlockly.png'; import ImgRules from '../assets/tileRules.png'; -const styles = { +const styles: Record = { card: { maxWidth: 345, minWidth: 250, @@ -44,132 +44,212 @@ const styles = { }, }; -class DialogAddNew extends React.Component { - handleCancel = () => { - this.props.onClose(); - }; +interface DialogAddNewProps { + onClose: (type?: ScriptType) => void; +} - handleOk = type => { - this.props.onClose(type); +class DialogAddNew extends React.Component { + handleCancel = (): void => { + this.props.onClose(); }; - openHtml(html) { + static openHtml(html: string): void { const lang = I18n.getLanguage(); if (!html.includes('javascript.md') && (lang === 'de' || lang === 'ru')) { html = html.replace(/\/en\//, `/${lang}/`); } - const win = window.open(html, '_blank'); - win.focus(); + const win: Window | null = window.open(html, '_blank'); + win?.focus(); } - getJSCard() { - return - this.props.onClose && this.props.onClose('Javascript/js')}> - - -

JavaScript

-
{I18n.t('for programmers')}
-
{I18n.t('JS description')}
-
-
- - - - -
; + getJSCard(): React.JSX.Element { + return ( + + this.props.onClose && this.props.onClose('Javascript/js')}> + + +

JavaScript

+
{I18n.t('for programmers')}
+
{I18n.t('JS description')}
+
+
+ + + + +
+ ); } - getTSCard() { - return - this.props.onClose && this.props.onClose('TypeScript/ts')}> - - -

TypeScript

-
{I18n.t('for professionals')}
-
{I18n.t('TS description')}
-
-
- - - - -
; + getTSCard(): React.JSX.Element { + return ( + + this.props.onClose && this.props.onClose('TypeScript/ts')}> + + +

TypeScript

+
{I18n.t('for professionals')}
+
{I18n.t('TS description')}
+
+
+ + + + +
+ ); } - getBlocklyCard() { - return - this.props.onClose && this.props.onClose('Blockly')}> - - -

Blockly

-
{I18n.t('normal')}
-
{I18n.t('Blockly description')}
-
-
- - - - -
; + getBlocklyCard(): React.JSX.Element { + return ( + + this.props.onClose && this.props.onClose('Blockly')}> + + +

Blockly

+
{I18n.t('normal')}
+
{I18n.t('Blockly description')}
+
+
+ + + + +
+ ); } - getRulesCard() { - return - this.props.onClose && this.props.onClose('Rules')}> - - -

Rules

-
{I18n.t('easy')}
-
{I18n.t('Rules description')}
-
-
- - - - -
; + getRulesCard(): React.JSX.Element { + return ( + + this.props.onClose && this.props.onClose('Rules')}> + + +

Rules

+
{I18n.t('easy')}
+
{I18n.t('Rules description')}
+
+
+ + + + +
+ ); } - render() { - return false} - maxWidth="lg" - fullWidth - open={!0} - aria-labelledby="confirmation-dialog-title" - > - {I18n.t('Add new script')} - - {this.getRulesCard()} - {this.getBlocklyCard()} - {this.getJSCard()} - {this.getTSCard()} - - - - - ; + render(): React.JSX.Element { + return ( + false} + maxWidth="lg" + fullWidth + open={!0} + aria-labelledby="confirmation-dialog-title" + > + {I18n.t('Add new script')} + + {this.getRulesCard()} + {this.getBlocklyCard()} + {this.getJSCard()} + {this.getTSCard()} + + + + + + ); } } -DialogAddNew.propTypes = { - onClose: PropTypes.func, -}; - export default DialogAddNew; diff --git a/src-editor/src/Dialogs/Delete.tsx b/src-editor/src/Dialogs/Delete.tsx index 4febfada..40bda6d0 100644 --- a/src-editor/src/Dialogs/Delete.tsx +++ b/src-editor/src/Dialogs/Delete.tsx @@ -1,24 +1,24 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - DialogTitle, - DialogContent, - DialogActions, - Dialog, -} from '@mui/material'; +import { Button, DialogTitle, DialogContent, DialogActions, Dialog } from '@mui/material'; -import { - Check as IconOk, - Cancel as IconCancel, - Delete as IconDelete, -} from '@mui/icons-material'; +import { Check as IconOk, Cancel as IconCancel, Delete as IconDelete } from '@mui/icons-material'; import { I18n } from '@iobroker/adapter-react-v5'; -class DialogDelete extends React.Component { - constructor(props) { +interface DialogDeleteProps { + onClose: () => void; + onDelete: (id: string) => void; + name: string; + id: string; +} +interface DialogDeleteState { + name: string; + id: string; +} + +class DialogDelete extends React.Component { + constructor(props: DialogDeleteProps) { super(props); this.state = { name: props.name, @@ -26,49 +26,49 @@ class DialogDelete extends React.Component { }; } - componentWillReceiveProps(nextProps) { - if (nextProps.name !== this.props.name) { - this.setState({name: nextProps.name}); - } - if (nextProps.id !== this.props.id) { - this.setState({id: nextProps.id}); - } - } - - handleCancel = () => { - this.props.onClose(null); + handleCancel = (): void => { + this.props.onClose(); }; - handleOk = () => { + handleOk = (): void => { this.props.onDelete(this.state.id); - this.props.onClose(this.props.value); + this.props.onClose(); }; - render() { - return false} - maxWidth="md" - open={!0} - aria-labelledby="confirmation-dialog-title" - > - {I18n.t('Are you sure?')} - - - {I18n.t('Delete %s', this.state.name)} - - - - - - ; + render(): React.JSX.Element { + return ( + false} + maxWidth="md" + open={!0} + aria-labelledby="confirmation-dialog-title" + > + {I18n.t('Are you sure?')} + + + {I18n.t('Delete %s', this.state.name)} + + + + + + + ); } } -DialogDelete.propTypes = { - onClose: PropTypes.func, - onDelete: PropTypes.func, - name: PropTypes.string, - id: PropTypes.string, -}; - export default DialogDelete; diff --git a/src-editor/src/Dialogs/Error.tsx b/src-editor/src/Dialogs/Error.tsx index 0662e959..31f85ccf 100644 --- a/src-editor/src/Dialogs/Error.tsx +++ b/src-editor/src/Dialogs/Error.tsx @@ -1,69 +1,66 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from '@mui/material'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; import { Check as IconOk } from '@mui/icons-material'; -import { I18n } from '@iobroker/adapter-react-v5'; +import { I18n, type IobTheme } from '@iobroker/adapter-react-v5'; -const styles = { - title: theme => ({ +const styles: Record = { + title: (theme: IobTheme) => ({ background: theme.palette.error.main, color: theme.palette.error.contrastText, '&>h2': { color: theme.palette.error.contrastText, - } + }, }), }; +interface DialogErrorProps { + onClose: () => void; + title?: string; + text: string | React.JSX.Element; +} -class DialogError extends React.Component { - constructor(props) { - super(props); - console.log('Error created') - } - handleOk = () => { - this.props.onClose && this.props.onClose(); +class DialogError extends React.Component { + handleOk = (): void => { + this.props.onClose(); }; - render() { - return this.handleOk()} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - - {this.props.title || I18n.t('Error')} - - - - {this.props.text || I18n.t('Unknown error!')} - - - - - - ; + render(): React.JSX.Element { + return ( + this.handleOk()} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + {this.props.title || I18n.t('Error')} + + + + {this.props.text || I18n.t('Unknown error!')} + + + + + + + ); } } -DialogError.propTypes = { - onClose: PropTypes.func, - title: PropTypes.string, - text: PropTypes.string, - icon: PropTypes.object -}; - export default DialogError; diff --git a/src-editor/src/Dialogs/Export.tsx b/src-editor/src/Dialogs/Export.tsx index 2f7724f5..ef349544 100644 --- a/src-editor/src/Dialogs/Export.tsx +++ b/src-editor/src/Dialogs/Export.tsx @@ -1,25 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - DialogTitle, - DialogContent, - DialogActions, - Dialog, - Popper, - Fade, - Paper, -} from '@mui/material'; +import { Button, DialogTitle, DialogContent, DialogActions, Dialog, Popper, Fade, Paper } from '@mui/material'; -import { - FileCopy as IconCopy, - Cancel as IconCancel, -} from '@mui/icons-material'; +import { FileCopy as IconCopy, Cancel as IconCancel } from '@mui/icons-material'; import { FaFileExport as IconExport } from 'react-icons/fa'; -import { I18n, Utils } from '@iobroker/adapter-react-v5'; -const styles = { +import { I18n, type ThemeType, Utils } from '@iobroker/adapter-react-v5'; + +const styles: Record = { textArea: { width: '100%', height: '100%', @@ -39,8 +27,19 @@ const styles = { }, }; -class DialogExport extends React.Component { - constructor(props) { +interface DialogExportProps { + onClose: () => void; + text: string; + scriptId: string; + themeType: ThemeType; +} +interface DialogExportState { + anchorEl: null | HTMLElement; + popper: string; +} + +class DialogExport extends React.Component { + constructor(props: DialogExportProps) { super(props); this.state = { anchorEl: null, @@ -48,11 +47,11 @@ class DialogExport extends React.Component { }; } - handleCancel() { + handleCancel(): void { this.props.onClose(); } - onCopy(event) { + onCopy(event: React.MouseEvent): void { Utils.copyToClipboard(this.props.text); const anchorEl = event.currentTarget; @@ -62,70 +61,99 @@ class DialogExport extends React.Component { }, 50); } - render() { - const file = new Blob([this.props.text], {type: 'application/xml'}); - const fileName = this.props.scriptId.substring('scripts.js'.length) + '.xml'; + render(): React.JSX.Element { + const file = new Blob([this.props.text], { type: 'application/xml' }); + const fileName = `${this.props.scriptId.substring('scripts.js'.length)}.xml`; - return false} - maxWidth="lg" - sx={{ '& .MuiDialog-paper': styles.dialog }} - fullWidth - open={this.props.open} - aria-labelledby="export-dialog-title" - > - {I18n.t('Export selected blocks')} - -
-                    {this.props.text}
-            
-
- - - - + return ( + false} + maxWidth="lg" + sx={{ '& .MuiDialog-paper': styles.dialog }} + fullWidth + open={!0} + aria-labelledby="export-dialog-title" + > + {I18n.t('Export selected blocks')} + +
+                        {this.props.text}
+                    
+
+ + + + - - {({ TransitionProps }) => ( - - -

{this.state.popper}

-
-
- )} -
-