diff --git a/package.json b/package.json index 3799ad24..1248868a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "test": "npm run test:declarations && npm run test:javascript", "translate": "translate-adapter", "//postinstall": "node ./install/installTypings.js", - "build": "node tasks", + "build": "node tasks && npm run tsc-backend", "tsc-backend": "tsc -p tsconfig.build.json", "release": "release-script --noPush -y --all", "update-packages": "ncu --upgrade && cd src && ncu --upgrade && cd ../src-admin && ncu --upgrade", diff --git a/src-editor/package.json b/src-editor/package.json index b1bdd9f0..3ed98b55 100644 --- a/src-editor/package.json +++ b/src-editor/package.json @@ -44,7 +44,8 @@ "scripts": { "start": "craco start", "lint": "eslint -c eslint.config.mjs", - "build": "craco build" + "build": "craco build", + "tsc": "tsc -p tsconfig.json" }, "eslintConfig": { "extends": "react-app" @@ -55,4 +56,4 @@ "not ie <= 11", "not op_mini all" ] -} \ No newline at end of file +} diff --git a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.jsx b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.jsx index 4804908f..40066661 100644 --- a/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.jsx +++ b/src-editor/src/Components/RulesEditor/components/Blocks/TriggerSchedule.jsx @@ -6,7 +6,7 @@ import { Schedule, I18n, } from '@iobroker/adapter-react-v5'; -import convertCronToText from '@iobroker/adapter-react-v5/Components/SimpleCron/cronText'; +import convertCronToText from '@iobroker/adapter-react-v5/build/Components/SimpleCron/cronText'; import GenericBlock from '../GenericBlock'; import Compile from '../../helpers/Compile'; diff --git a/src-editor/src/Components/RulesEditor/components/GenericBlock/index.jsx b/src-editor/src/Components/RulesEditor/components/GenericBlock/index.jsx index 97a917a3..c138373f 100644 --- a/src-editor/src/Components/RulesEditor/components/GenericBlock/index.jsx +++ b/src-editor/src/Components/RulesEditor/components/GenericBlock/index.jsx @@ -8,8 +8,8 @@ import { } from '@mui/material'; import { HelpOutline as IconHelp } from '@mui/icons-material'; -import { getSelectIdIcon } from '@iobroker/adapter-react-v5/Components/Icon'; import { + getSelectIdIcon, I18n, Utils, SelectID as DialogSelectID, Error as DialogError, diff --git a/src-editor/src/Components/RulesEditor/index.jsx b/src-editor/src/Components/RulesEditor/index.jsx index 0fecab56..dade3405 100644 --- a/src-editor/src/Components/RulesEditor/index.jsx +++ b/src-editor/src/Components/RulesEditor/index.jsx @@ -160,17 +160,13 @@ const RulesEditor = ({ allBlocks={allBlocks} socket={socket} /> - {importExport === 'export' ? ( + {modal ? (importExport === 'export' ? ( setModal(false)} - open={modal} text={JSON.stringify(userRules, null, 2)} /> ) : ( { setModal(false); if (text) { @@ -178,7 +174,7 @@ const RulesEditor = ({ } }} /> - )} + )) : null} {
= { buttonIcon: { marginRight: 8, }, @@ -41,8 +40,28 @@ const styles = { }, }; -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: [], @@ -54,9 +73,14 @@ class DialogAdapterDebug extends React.Component { }; } - componentDidMount() { - this.props.socket.getAdapterInstances().then(instances => { - instances = instances + 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\./, ''); @@ -69,17 +93,19 @@ class DialogAdapterDebug extends React.Component { }; }); 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; + let jsInstance: string = this.state.jsInstance || ''; + const jsInstanceObj = this.state.jsInstance + ? instances.find(item => item.id === this.state.jsInstance) + : null; + let jsInstanceHost: string; // 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 : ''; + const oJsInstance = instances.find(item => item.id.startsWith('javascript.')); // take the first one + jsInstanceHost = oJsInstance?.host || ''; + jsInstance = oJsInstance?.id || ''; } else { - jsInstanceHost = jsInstanceObj ? jsInstanceObj.host : ''; + jsInstanceHost = jsInstanceObj?.host || ''; } let adapterToDebug = this.state.adapterToDebug || ''; @@ -91,21 +117,23 @@ class DialogAdapterDebug extends React.Component { }); } - 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 => { - obj.common.enabled = false; - this.props.socket - .setObject(obj._id, obj) - .then(() => this.props.onDebug(this.state.jsInstance, this.state.adapterToDebug)); + 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; + void 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); + 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; @@ -115,8 +143,9 @@ class DialogAdapterDebug extends React.Component {
{I18n.t('Host')}
{js.map(item => ( - this.setState({ jsInstance: item.id, jsInstanceHost: item.host })} > @@ -128,14 +157,14 @@ class DialogAdapterDebug extends React.Component { /> - + ))} ); } - renderInstances() { + renderInstances(): React.JSX.Element { if (!this.state.jsInstance) { return ; } @@ -151,8 +180,8 @@ class DialogAdapterDebug extends React.Component {
{I18n.t('Instances')}
{instances.map(item => ( - this.setState({ adapterToDebug: item.id }, () => this.handleOk())} onClick={() => this.setState({ adapterToDebug: item.id })} @@ -165,14 +194,14 @@ class DialogAdapterDebug extends React.Component { /> - + ))}
); } - render() { + render(): React.JSX.Element { return ( = { card: { maxWidth: 345, minWidth: 250, @@ -44,25 +43,25 @@ const styles = { }, }; -class DialogAddNew extends React.Component { - handleCancel = () => { - this.props.onClose(); - }; +interface DialogAddNewProps { + onClose: (type?: 'TypeScript/ts' | 'Javascript/js' | 'Blockly' | 'Rules') => 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() { + getJSCard(): React.JSX.Element { return ( this.props.onClose && this.props.onClose('Javascript/js')}> @@ -90,7 +89,7 @@ class DialogAddNew extends React.Component { size="small" color="secondary" onClick={() => - this.openHtml( + DialogAddNew.openHtml( 'https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/javascript.md', ) } @@ -102,7 +101,7 @@ class DialogAddNew extends React.Component { ); } - getTSCard() { + getTSCard(): React.JSX.Element { return ( this.props.onClose && this.props.onClose('TypeScript/ts')}> @@ -130,7 +129,7 @@ class DialogAddNew extends React.Component { size="small" color="secondary" onClick={() => - this.openHtml( + DialogAddNew.openHtml( 'https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/javascript.md', ) } @@ -142,7 +141,7 @@ class DialogAddNew extends React.Component { ); } - getBlocklyCard() { + getBlocklyCard(): React.JSX.Element { return ( this.props.onClose && this.props.onClose('Blockly')}> @@ -170,7 +169,7 @@ class DialogAddNew extends React.Component { size="small" color="secondary" onClick={() => - this.openHtml( + DialogAddNew.openHtml( 'https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/blockly.md', ) } @@ -182,7 +181,7 @@ class DialogAddNew extends React.Component { ); } - getRulesCard() { + getRulesCard(): React.JSX.Element { return ( this.props.onClose && this.props.onClose('Rules')}> @@ -210,7 +209,7 @@ class DialogAddNew extends React.Component { size="small" color="secondary" onClick={() => - this.openHtml( + DialogAddNew.openHtml( 'https://github.com/ioBroker/ioBroker.javascript/blob/master/docs/en/javascript.md', ) } @@ -222,7 +221,7 @@ class DialogAddNew extends React.Component { ); } - render() { + render(): React.JSX.Element { return ( false} @@ -252,8 +251,4 @@ class DialogAddNew extends React.Component { } } -DialogAddNew.propTypes = { - onClose: PropTypes.func, -}; - export default DialogAddNew; diff --git a/src-editor/src/Dialogs/Cron.jsx b/src-editor/src/Dialogs/Cron.jsx deleted file mode 100644 index 3de5e751..00000000 --- a/src-editor/src/Dialogs/Cron.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Button, DialogTitle, DialogContent, DialogActions, Dialog, Radio } from '@mui/material'; - -import { Check as IconOk, Cancel as IconCancel } from '@mui/icons-material'; - -import { I18n } from '@iobroker/adapter-react-v5'; - -import ComplexCron from '../Components/ComplexCron'; -import SimpleCron from '../Components/simple-cron/SimpleCron'; -import Schedule from '../Components/Schedule'; - -// Generate cron expression - -const styles = { - dialogPaper: { - height: 'calc(100% - 96px)', - }, -}; - -class DialogCron extends React.Component { - constructor(props) { - super(props); - let cron; - if (this.props.cron && typeof this.props.cron === 'string' && this.props.cron.replace(/^["']/, '')[0] !== '{') { - cron = this.props.cron.replace(/['"]/g, '').trim(); - } else { - cron = this.props.cron || '{}'; - if (typeof cron === 'string') { - cron = cron.replace(/^["']/, '').replace(/["']\n?$/, ''); - } - } - - this.state = { - cron, - mode: this.props.simple - ? 'simple' - : typeof cron === 'object' || cron[0] === '{' - ? 'wizard' - : SimpleCron.cron2state(this.props.cron || '* * * * *') - ? 'simple' - : 'complex', - }; - } - - handleCancel() { - this.props.onClose(); - } - - handleOk() { - this.props.onOk(this.state.cron); - this.props.onClose(); - } - - setMode(mode) { - this.setState({ mode }); - } - - render() { - return ( - false} - maxWidth="md" - fullWidth - sx={{ '& .MuiDialog-paper': styles.dialogPaper }} - open={!0} - aria-labelledby="cron-dialog-title" - > - {this.props.title || I18n.t('Define schedule...')} - - {!this.props.simple && ( -
- this.setMode('wizard')} - /> - - - this.setMode('simple')} - /> - - this.setMode('complex')} - /> - -
- )} - - {this.state.mode === 'simple' && ( - this.setState({ cron })} - language={I18n.getLanguage()} - /> - )} - {this.state.mode === 'wizard' && ( - this.setState({ cron })} - language={I18n.getLanguage()} - /> - )} - {this.state.mode === 'complex' && ( - this.setState({ cron })} - language={I18n.getLanguage()} - /> - )} -
- - - - -
- ); - } -} - -DialogCron.propTypes = { - onClose: PropTypes.func, - onOk: PropTypes.func.isRequired, - title: PropTypes.string, - cron: PropTypes.string, - cancel: PropTypes.string, - ok: PropTypes.string, - simple: PropTypes.bool, - language: PropTypes.string, -}; - -export default DialogCron; diff --git a/src-editor/src/Dialogs/Delete.jsx b/src-editor/src/Dialogs/Delete.tsx similarity index 67% rename from src-editor/src/Dialogs/Delete.jsx rename to src-editor/src/Dialogs/Delete.tsx index 84d514f3..f43eaa1b 100644 --- a/src-editor/src/Dialogs/Delete.jsx +++ b/src-editor/src/Dialogs/Delete.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { Button, DialogTitle, DialogContent, DialogActions, Dialog } from '@mui/material'; @@ -7,8 +6,19 @@ import { Check as IconOk, Cancel as IconCancel, Delete as IconDelete } from '@mu 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, @@ -16,28 +26,32 @@ class DialogDelete extends React.Component { }; } - componentWillReceiveProps(nextProps) { - if (nextProps.name !== this.props.name) { - this.setState({ name: nextProps.name }); + static getDerivedStateFromProps( + props: DialogDeleteProps, + state: DialogDeleteState, + ): Partial | null { + if (props.name !== state.name) { + return { name: props.name }; } - if (nextProps.id !== this.props.id) { - this.setState({ id: nextProps.id }); + if (props.id !== state.id) { + return { id: props.id }; } + return null; } - 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() { + render(): React.JSX.Element { return ( false} + onClose={() => false} maxWidth="md" open={!0} aria-labelledby="confirmation-dialog-title" @@ -70,11 +84,4 @@ class DialogDelete extends React.Component { } } -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.jsx b/src-editor/src/Dialogs/Error.tsx similarity index 76% rename from src-editor/src/Dialogs/Error.jsx rename to src-editor/src/Dialogs/Error.tsx index 6d4822f5..ec2058ee 100644 --- a/src-editor/src/Dialogs/Error.jsx +++ b/src-editor/src/Dialogs/Error.tsx @@ -1,14 +1,13 @@ import React from 'react'; -import PropTypes from 'prop-types'; 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': { @@ -16,17 +15,18 @@ const styles = { }, }), }; +interface DialogErrorProps { + onClose: () => void; + title?: string; + text: string; +} -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() { + render(): React.JSX.Element { return ( = { textArea: { width: '100%', height: '100%', @@ -27,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, @@ -36,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; @@ -50,9 +61,9 @@ class DialogExport extends React.Component { }, 50); } - render() { + render(): React.JSX.Element { const file = new Blob([this.props.text], { type: 'application/xml' }); - const fileName = this.props.scriptId.substring('scripts.js'.length) + '.xml'; + const fileName = `${this.props.scriptId.substring('scripts.js'.length)}.xml`; return ( {I18n.t('Export selected blocks')} @@ -145,15 +156,4 @@ class DialogExport extends React.Component { } } -DialogExport.defaultProps = { - open: true, -}; - -DialogExport.propTypes = { - onClose: PropTypes.func, - text: PropTypes.string, - scriptId: PropTypes.string, - themeType: PropTypes.string, -}; - export default DialogExport; diff --git a/src-editor/src/Dialogs/Import.jsx b/src-editor/src/Dialogs/Import.tsx similarity index 65% rename from src-editor/src/Dialogs/Import.jsx rename to src-editor/src/Dialogs/Import.tsx index aa40832e..5ca8bbf5 100644 --- a/src-editor/src/Dialogs/Import.jsx +++ b/src-editor/src/Dialogs/Import.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import Dropzone from 'react-dropzone'; +import Dropzone, { type FileRejection } from 'react-dropzone'; import { Button, DialogTitle, DialogContent, DialogActions, Dialog } from '@mui/material'; @@ -9,7 +8,7 @@ import { MdFileUpload as IconUpload, MdCancel as IconNo, MdAdd as IconPlus } fro import { I18n } from '@iobroker/adapter-react-v5'; -const styles = { +const styles: Record = { textArea: { width: 'calc(100% - 10px)', height: '80%', @@ -73,23 +72,41 @@ const styles = { }, }; -class DialogImport extends React.Component { - constructor(props) { +interface DialogImportProps { + onClose: (text?: string) => void; +} + +interface DialogImportState { + text: string; + imageStatus: 'accepted' | 'rejected' | 'wait' | ''; + error?: string; +} + +class DialogImport extends React.Component { + constructor(props: DialogImportProps) { super(props); this.state = { text: '', + imageStatus: '', + error: '', }; } - componentDidMount() { + // eslint-disable-next-line class-methods-use-this + componentDidMount(): void { setTimeout(() => { try { - window.document.getElementById('import-text-area').focus(); - } catch (e) {} + window.document.getElementById('import-text-area')?.focus(); + } catch { + // ignore + } }, 100); } - static readFileDataUrl(file, cb) { + static readFileDataUrl( + file: File, + cb: (error: string | null, result?: { data: string | ArrayBuffer | null; name: string }) => void, + ): void { const reader = new FileReader(); reader.onload = () => { cb(null, { data: reader.result, name: file.name }); @@ -106,12 +123,8 @@ class DialogImport extends React.Component { reader.readAsText(file); } - handleDropFile(files) { - if (files && files.hasOwnProperty('target')) { - files = files.target.files; - } - - if (!files && !files.length) { + handleDropFile(files: File[]): void { + if (!files?.length) { return; } @@ -121,28 +134,31 @@ class DialogImport extends React.Component { return; } - DialogImport.readFileDataUrl(file, (err, result) => { - if (err) { - this.setState({ error: err }); - } else { - this.setState({ text: result.data }); - } - }); + DialogImport.readFileDataUrl( + file, + (err: string | null, result?: { data: string | ArrayBuffer | null; name: string }): void => { + if (err || !result) { + this.setState({ error: err || 'No data' }); + } else { + this.setState({ text: result.data?.toString() || '' }); + } + }, + ); } - handleCancel() { + handleCancel(): void { this.props.onClose(); } - handleOk() { + handleOk(): void { this.props.onClose(this.state.text); } - onChange(e) { + onChange(e: React.ChangeEvent): void { this.setState({ text: e.target.value }); } - render() { + render(): React.JSX.Element { const style = { ...styles.dropzone, ...(this.state.imageStatus === 'accepted' @@ -158,37 +174,29 @@ class DialogImport extends React.Component { maxWidth="lg" sx={{ '& .MuiDialog-paper': styles.dialog }} fullWidth - open={this.props.open} + open={!0} aria-labelledby="import-dialog-title" > {I18n.t('Import blocks')} -