Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/events-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/events-web/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
33 changes: 10 additions & 23 deletions packages/pluggableWidgets/events-web/src/Events.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -23,7 +23,6 @@ export default function Events(props: EventsContainerProps): ReactElement {
onEventChangeDelayParameterType,
onEventChangeDelayExpression
} = props;
const prevOnChangeAttributeValue = useRef<EditableValue<any> | undefined>(undefined);

const delayValue = useParameterValue({
parameterType: componentLoadDelayParameterType,
Expand All @@ -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 <div className={classnames("widget-events", className)}></div>;
}
48 changes: 0 additions & 48 deletions packages/pluggableWidgets/events-web/src/hooks/timer.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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]);
}
107 changes: 107 additions & 0 deletions packages/pluggableWidgets/events-web/src/hooks/useOnLoadTimer.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setTimeout> | 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]);
}
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/events-web/src/package.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="Events" version="1.1.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<clientModule name="Events" version="1.2.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="Events.xml" />
</widgetFiles>
Expand Down
Loading