Skip to content

Commit

Permalink
bring back type inference (#400)
Browse files Browse the repository at this point in the history
* bring back type inference

* also infer state correctly for rxwidget

* extend rxwidgetstate in single widgets

* further cleanup

* add check-ts step to github ci

---------

Signed-off-by: Bluefox <[email protected]>
Co-authored-by: Bluefox <[email protected]>
  • Loading branch information
foxriver76 and GermanBluefox authored Mar 12, 2024
1 parent b6bd813 commit e627ca0
Show file tree
Hide file tree
Showing 17 changed files with 61 additions and 87 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Build
run: NODE_OPTIONS=--max_old_space_size=8192 npm run build

- name: Test TypeScript
run: npm run check-ts

- name: Run local tests
run: npm run test-gui

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
"@alcalzone/release-script-plugin-iobroker": "^3.7.0",
"@alcalzone/release-script-plugin-license": "^3.7.0",
"@iobroker/vis-2-widgets-testing": "^1.0.0",
"@tsconfig/node16": "^16.1.1",
"@tsconfig/node18": "^18.2.2",
"@types/mocha": "^10.0.6",
"chai": "^4.3.10",
"gulp": "^4.0.2",
"iobroker.web": "*",
"mocha": "^10.3.0",
"typescript": "^5.4.2",
"unzipper": "^0.10.14"
},
"bugs": {
Expand All @@ -57,6 +58,7 @@
"main.js"
],
"scripts": {
"check-ts": "tsc --project src/tsconfig.json",
"start": "cd src && npm run start",
"test": "mocha ./test/*.engine.js --exit",
"test-gui": "mocha ./test/*.gui.js --exit",
Expand Down
5 changes: 3 additions & 2 deletions src/src/Toolbar/WidgetImportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { useFocus } from '@/Utils';
import { store } from '@/Store';
import {
AnyWidgetId,
GroupWidget, GroupWidgetId, Project, Widget,
} from '@/types';
import CustomAceEditor from '../Components/CustomAceEditor';
Expand Down Expand Up @@ -59,7 +60,7 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => {
newWidgets[newKey] = widget;
if (widget.grouped && widget.groupid && newWidgets[widget.groupid]?.data?.members) {
// find group
const pos = (newWidgets[widget.groupid] as GroupWidget).data.members.indexOf(widget._id as string);
const pos = (newWidgets[widget.groupid] as GroupWidget).data.members.indexOf(widget._id as AnyWidgetId);
if (pos !== -1) {
(newWidgets[widget.groupid] as GroupWidget).data.members[pos] = newKey;
}
Expand All @@ -73,7 +74,7 @@ const WidgetImportDialog = (props: WidgetImportDialogProps) => {
if (!isGroup(newWidgets[wid]) && props.selectedGroup !== undefined) {
newWidgets[wid].grouped = true;
newWidgets[wid].groupid = props.selectedGroup;
(project[props.selectedView].widgets[props.selectedGroup] as GroupWidget).data.members.push(wid);
(project[props.selectedView].widgets[props.selectedGroup] as GroupWidget).data.members.push(wid as AnyWidgetId);
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/src/Vis/Widgets/Basic/BasicBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default class BasicBar extends VisRxWidget<RxData> {
width: 200,
height: 130,
},
};
} as const;
}

/**
Expand Down
10 changes: 6 additions & 4 deletions src/src/Vis/Widgets/Basic/BasicFilterDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import { Edit } from '@mui/icons-material';

import { I18n, Icon } from '@iobroker/adapter-react-v5';

import { GetRxDataFromWidget, RxRenderWidgetProps, RxWidgetInfo } from '@/types';
import {
GetRxDataFromWidget, RxRenderWidgetProps, RxWidgetInfo, WidgetData,
} from '@/types';
import VisRxWidget from '@/Vis/visRxWidget';
import { WidgetData } from '@/types';
import { RxWidgetInfoAttributesField, RxWidgetInfoCustomComponentProperties, RxWidgetInfoCustomComponentContext } from '@/allInOneTypes';
import { VisWidgetCommand } from '@/Vis/visBaseWidget';
import FiltersEditorDialog from './FiltersEditorDialog';
import { RxWidgetInfoAttributesField, RxWidgetInfoCustomComponentProperties, RxWidgetInfoCustomComponentContext } from "@/allInOneTypes";

// eslint-disable-next-line no-use-before-define
type RxData = GetRxDataFromWidget<typeof BasicFilterDropdown>
Expand Down Expand Up @@ -281,7 +283,7 @@ class BasicFilterDropdown extends VisRxWidget<RxData> {
}
}

onCommand(command: string): void {
onCommand(command: VisWidgetCommand): void {
if (command === 'changeFilter') {
// analyse filter
this.forceUpdate();
Expand Down
18 changes: 3 additions & 15 deletions src/src/Vis/Widgets/Basic/BasicIFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,9 @@ import { GetRxDataFromWidget, RxRenderWidgetProps } from '@/types';
import VisRxWidget from '@/Vis/visRxWidget';

// eslint-disable-next-line no-use-before-define
// type RxData = GetRxDataFromWidget<typeof BasicIFrame>

interface BasicIFrameData {
src: string;
refreshInterval: number;
refreshWithNoQuery: boolean;
noSandbox: boolean;
refreshOnViewChange: boolean;
refreshOnWakeUp: boolean;
scrollX: boolean;
scrollY: boolean;
seamless: boolean;
}
type RxData = GetRxDataFromWidget<typeof BasicIFrame>

export default class BasicIFrame extends VisRxWidget<BasicIFrameData> {
export default class BasicIFrame extends VisRxWidget<RxData> {
private refreshInterval: ReturnType<typeof setInterval> | null = null;

private readonly frameRef: React.RefObject<HTMLIFrameElement>;
Expand Down Expand Up @@ -96,7 +84,7 @@ export default class BasicIFrame extends VisRxWidget<BasicIFrameData> {
width: 600,
height: 320,
},
};
} as const;
}

async componentDidMount(): Promise<void> {
Expand Down
16 changes: 3 additions & 13 deletions src/src/Vis/Widgets/Basic/BasicImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,9 @@ import { GetRxDataFromWidget, RxRenderWidgetProps } from '@/types';
import VisRxWidget from '@/Vis/visRxWidget';

// eslint-disable-next-line no-use-before-define
// type RxData = GetRxDataFromWidget<typeof BasicImage>

interface BasicImageData {
src: string;
stretch: boolean;
refreshInterval: number;
refreshOnWakeUp: boolean;
refreshOnViewChange: boolean;
refreshWithNoQuery: boolean;
allowUserInteractions: boolean;
}
type RxData = GetRxDataFromWidget<typeof BasicImage>

export default class BasicImage extends VisRxWidget<BasicImageData> {
export default class BasicImage extends VisRxWidget<RxData> {
private refreshInterval: ReturnType<typeof setInterval> | null = null;

private readonly imageRef: React.RefObject<HTMLImageElement>;
Expand Down Expand Up @@ -85,7 +75,7 @@ export default class BasicImage extends VisRxWidget<BasicImageData> {
width: 200,
height: 130,
},
};
} as const;
}

async componentDidMount(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/src/Vis/Widgets/Basic/BasicImage8.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default class BasicImage8 extends VisRxWidget<RxData> {
},
],
}],
};
} as const;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/src/Vis/Widgets/Basic/BasicRedNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class BasicRedNumber extends VisRxWidget<RxData> {
width: 52,
height: 30,
},
};
} as const;
}

/**
Expand Down Expand Up @@ -123,7 +123,7 @@ export default class BasicRedNumber extends VisRxWidget<RxData> {
top: 0,
left: 0,
zIndex: 0,
color: this.state.rxData.backgroundColor || 'red',
color: this.state.rxData.background || 'red',
}}
/>
<div
Expand Down Expand Up @@ -154,7 +154,7 @@ export default class BasicRedNumber extends VisRxWidget<RxData> {
borderColor: this.state.rxData.borderColor || 'white',
borderWidth: 3,
borderStyle: 'solid',
backgroundColor: this.state.rxData.backgroundColor || 'red',
backgroundColor: this.state.rxData.background || 'red',
minWidth: 21,
textAlign: 'center',
color: props.style.color || 'white',
Expand Down
18 changes: 9 additions & 9 deletions src/src/Vis/Widgets/Basic/BasicScreenResolution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import React from 'react';
import { I18n } from '@iobroker/adapter-react-v5';

import {
RxRenderWidgetProps, VisLegacy,
RxWidgetState, RxWidgetProps, GetRxDataFromWidget,
RxRenderWidgetProps, VisLegacy, RxWidgetProps, GetRxDataFromWidget,
} from '@/types';
import VisRxWidget from '@/Vis/visRxWidget';
import VisRxWidget, { VisRxWidgetState } from '@/Vis/visRxWidget';

declare global {
interface Window {
vis: VisLegacy;
}
}

interface BasicScreenResolutionState extends RxWidgetState {
interface BasicScreenResolutionState extends VisRxWidgetState {
width: number;
height: number;
defaultView: string;
Expand All @@ -30,9 +29,11 @@ export default class BasicScreenResolution extends VisRxWidget<RxData, BasicScre
constructor(props: RxWidgetProps) {
// @ts-expect-error refactor types to extend from parent types
super(props);
const state = this.state;
state.width = document.documentElement.clientWidth;
state.height = document.documentElement.clientHeight;
this.state = {
...this.state,
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
};
this.essentialData = JSON.stringify(this.buildEssentialProjectData());
}

Expand Down Expand Up @@ -63,7 +64,7 @@ export default class BasicScreenResolution extends VisRxWidget<RxData, BasicScre
width: 170,
height: 75,
},
};
} as const;
}

async componentDidMount(): Promise<void> {
Expand Down Expand Up @@ -122,7 +123,6 @@ export default class BasicScreenResolution extends VisRxWidget<RxData, BasicScre
defaultView = window.vis.findNearestResolution();
}
this.setState({
// @ts-expect-error unknown error
width,
height,
defaultView,
Expand Down
3 changes: 2 additions & 1 deletion src/src/Vis/Widgets/Basic/BasicSpeechToText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import type {
import { I18n } from '@iobroker/adapter-react-v5';
// @ts-expect-error fix import
import type * as SpeechRecognition from 'dom-speech-recognition';
import type { VisBaseWidgetState } from '@/Vis/visBaseWidget';

// eslint-disable-next-line no-use-before-define
type RxData = GetRxDataFromWidget<typeof BasicSpeechToText>

interface BasicSpeechToTextState {
interface BasicSpeechToTextState extends VisBaseWidgetState {
/** Current shown module text */
text: string;
/** Current shown image */
Expand Down
2 changes: 1 addition & 1 deletion src/src/Vis/Widgets/Basic/BasicSvgShape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default class BasicSvgShape extends VisRxWidget<RxData> {
width: 100,
height: 100,
},
};
} as const;
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/src/Vis/visBaseWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export interface VisBaseWidgetState {
widgetHint?: 'light' | 'dark' | 'hide';
hideHelper?: boolean;
isHidden?: boolean;
gap?: number,
gap?: number;
draggable?: boolean;
showRelativeMoveMenu?: boolean;
}
Expand Down Expand Up @@ -148,7 +148,7 @@ interface VisBaseWidget {
renderLastChange(style: unknown): React.ReactNode;
}

