diff --git a/packages/pluggableWidgets/events-web/CHANGELOG.md b/packages/pluggableWidgets/events-web/CHANGELOG.md index c42b7ed812..e9c8aaf45d 100644 --- a/packages/pluggableWidgets/events-web/CHANGELOG.md +++ b/packages/pluggableWidgets/events-web/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue with burst executon in inactive tabs. + +### Changed + +- Repeated execution is not executing next action if previous execution is not yet finished. + ## [1.1.0] - 2025-08-12 ### Added diff --git a/packages/pluggableWidgets/events-web/package.json b/packages/pluggableWidgets/events-web/package.json index 88686fe32f..4e7eb35d78 100644 --- a/packages/pluggableWidgets/events-web/package.json +++ b/packages/pluggableWidgets/events-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/app-events-web", "widgetName": "Events", - "version": "1.1.0", + "version": "1.2.0", "description": "Events", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/events-web/src/Events.tsx b/packages/pluggableWidgets/events-web/src/Events.tsx index 892d909ce5..702d477907 100644 --- a/packages/pluggableWidgets/events-web/src/Events.tsx +++ b/packages/pluggableWidgets/events-web/src/Events.tsx @@ -1,8 +1,8 @@ import classnames from "classnames"; -import { EditableValue } from "mendix"; -import { ReactElement, useRef } from "react"; +import { ReactElement } from "react"; import { EventsContainerProps } from "../typings/EventsProps"; -import { useActionTimer } from "./hooks/timer"; +import { useOnLoadTimer } from "./hooks/useOnLoadTimer"; +import { useAttributeMonitor } from "./hooks/useAttributeMonitor"; import { useParameterValue } from "./hooks/parameterValue"; import "./ui/Events.scss"; @@ -23,7 +23,6 @@ export default function Events(props: EventsContainerProps): ReactElement { onEventChangeDelayParameterType, onEventChangeDelayExpression } = props; - const prevOnChangeAttributeValue = useRef | undefined>(undefined); const delayValue = useParameterValue({ parameterType: componentLoadDelayParameterType, @@ -41,33 +40,21 @@ export default function Events(props: EventsContainerProps): ReactElement { parameterExpression: onEventChangeDelayExpression }); - useActionTimer({ - canExecute: onComponentLoad?.canExecute, + useOnLoadTimer({ + canExecute: onComponentLoad ? onComponentLoad.canExecute && !onComponentLoad.isExecuting : false, execute: onComponentLoad?.execute, delay: delayValue, interval: intervalValue, repeat: componentLoadRepeat, attribute: undefined }); - useActionTimer({ - canExecute: onEventChange?.canExecute, - execute: () => { - if (onEventChangeAttribute?.status === "loading") { - return; - } - if (prevOnChangeAttributeValue?.current?.value === undefined) { - prevOnChangeAttributeValue.current = onEventChangeAttribute; - } else { - if (onEventChangeAttribute?.value !== prevOnChangeAttributeValue.current?.value) { - prevOnChangeAttributeValue.current = onEventChangeAttribute; - onEventChange?.execute(); - } - } - }, + + useAttributeMonitor({ + canExecute: onEventChange ? onEventChange.canExecute && !onEventChange.isExecuting : false, + execute: onEventChange?.execute, delay: onEventChangeDelayValue, - interval: 0, - repeat: false, attribute: onEventChangeAttribute }); + return
; } diff --git a/packages/pluggableWidgets/events-web/src/hooks/timer.ts b/packages/pluggableWidgets/events-web/src/hooks/timer.ts deleted file mode 100644 index cd1fd53216..0000000000 --- a/packages/pluggableWidgets/events-web/src/hooks/timer.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { EditableValue } from "mendix"; -import { useEffect, useState } from "react"; - -interface ActionTimerProps { - canExecute?: boolean; - execute?: () => void; - delay: number | undefined; - interval: number | undefined; - repeat: boolean; - attribute?: EditableValue; -} - -export function useActionTimer(props: ActionTimerProps): void { - const { canExecute, execute, delay, interval, repeat, attribute } = props; - const [toggleTimer, setToggleTimer] = useState(-1); - useEffect(() => { - // If the delay is set to undefined, we should not start a timer. - if (delay === undefined || delay < 0 || (interval === undefined && repeat)) { - return; - } - let counter: NodeJS.Timeout; - if (canExecute) { - if (repeat) { - counter = setInterval( - () => { - execute?.call(attribute); - - if (toggleTimer < 0) { - // this will clear delay timer and switch to interval timer. - setToggleTimer(1); - } - }, - toggleTimer < 0 ? delay : interval - ); - } else { - counter = setTimeout(() => { - execute?.call(attribute); - }, delay); - } - } - - return () => { - clearInterval(counter); - clearTimeout(counter); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [attribute, toggleTimer, delay, interval, canExecute]); -} diff --git a/packages/pluggableWidgets/events-web/src/hooks/useAttributeMonitor.ts b/packages/pluggableWidgets/events-web/src/hooks/useAttributeMonitor.ts new file mode 100644 index 0000000000..81660fd8d9 --- /dev/null +++ b/packages/pluggableWidgets/events-web/src/hooks/useAttributeMonitor.ts @@ -0,0 +1,94 @@ +import { EditableValue } from "mendix"; +import { useEffect, useState } from "react"; +import { debounce } from "@mendix/widget-plugin-platform/utils/debounce"; + +interface UseAttributeMonitorProps { + canExecute: boolean; + execute?: () => void; + delay: number | undefined; + attribute?: EditableValue; +} + +class AttributeMonitor { + private currentValue: EditableValue | undefined; + private canExecute = false; + private debouncedCallback: [() => void, () => void] | undefined; + + updateCallback(newCb?: () => void, delay?: number): void { + this.debouncedCallback?.[1](); // cancel previous one + if (newCb && delay !== undefined) { + this.debouncedCallback = debounce(newCb, delay); + } + } + + updateCanExecute(canExecute: boolean): void { + this.canExecute = canExecute; + } + + updateAttribute(newValue?: EditableValue): void { + if (newValue === undefined || newValue.status === "unavailable") { + // value is not present at all or not available, do nothing. + return; + } + + if (newValue.status === "loading") { + // value is still loading, wait for it + return; + } + + if (this.currentValue === undefined) { + // this is the first load, as the current value is absent + // remember the value and wait for next updates + this.currentValue = newValue; + + return; + } + + // we got new value, compare it to the current one + // and execute callback if it changed. + if (this.currentValue.value !== newValue.value) { + // todo: execute debounced + // onEventChange?.execute(); + this.trigger(); + } + + // remember the value for the next time + this.currentValue = newValue; + } + + trigger(): void { + if (this.canExecute) { + this.debouncedCallback?.[0](); + } + } + + stop(): void { + // drop all pending executions + this.debouncedCallback?.[1](); + } +} + +export function useAttributeMonitor(props: UseAttributeMonitorProps): void { + const [attributeMonitor] = useState(() => new AttributeMonitor()); + + // update canExecute props + useEffect(() => { + attributeMonitor.updateCanExecute(props.canExecute); + }, [attributeMonitor, props.canExecute]); + + // update callback props + useEffect(() => { + attributeMonitor.updateCallback(props.execute, props.delay); + }, [attributeMonitor, props.execute, props.delay]); + + useEffect(() => { + attributeMonitor.updateAttribute(props.attribute); + }, [attributeMonitor, props.attribute]); + + // cleanup + useEffect(() => { + return () => { + attributeMonitor.stop(); + }; + }, [attributeMonitor]); +} diff --git a/packages/pluggableWidgets/events-web/src/hooks/useOnLoadTimer.ts b/packages/pluggableWidgets/events-web/src/hooks/useOnLoadTimer.ts new file mode 100644 index 0000000000..d317fda2de --- /dev/null +++ b/packages/pluggableWidgets/events-web/src/hooks/useOnLoadTimer.ts @@ -0,0 +1,107 @@ +import { EditableValue } from "mendix"; +import { useEffect, useState } from "react"; + +interface UseOnLoadTimerProps { + canExecute: boolean; + execute?: () => void; + delay: number | undefined; + interval: number | undefined; + repeat: boolean; + attribute?: EditableValue; +} + +class TimerExecutor { + private intervalHandle: ReturnType | undefined; + private isFirstTime: boolean = true; + private isPendingExecution: boolean = false; + private canExecute: boolean = false; + + private delay?: number; + private interval?: number; + private repeat?: boolean; + + private callback?: () => void; + + setCallback(callback: () => void, canExecute: boolean): void { + this.callback = callback; + this.canExecute = canExecute; + + this.trigger(); + } + + setParams(delay: number | undefined, interval: number | undefined, repeat: boolean): void { + this.delay = delay; + this.interval = interval; + this.repeat = repeat; + + this.next(); + } + + get isReady(): boolean { + return this.delay !== undefined && (!this.repeat || this.interval !== undefined); + } + + next(): void { + if (!this.isReady) { + return; + } + + if (!this.isFirstTime && !this.repeat) { + // we did execute it once, and we don't need to repeat + // so do nothing + return; + } + + // schedule a timer + this.intervalHandle = setTimeout( + () => { + this.isPendingExecution = true; + this.trigger(); + this.isFirstTime = false; + this.next(); + }, + this.isFirstTime ? this.delay : this.interval + ); + } + + trigger(): void { + if (this.isPendingExecution && this.canExecute) { + this.isPendingExecution = false; + this.callback?.(); + } + } + + stop(): void { + clearTimeout(this.intervalHandle); + this.intervalHandle = undefined; + this.delay = undefined; + this.interval = undefined; + this.repeat = false; + } +} + +export function useOnLoadTimer(props: UseOnLoadTimerProps): void { + const { canExecute, execute, delay, interval, repeat, attribute } = props; + + const [timerExecutor] = useState(() => new TimerExecutor()); + + // update callback props + useEffect(() => { + timerExecutor.setCallback(() => execute?.call(attribute), canExecute); + }, [timerExecutor, execute, attribute, canExecute]); + + // update interval props + useEffect(() => { + timerExecutor.setParams(delay, interval, repeat); + return () => { + timerExecutor.stop(); + }; + }, [timerExecutor, delay, interval, repeat]); + + // cleanup + useEffect(() => { + return () => { + timerExecutor.stop(); + }; + }, [timerExecutor]); +} diff --git a/packages/pluggableWidgets/events-web/src/package.xml b/packages/pluggableWidgets/events-web/src/package.xml index c3fc680b31..05f535d834 100644 --- a/packages/pluggableWidgets/events-web/src/package.xml +++ b/packages/pluggableWidgets/events-web/src/package.xml @@ -1,6 +1,6 @@ - +