class VisBaseWidget extends React.Component<VisBaseWidgetProps, VisBaseWidgetState> {
class VisBaseWidget<TState extends Partial<VisBaseWidgetState> = VisBaseWidgetState> extends React.Component<VisBaseWidgetProps, TState & VisBaseWidgetState> {
static FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~]+/g; // from https://github.com/ioBroker/ioBroker.js-controller/blob/master/packages/common/lib/common/tools.js

/** We do not store the SVG Element in the state because it is cyclic */
Expand Down Expand Up @@ -219,7 +219,7 @@ class VisBaseWidget extends React.Component<VisBaseWidgetProps, VisBaseWidgetSta
),
hideHelper: false,
gap: style.position === 'relative' ? (isVarFinite(props.context.views[props.view].settings?.rowGap) ? parseFloat(props.context.views[props.view].settings?.rowGap as string) : 0) : 0,
};
} as TState & VisBaseWidgetState;

this.onCommandBound = this.onCommand.bind(this);
}
Expand Down Expand Up @@ -399,8 +399,8 @@ class VisBaseWidget extends React.Component<VisBaseWidgetProps, VisBaseWidgetSta
}

// take actual (old) style and data
let styleStr: string = state.style?._originalData ? state.style._originalData : JSON.stringify(state.style);
let dataStr: string = state.data?._originalData ? state.data._originalData : JSON.stringify(state.data);
const styleStr: string = state.style?._originalData ? state.style._originalData : JSON.stringify(state.style);
const dataStr: string = state.data?._originalData ? state.data._originalData : JSON.stringify(state.data);

const isHidden = VisBaseWidget.isWidgetFilteredOutStatic(
props.viewsActiveFilter,
Expand Down
25 changes: 6 additions & 19 deletions src/src/Vis/visRxWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ const POSSIBLE_MUI_STYLES = [
'word-spacing',
];

interface VisRxWidgetProps extends VisBaseWidgetProps {

}
type VisRxWidgetProps = VisBaseWidgetProps

interface RxData {
_originalData: any;
Expand Down Expand Up @@ -111,24 +109,14 @@ export interface CustomWidgetProperties {
selectedWidget: AnyWidgetId;
}

interface VisRxWidgetState extends VisBaseWidgetState {
export interface VisRxWidgetState extends VisBaseWidgetState {
rxData: RxData;
values: VisRxWidgetStateValues;
visible: boolean;
disabled?: boolean;
}

/** TODO: this overload can be removed as soon as VisBaseWidget is written correctly in TS */
interface VisRxWidget<TRxData extends Record<string, any>, TState extends Record<string, any> = Record<string, never>> extends VisBaseWidget {
visHidden?: boolean;
adapter?: string;
version?: string;
url?: string;
custom?: any;
state: VisRxWidgetState & TState & { rxData: TRxData };
}

class VisRxWidget<TRxData extends Record<string, any>, TState extends Record<string, any> = Record<string, never>> extends VisBaseWidget<TState>{
class VisRxWidget<TRxData extends Record<string, any>, TState extends Partial<VisRxWidgetState> = VisRxWidgetState> extends VisBaseWidget<VisRxWidgetState & TState & { rxData: TRxData }> {
static POSSIBLE_MUI_STYLES = POSSIBLE_MUI_STYLES;

static i18nPrefix: string | undefined;
Expand Down Expand Up @@ -432,7 +420,6 @@ class VisRxWidget<TRxData extends Record<string, any>, TState extends Record<str
}

async componentWillUnmount() {
// @ts-expect-error check later if types wrong or call wrong
if (this.linkContext.IDs.length) {
await this.props.context.socket.unsubscribeState(this.linkContext.IDs, this.onStateChangedBind);
}
Expand Down Expand Up @@ -510,14 +497,14 @@ class VisRxWidget<TRxData extends Record<string, any>, TState extends Record<str

// subscribe on some new IDs and remove old IDs
const unsubscribe = oldIDs.filter(id => !this.linkContext.IDs.includes(id));
// @ts-expect-error check later if types wrong or call wrong
if (unsubscribe.length) {
// @ts-expect-error check later if types wrong or call wrong
await context.socket.unsubscribeState(unsubscribe, this.onStateChangedBind);
}

const subscribe = this.linkContext.IDs.filter(id => !oldIDs.includes(id));
// @ts-expect-error check later if types wrong or call wrong
if (subscribe.length) {
// @ts-expect-error check later if types wrong or call wrong
await context.socket.subscribeState(subscribe, this.onStateChangedBind);
}

Expand Down Expand Up @@ -1028,7 +1015,7 @@ class VisRxWidget<TRxData extends Record<string, any>, TState extends Record<str
* Get information about specific widget, needs to be implemented by widget class
*/
// eslint-disable-next-line class-methods-use-this
getWidgetInfo(): Record<string, any> {
getWidgetInfo(): Readonly<RxWidgetInfo> {
throw new Error('not implemented');
}
}
Expand Down
Loading

0 comments on commit e627ca0

Please sign in to comment.