From c14d3bb26cbb53dd2e774f2139f3b6dad0af53ea Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Thu, 21 May 2026 17:25:29 +0200 Subject: [PATCH 1/6] chore(pusher-web): initial implementation --- .../pusher-web/.prettierrc.js | 5 + .../pluggableWidgets/pusher-web/CHANGELOG.md | 11 ++ .../pluggableWidgets/pusher-web/README.md | 1 + .../pusher-web/eslint.config.mjs | 3 + .../pluggableWidgets/pusher-web/package.json | 62 +++++++ .../pusher-web/src/Pusher.editorConfig.ts | 31 ++++ .../pusher-web/src/Pusher.editorPreview.tsx | 9 + .../pusher-web/src/Pusher.tsx | 50 ++++++ .../pusher-web/src/Pusher.xml | 24 +++ .../pusher-web/src/__tests__/Pusher.spec.tsx | 7 + .../pusher-web/src/hooks/usePusherConfig.ts | 55 ++++++ .../pusher-web/src/hooks/usePusherListener.ts | 51 ++++++ .../pusher-web/src/package.xml | 11 ++ .../pusher-web/src/ui/Pusher.scss | 0 .../pusher-web/src/ui/PusherPreview.css | 6 + .../pusher-web/src/utils/PusherListener.ts | 127 ++++++++++++++ .../pusher-web/src/utils/useMxObjectInfo.ts | 32 ++++ .../pluggableWidgets/pusher-web/tsconfig.json | 30 ++++ .../pusher-web/typings/PusherProps.d.ts | 33 ++++ pnpm-lock.yaml | 166 ++++++++++++------ 20 files changed, 658 insertions(+), 56 deletions(-) create mode 100644 packages/pluggableWidgets/pusher-web/.prettierrc.js create mode 100644 packages/pluggableWidgets/pusher-web/CHANGELOG.md create mode 100644 packages/pluggableWidgets/pusher-web/README.md create mode 100644 packages/pluggableWidgets/pusher-web/eslint.config.mjs create mode 100644 packages/pluggableWidgets/pusher-web/package.json create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/Pusher.xml create mode 100644 packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/package.xml create mode 100644 packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss create mode 100644 packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts create mode 100644 packages/pluggableWidgets/pusher-web/tsconfig.json create mode 100644 packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts diff --git a/packages/pluggableWidgets/pusher-web/.prettierrc.js b/packages/pluggableWidgets/pusher-web/.prettierrc.js new file mode 100644 index 0000000000..13dc01f67f --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/.prettierrc.js @@ -0,0 +1,5 @@ +const base = require("@mendix/prettier-config-web-widgets"); + +module.exports = { + ...base +}; diff --git a/packages/pluggableWidgets/pusher-web/CHANGELOG.md b/packages/pluggableWidgets/pusher-web/CHANGELOG.md new file mode 100644 index 0000000000..d3fa2771ff --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this widget will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial widget scaffolding diff --git a/packages/pluggableWidgets/pusher-web/README.md b/packages/pluggableWidgets/pusher-web/README.md new file mode 100644 index 0000000000..cdcf2addda --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/README.md @@ -0,0 +1 @@ +# Pusher Widget diff --git a/packages/pluggableWidgets/pusher-web/eslint.config.mjs b/packages/pluggableWidgets/pusher-web/eslint.config.mjs new file mode 100644 index 0000000000..ed68ae9e78 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/eslint.config.mjs @@ -0,0 +1,3 @@ +import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs"; + +export default config; diff --git a/packages/pluggableWidgets/pusher-web/package.json b/packages/pluggableWidgets/pusher-web/package.json new file mode 100644 index 0000000000..21b64c822e --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/package.json @@ -0,0 +1,62 @@ +{ + "name": "@mendix/pusher-web", + "widgetName": "Pusher", + "version": "2.0.0", + "description": "Pusher.com integration widget for real-time communication", + "copyright": "© Mendix Technology BV 2026. All rights reserved.", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/mendix/web-widgets.git" + }, + "config": { + "developmentPort": 3000, + "mendixHost": "http://localhost:8080" + }, + "mxpackage": { + "name": "Pusher", + "type": "widget", + "mpkName": "com.mendix.widget.web.Pusher.mpk" + }, + "packagePath": "com.mendix.widget.web", + "marketplace": { + "minimumMXVersion": "11.11.0", + "appName": "Pusher", + "reactReady": true + }, + "testProject": { + "githubUrl": "https://github.com/mendix/testProjects", + "branchName": "pusher-web" + }, + "scripts": { + "build": "pluggable-widgets-tools build:web", + "create-gh-release": "rui-create-gh-release", + "create-translation": "rui-create-translation", + "dev": "pluggable-widgets-tools start:web", + "e2e": "echo \"Skipping this e2e test\"", + "e2edev": "run-e2e dev --with-preps", + "format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .", + "lint": "eslint src/ package.json", + "release": "pluggable-widgets-tools release:web", + "start": "pluggable-widgets-tools start:server", + "test": "pluggable-widgets-tools test:unit:web", + "update-changelog": "rui-update-changelog-widget", + "verify": "rui-verify-package-format" + }, + "dependencies": { + "classnames": "^2.5.1", + "pusher-js": "^8.5.0" + }, + "devDependencies": { + "@mendix/automation-utils": "workspace:*", + "@mendix/eslint-config-web-widgets": "workspace:*", + "@mendix/pluggable-widgets-tools": "*", + "@mendix/prettier-config-web-widgets": "workspace:*", + "@mendix/run-e2e": "workspace:^*", + "@mendix/widget-plugin-component-kit": "workspace:*", + "@mendix/widget-plugin-hooks": "workspace:*", + "@mendix/widget-plugin-platform": "workspace:*", + "@mendix/widget-plugin-test-utils": "workspace:*", + "cross-env": "^7.0.3" + } +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts new file mode 100644 index 0000000000..1a7e31068b --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts @@ -0,0 +1,31 @@ +import { Properties } from "@mendix/pluggable-widgets-tools"; +import { + container, + rowLayout, + structurePreviewPalette, + StructurePreviewProps, + text +} from "@mendix/widget-plugin-platform/preview/structure-preview-api"; +import { PusherPreviewProps } from "../typings/PusherProps"; + +export function getProperties(_values: PusherPreviewProps, defaultProperties: Properties): Properties { + return defaultProperties; +} + +export function getPreview(values: PusherPreviewProps, isDarkMode: boolean): StructurePreviewProps { + const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"]; + + return rowLayout({ columnSize: "grow", borders: true, backgroundColor: palette.background.containerFill })( + container()(), + rowLayout({ grow: 2, padding: 8 })(text({ fontColor: palette.text.primary, grow: 10 })(getCaption(values))), + container()() + ); +} + +export function getCustomCaption(values: PusherPreviewProps): string { + return getCaption(values); +} + +export function getCaption(values: PusherPreviewProps): string { + return `Pusher widget [${values.notifyChannelName}]`; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx new file mode 100644 index 0000000000..9e83c4b766 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorPreview.tsx @@ -0,0 +1,9 @@ +import classNames from "classnames"; +import { ReactElement } from "react"; +import { PusherPreviewProps } from "typings/PusherProps"; +import { getCaption } from "./Pusher.editorConfig"; +import "./ui/PusherPreview.css"; + +export function preview(props: PusherPreviewProps): ReactElement { + return
{getCaption(props)}
; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx new file mode 100644 index 0000000000..3c617a0c44 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -0,0 +1,50 @@ +import classnames from "classnames"; +import { ReactElement, useCallback, useMemo } from "react"; +import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; +import { PusherContainerProps } from "../typings/PusherProps"; +import { usePusherListener } from "./hooks/usePusherListener"; +import "./ui/Pusher.scss"; +import { useMxObjectInfo } from "./utils/useMxObjectInfo"; + +export default function Pusher(props: PusherContainerProps): ReactElement { + const { class: className, objectSource, notifyChannelName, notifyAction } = props; + + // Extract object GUID and entity name from data source + const mxObjectInfo = useMxObjectInfo(objectSource as any); // TODO: fix typings when PWT updated. + + // Event callback - triggered when Pusher event is received + const handleEvent = useCallback( + (data: unknown) => { + console.debug("[Pusher] Event received:", data); + + // Execute configured action + executeAction(notifyAction); + }, + [notifyAction] + ); + + // Error callback + const handleError = useCallback((error: Error) => { + console.error("[Pusher] Subscription error:", error.message); + }, []); + + // Setup stable subscription config + const subscription = useMemo(() => { + if (!mxObjectInfo) { + return undefined; + } + + return { + entityName: mxObjectInfo.entityName, + guid: mxObjectInfo.guid, + eventName: notifyChannelName, + onEvent: handleEvent, + onError: handleError + }; + }, [mxObjectInfo, handleEvent, handleError, notifyChannelName]); + + // Initialize Pusher listener + usePusherListener(subscription); + + return
; +} diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.xml b/packages/pluggableWidgets/pusher-web/src/Pusher.xml new file mode 100644 index 0000000000..ba46794ffd --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.xml @@ -0,0 +1,24 @@ + + + Pusher + Listen to Notify server action and perform client side action + https://docs.mendix.com/appstore/widgets/pusher + + + + Object to listen + + + + + Notify event name + The name should match the with the 'Notify' parameter `EventName` + + + + Action + + + + + diff --git a/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx new file mode 100644 index 0000000000..aafb61a263 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/__tests__/Pusher.spec.tsx @@ -0,0 +1,7 @@ +describe("Pusher", () => { + // TODO: Add comprehensive unit tests for: + // - PusherListener class (connection, subscription, cleanup) + // - usePusherConfig hook (fetching config) + // - usePusherListener hook (React lifecycle integration) + // - Event handling and action execution +}); diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts new file mode 100644 index 0000000000..b20b53c84a --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { PusherConfig } from "../utils/PusherListener"; + +interface KeyData { + key: string; + cluster: string; +} + +/** + * Fetch Pusher configuration from backend + * Returns null while loading or on error + */ +export function usePusherConfig(): PusherConfig | null { + const [config, setConfig] = useState(null); + + useEffect(() => { + const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; + const endpoint = `${baseUrl}rest/pusher/key`; + const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; + + const authEndpoint = `${baseUrl}rest/pusher/auth`; + + fetch(endpoint, { + method: "GET", + credentials: "same-origin", + headers: { + "X-Csrf-Token": csrfToken + } + }) + .then(response => { + if (response.status !== 200) { + throw new Error(`Failed to fetch Pusher key: HTTP ${response.status}`); + } + return response.text(); + }) + .then(data => { + const keyData = JSON.parse(data) as KeyData; + if (!keyData.key || !keyData.cluster) { + throw new Error("Invalid Pusher key data: missing key or cluster"); + } + setConfig({ + key: keyData.key, + cluster: keyData.cluster, + authEndpoint, + csrfToken + }); + }) + .catch(error => { + console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); + setConfig(null); + }); + }, []); + + return config; +} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts new file mode 100644 index 0000000000..193011f20c --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from "react"; +import { usePusherConfig } from "./usePusherConfig"; +import { PusherListener, SubscriptionConfig } from "../utils/PusherListener"; + +/** + * React hook to manage Pusher listener lifecycle + * Automatically handles initialization, subscription changes, and cleanup + */ +export function usePusherListener(subscription?: SubscriptionConfig): void { + const listenerRef = useRef(null); + + // Fetch Pusher config from backend + const pusherConfig = usePusherConfig(); + + const enabled = !!pusherConfig && !!subscription; + + // Initialize PusherListener once when config is available + useEffect(() => { + if (!enabled) { + return; + } + + const listener = new PusherListener(pusherConfig); + listenerRef.current = listener; + + listener.initialize().catch(error => { + console.error("[usePusherListener] Failed to initialize:", error); + }); + + // Cleanup on unmount or when config changes + return () => { + listener.destroy(); + listenerRef.current = null; + }; + }, [pusherConfig, enabled]); + + // Subscribe/unsubscribe based on subscription config changes + useEffect(() => { + const listener = listenerRef.current; + if (!listener || !subscription) { + return; + } + + listener.subscribe(subscription); + + // Unsubscribe on cleanup or when subscription changes + return () => { + listener.unsubscribe(); + }; + }, [subscription]); +} diff --git a/packages/pluggableWidgets/pusher-web/src/package.xml b/packages/pluggableWidgets/pusher-web/src/package.xml new file mode 100644 index 0000000000..414872761b --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/package.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss b/packages/pluggableWidgets/pusher-web/src/ui/Pusher.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css b/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css new file mode 100644 index 0000000000..c9a0020357 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/ui/PusherPreview.css @@ -0,0 +1,6 @@ +.widget-pusher-preview { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts new file mode 100644 index 0000000000..0da56ed040 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -0,0 +1,127 @@ +import Pusher, { Channel } from "pusher-js"; + +export interface PusherConfig { + key: string; + cluster: string; + authEndpoint: string; + csrfToken: string; +} + +export interface SubscriptionConfig { + entityName: string; + guid: string; + eventName: string; + onEvent: (data: unknown) => void; + onError?: (error: Error) => void; +} + +export class PusherListener { + private pusher: Pusher | null = null; + private currentChannel: Channel | null = null; + private currentChannelName: string | null = null; + private currentEventName: string | null = null; + + constructor(private config: PusherConfig) {} + + /** + * Initialize Pusher connection + * Should be called once on widget mount + */ + async initialize(): Promise { + if (this.pusher) { + return; // Already initialized + } + + this.pusher = new Pusher(this.config.key, { + cluster: this.config.cluster, + authEndpoint: this.config.authEndpoint, + auth: { + headers: { + "X-Csrf-Token": this.config.csrfToken + } + } + }); + + // Setup connection event handlers + this.pusher.connection.bind("error", this.handleConnectionError); + this.pusher.connection.bind("state_change", this.handleStateChange); + } + + /** + * Subscribe to channel for specific object and event + * Automatically unsubscribes from previous channel if different + */ + subscribe(config: SubscriptionConfig): void { + if (!this.pusher) { + throw new Error("PusherListener not initialized. Call initialize() first."); + } + + const channelName = this.buildChannelName(config.entityName, config.guid); + + // If already subscribed to same channel and event, do nothing + if (channelName === this.currentChannelName && config.eventName === this.currentEventName) { + return; + } + + // Unsubscribe from previous channel if exists + this.unsubscribe(); + + // Subscribe to new channel + this.currentChannelName = channelName; + this.currentEventName = config.eventName; + this.currentChannel = this.pusher.subscribe(channelName); + + // Bind event handler + this.currentChannel.bind(config.eventName, config.onEvent); + + // Bind error handler + this.currentChannel.bind("pusher:subscription_error", (error: unknown) => { + console.error(error); + const errorMsg = + error === 515 + ? "Authentication failed. Please verify Pusher configuration constants." + : `Subscription error: ${String(error)}`; + config.onError?.(new Error(errorMsg)); + }); + } + + /** + * Unsubscribe from current channel + */ + unsubscribe(): void { + if (this.currentChannel && this.currentChannelName) { + // Unbind event handler before unsubscribing + if (this.currentEventName) { + this.currentChannel.unbind(this.currentEventName); + } + this.pusher?.unsubscribe(this.currentChannelName); + this.currentChannel = null; + this.currentChannelName = null; + this.currentEventName = null; + } + } + + /** + * Disconnect and cleanup + * Should be called on widget unmount + */ + destroy(): void { + this.unsubscribe(); + if (this.pusher) { + this.pusher.disconnect(); + this.pusher = null; + } + } + + private buildChannelName(entityName: string, guid: string): string { + return `private-${entityName}.${guid}`; + } + + private handleConnectionError = (error: unknown): void => { + console.error("[PusherListener] Connection error:", error); + }; + + private handleStateChange = (states: { previous: string; current: string }): void => { + console.debug(`[PusherListener] State changed: ${states.previous} → ${states.current}`); + }; +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts b/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts new file mode 100644 index 0000000000..9ebc31771e --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/useMxObjectInfo.ts @@ -0,0 +1,32 @@ +import { DynamicValue, ObjectItem } from "mendix"; +import { useMemo } from "react"; + +interface MxObjectInfo { + guid: string; + entityName: string; +} + +export function useMxObjectInfo(objectSource: DynamicValue): MxObjectInfo | undefined { + const object = (objectSource as any)?.value as ObjectItem | undefined; + + const guid = object?.id; + const entityName = object ? extractEntityName(object) : undefined; + return useMemo(() => { + if (!guid || !entityName) { + return undefined; + } + + return { + guid, + entityName + }; + }, [guid, entityName]); +} + +function extractEntityName(object: ObjectItem): string { + const mxObj = (object as any)[Object.getOwnPropertySymbols(object)[0]]; + if (!mxObj) { + throw new Error("Unable to extract entity name. mxObject was not found."); + } + return mxObj.getEntity(); +} diff --git a/packages/pluggableWidgets/pusher-web/tsconfig.json b/packages/pluggableWidgets/pusher-web/tsconfig.json new file mode 100644 index 0000000000..7aa60df0c9 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": ["./src", "./typings"], + "compilerOptions": { + "baseUrl": "./", + "noEmitOnError": true, + "sourceMap": true, + "module": "esnext", + "target": "es6", + "lib": ["esnext", "dom"], + "types": ["jest", "node"], + "moduleResolution": "node", + "declaration": false, + "noLib": false, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictFunctionTypes": false, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "useUnknownInCatchVariables": false, + "exactOptionalPropertyTypes": false, + "paths": { + "react-hot-loader/root": ["./hot-typescript.ts"] + } + } +} diff --git a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts new file mode 100644 index 0000000000..1db883cffb --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts @@ -0,0 +1,33 @@ +/** + * This file was generated from Pusher.xml + * WARNING: All changes made to this file will be overwritten + * @author Mendix Widgets Framework Team + */ +import { CSSProperties } from "react"; +import { ActionValue, ListValue } from "mendix"; + +export interface PusherContainerProps { + name: string; + class: string; + style?: CSSProperties; + tabIndex?: number; + objectSource: ListValue; + notifyChannelName: string; + notifyAction?: ActionValue; +} + +export interface PusherPreviewProps { + /** + * @deprecated Deprecated since version 9.18.0. Please use class property instead. + */ + className: string; + class: string; + style: string; + styleObject?: CSSProperties; + readOnly: boolean; + renderMode: "design" | "xray" | "structure"; + translate: (text: string) => string; + objectSource: {} | { caption: string } | { type: string } | null; + notifyChannelName: string; + notifyAction: {} | null; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb1ef83ce6..9b7875ba7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2077,6 +2077,46 @@ importers: specifier: ^7.0.3 version: 7.0.3 + packages/pluggableWidgets/pusher-web: + dependencies: + classnames: + specifier: ^2.5.1 + version: 2.5.1 + pusher-js: + specifier: ^8.5.0 + version: 8.5.0 + devDependencies: + '@mendix/automation-utils': + specifier: workspace:* + version: link:../../../automation/utils + '@mendix/eslint-config-web-widgets': + specifier: workspace:* + version: link:../../shared/eslint-config-web-widgets + '@mendix/pluggable-widgets-tools': + specifier: 11.8.0 + version: 11.8.0(@jest/transform@29.7.0)(@jest/types@30.2.0)(@swc/core@1.13.5)(@types/babel__core@7.20.5)(@types/node@22.14.1)(eslint@9.39.3(jiti@2.6.1))(jest-util@30.2.0)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.82.0(@babel/core@7.29.0)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + '@mendix/prettier-config-web-widgets': + specifier: workspace:* + version: link:../../shared/prettier-config-web-widgets + '@mendix/run-e2e': + specifier: workspace:^* + version: link:../../../automation/run-e2e + '@mendix/widget-plugin-component-kit': + specifier: workspace:* + version: link:../../shared/widget-plugin-component-kit + '@mendix/widget-plugin-hooks': + specifier: workspace:* + version: link:../../shared/widget-plugin-hooks + '@mendix/widget-plugin-platform': + specifier: workspace:* + version: link:../../shared/widget-plugin-platform + '@mendix/widget-plugin-test-utils': + specifier: workspace:* + version: link:../../shared/widget-plugin-test-utils + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + packages/pluggableWidgets/range-slider-web: dependencies: '@mendix/widget-plugin-component-kit': @@ -3401,12 +3441,6 @@ packages: peerDependencies: '@babel/core': 7.29.0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-typescript@7.28.6': resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} @@ -9347,6 +9381,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pusher-js@8.5.0: + resolution: {integrity: sha512-V7uzGi9bqOOOyM/6IkJdpFyjGZj7llz1v0oWnYkZKcYLvbz6VcHVLmzKqkvegjuMumpfIEKGLmWHwFb39XFCpw==} + qrcode.react@4.2.0: resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} peerDependencies: @@ -10472,6 +10509,9 @@ packages: resolution: {integrity: sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA==} hasBin: true + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -10632,6 +10672,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -11012,8 +11053,8 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -11230,22 +11271,22 @@ snapshots: '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': dependencies: @@ -11270,67 +11311,62 @@ snapshots: '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: @@ -11873,8 +11909,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@babel/template@7.28.6': dependencies: @@ -12615,7 +12651,7 @@ snapshots: identity-obj-proxy: 3.0.0 jasmine: 3.99.0 jasmine-core: 3.99.1 - jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.14.1) jest-environment-jsdom: 29.7.0 jest-jasmine2: 29.7.0 jest-junit: 13.2.0 @@ -13290,7 +13326,7 @@ snapshots: react-test-renderer: 19.2.4(react@18.3.1) redent: 3.0.0 optionalDependencies: - jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.14.1) '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -13354,24 +13390,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.29.0 '@types/big.js@6.2.2': {} @@ -14118,7 +14154,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -14128,8 +14164,8 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 @@ -14331,7 +14367,7 @@ snapshots: caniuse-api@3.0.0: dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-lite: 1.0.30001750 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 @@ -16589,7 +16625,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -16599,7 +16635,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.7.4 @@ -16973,10 +17009,10 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.29.0 - '@babel/generator': 7.28.3 + '@babel/generator': 7.29.1 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0) - '@babel/types': 7.28.4 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -17046,6 +17082,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@29.7.0(@types/node@22.14.1): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) @@ -18227,7 +18275,7 @@ snapshots: postcss-colormin@5.3.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 colord: 2.9.3 postcss: 8.5.6 @@ -18235,7 +18283,7 @@ snapshots: postcss-convert-values@5.1.3(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18285,7 +18333,7 @@ snapshots: postcss-merge-rules@5.1.4(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 cssnano-utils: 3.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -18305,7 +18353,7 @@ snapshots: postcss-minify-params@5.1.4(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 cssnano-utils: 3.1.0(postcss@8.5.6) postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18379,7 +18427,7 @@ snapshots: postcss-normalize-unicode@5.1.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 @@ -18402,7 +18450,7 @@ snapshots: postcss-reduce-initial@5.1.2(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 caniuse-api: 3.0.0 postcss: 8.5.6 @@ -18576,6 +18624,10 @@ snapshots: pure-rand@6.1.0: {} + pusher-js@8.5.0: + dependencies: + tweetnacl: 1.0.3 + qrcode.react@4.2.0(react@18.3.1): dependencies: react: 18.3.1 @@ -19683,7 +19735,7 @@ snapshots: stylehacks@5.1.1(postcss@8.5.6): dependencies: - browserslist: 4.26.3 + browserslist: 4.28.1 postcss: 8.5.6 postcss-selector-parser: 6.1.2 @@ -19882,7 +19934,7 @@ snapshots: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.14.1) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -19970,6 +20022,8 @@ snapshots: turbo-windows-64: 2.8.16 turbo-windows-arm64: 2.8.16 + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 From a8c5c654b649bff331f6acdfae6f1c61dd8ece5f Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 10:47:56 +0200 Subject: [PATCH 2/6] fix(pusher-web): depend on enable to react on config changes --- .../pusher-web/src/hooks/usePusherListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts index 193011f20c..4a269c017e 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -37,7 +37,7 @@ export function usePusherListener(subscription?: SubscriptionConfig): void { // Subscribe/unsubscribe based on subscription config changes useEffect(() => { const listener = listenerRef.current; - if (!listener || !subscription) { + if (!enabled || !listener) { return; } @@ -47,5 +47,5 @@ export function usePusherListener(subscription?: SubscriptionConfig): void { return () => { listener.unsubscribe(); }; - }, [subscription]); + }, [enabled, subscription]); } From 23a159b7df055be32e06a6aceb2cbbcd443c6ea8 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 11:05:56 +0200 Subject: [PATCH 3/6] fix(pusher-web): abort key fetching on unmount --- .../pusher-web/src/hooks/usePusherConfig.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts index b20b53c84a..6637657031 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts @@ -14,6 +14,8 @@ export function usePusherConfig(): PusherConfig | null { const [config, setConfig] = useState(null); useEffect(() => { + let active = true; + const controller = new AbortController(); const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; const endpoint = `${baseUrl}rest/pusher/key`; const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; @@ -25,7 +27,8 @@ export function usePusherConfig(): PusherConfig | null { credentials: "same-origin", headers: { "X-Csrf-Token": csrfToken - } + }, + signal: controller.signal }) .then(response => { if (response.status !== 200) { @@ -34,6 +37,9 @@ export function usePusherConfig(): PusherConfig | null { return response.text(); }) .then(data => { + if (!active) { + return; + } const keyData = JSON.parse(data) as KeyData; if (!keyData.key || !keyData.cluster) { throw new Error("Invalid Pusher key data: missing key or cluster"); @@ -46,9 +52,17 @@ export function usePusherConfig(): PusherConfig | null { }); }) .catch(error => { + if (!active || (error instanceof DOMException && error.name === "AbortError")) { + return; + } console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); setConfig(null); }); + + return () => { + active = false; + controller.abort(); + }; }, []); return config; From 04549ab5007ee33c9ecc28716867380f9949635e Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 11:17:05 +0200 Subject: [PATCH 4/6] chore(pusher-web): rename properties --- .../pusher-web/src/Pusher.editorConfig.ts | 2 +- packages/pluggableWidgets/pusher-web/src/Pusher.tsx | 10 +++++----- packages/pluggableWidgets/pusher-web/src/Pusher.xml | 8 ++++---- .../pusher-web/typings/PusherProps.d.ts | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts index 1a7e31068b..ff85ea8768 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts @@ -27,5 +27,5 @@ export function getCustomCaption(values: PusherPreviewProps): string { } export function getCaption(values: PusherPreviewProps): string { - return `Pusher widget [${values.notifyChannelName}]`; + return `Pusher widget [${values.notifyActionName}]`; } diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index 3c617a0c44..ea7bcd9e99 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -7,7 +7,7 @@ import "./ui/Pusher.scss"; import { useMxObjectInfo } from "./utils/useMxObjectInfo"; export default function Pusher(props: PusherContainerProps): ReactElement { - const { class: className, objectSource, notifyChannelName, notifyAction } = props; + const { class: className, objectSource, notifyActionName, notifyEventAction } = props; // Extract object GUID and entity name from data source const mxObjectInfo = useMxObjectInfo(objectSource as any); // TODO: fix typings when PWT updated. @@ -18,9 +18,9 @@ export default function Pusher(props: PusherContainerProps): ReactElement { console.debug("[Pusher] Event received:", data); // Execute configured action - executeAction(notifyAction); + executeAction(notifyEventAction); }, - [notifyAction] + [notifyEventAction] ); // Error callback @@ -37,11 +37,11 @@ export default function Pusher(props: PusherContainerProps): ReactElement { return { entityName: mxObjectInfo.entityName, guid: mxObjectInfo.guid, - eventName: notifyChannelName, + eventName: notifyActionName, onEvent: handleEvent, onError: handleError }; - }, [mxObjectInfo, handleEvent, handleError, notifyChannelName]); + }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); // Initialize Pusher listener usePusherListener(subscription); diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.xml b/packages/pluggableWidgets/pusher-web/src/Pusher.xml index ba46794ffd..f17f0a8f7a 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.xml +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.xml @@ -10,12 +10,12 @@ - - Notify event name - The name should match the with the 'Notify' parameter `EventName` + + Notify action name + The name should match the with the 'Notify' parameter `ActionName` - + Action diff --git a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts index 1db883cffb..66aa96b78a 100644 --- a/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts +++ b/packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts @@ -12,8 +12,8 @@ export interface PusherContainerProps { style?: CSSProperties; tabIndex?: number; objectSource: ListValue; - notifyChannelName: string; - notifyAction?: ActionValue; + notifyActionName: string; + notifyEventAction?: ActionValue; } export interface PusherPreviewProps { @@ -28,6 +28,6 @@ export interface PusherPreviewProps { renderMode: "design" | "xray" | "structure"; translate: (text: string) => string; objectSource: {} | { caption: string } | { type: string } | null; - notifyChannelName: string; - notifyAction: {} | null; + notifyActionName: string; + notifyEventAction: {} | null; } From c8218349d726d21635b48bca734b499a572d9a54 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 12:07:49 +0200 Subject: [PATCH 5/6] chore(pusher-web): reorganize hooks --- .../pusher-web/src/Pusher.tsx | 4 +- .../src/hooks/useFetchPusherConfig.ts | 29 ++++++++ .../pusher-web/src/hooks/usePusherConfig.ts | 69 ------------------- .../pusher-web/src/hooks/usePusherListener.ts | 60 ++++++---------- .../src/hooks/usePusherSubscribe.ts | 21 ++++++ .../pusher-web/src/utils/PusherListener.ts | 2 +- .../pusher-web/src/utils/fetchPusherConfig.ts | 53 ++++++++++++++ 7 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts delete mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts create mode 100644 packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts diff --git a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx index ea7bcd9e99..8269158b43 100644 --- a/packages/pluggableWidgets/pusher-web/src/Pusher.tsx +++ b/packages/pluggableWidgets/pusher-web/src/Pusher.tsx @@ -2,7 +2,7 @@ import classnames from "classnames"; import { ReactElement, useCallback, useMemo } from "react"; import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action"; import { PusherContainerProps } from "../typings/PusherProps"; -import { usePusherListener } from "./hooks/usePusherListener"; +import { usePusherSubscribe } from "./hooks/usePusherSubscribe"; import "./ui/Pusher.scss"; import { useMxObjectInfo } from "./utils/useMxObjectInfo"; @@ -44,7 +44,7 @@ export default function Pusher(props: PusherContainerProps): ReactElement { }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); // Initialize Pusher listener - usePusherListener(subscription); + usePusherSubscribe(subscription); return
; } diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts new file mode 100644 index 0000000000..660220c56c --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/useFetchPusherConfig.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { fetchPusherConfig } from "../utils/fetchPusherConfig"; +import { PusherConfig } from "../utils/PusherListener"; + +/** + * Provides Pusher configuration fetched from the backend. + * Returns null while loading or on error. + */ +export function useFetchPusherConfig(): PusherConfig | null { + const [config, setConfig] = useState(null); + + useEffect(() => { + let active = true; + const controller = new AbortController(); + + fetchPusherConfig(controller.signal).then(result => { + if (active) { + setConfig(result); + } + }); + + return () => { + active = false; + controller.abort(); + }; + }, []); + + return config; +} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts deleted file mode 100644 index 6637657031..0000000000 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherConfig.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useEffect, useState } from "react"; -import { PusherConfig } from "../utils/PusherListener"; - -interface KeyData { - key: string; - cluster: string; -} - -/** - * Fetch Pusher configuration from backend - * Returns null while loading or on error - */ -export function usePusherConfig(): PusherConfig | null { - const [config, setConfig] = useState(null); - - useEffect(() => { - let active = true; - const controller = new AbortController(); - const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; - const endpoint = `${baseUrl}rest/pusher/key`; - const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; - - const authEndpoint = `${baseUrl}rest/pusher/auth`; - - fetch(endpoint, { - method: "GET", - credentials: "same-origin", - headers: { - "X-Csrf-Token": csrfToken - }, - signal: controller.signal - }) - .then(response => { - if (response.status !== 200) { - throw new Error(`Failed to fetch Pusher key: HTTP ${response.status}`); - } - return response.text(); - }) - .then(data => { - if (!active) { - return; - } - const keyData = JSON.parse(data) as KeyData; - if (!keyData.key || !keyData.cluster) { - throw new Error("Invalid Pusher key data: missing key or cluster"); - } - setConfig({ - key: keyData.key, - cluster: keyData.cluster, - authEndpoint, - csrfToken - }); - }) - .catch(error => { - if (!active || (error instanceof DOMException && error.name === "AbortError")) { - return; - } - console.error("[usePusherConfig] Failed to fetch Pusher configuration:", error); - setConfig(null); - }); - - return () => { - active = false; - controller.abort(); - }; - }, []); - - return config; -} diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts index 4a269c017e..d91d7c4ecf 100644 --- a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -1,51 +1,35 @@ -import { useEffect, useRef } from "react"; -import { usePusherConfig } from "./usePusherConfig"; -import { PusherListener, SubscriptionConfig } from "../utils/PusherListener"; +import { useEffect, useRef, useState } from "react"; +import { useFetchPusherConfig } from "./useFetchPusherConfig"; +import { PusherListener } from "../utils/PusherListener"; /** - * React hook to manage Pusher listener lifecycle - * Automatically handles initialization, subscription changes, and cleanup + * Creates and initializes a PusherListener */ -export function usePusherListener(subscription?: SubscriptionConfig): void { - const listenerRef = useRef(null); +export function usePusherListener(): PusherListener | null { + const instanceRef = useRef(null); + const [ready, setReady] = useState(false); - // Fetch Pusher config from backend - const pusherConfig = usePusherConfig(); + const pusherConfig = useFetchPusherConfig(); - const enabled = !!pusherConfig && !!subscription; - - // Initialize PusherListener once when config is available useEffect(() => { - if (!enabled) { + if (!pusherConfig) { return; } - - const listener = new PusherListener(pusherConfig); - listenerRef.current = listener; - - listener.initialize().catch(error => { - console.error("[usePusherListener] Failed to initialize:", error); - }); - - // Cleanup on unmount or when config changes - return () => { - listener.destroy(); - listenerRef.current = null; - }; - }, [pusherConfig, enabled]); - - // Subscribe/unsubscribe based on subscription config changes - useEffect(() => { - const listener = listenerRef.current; - if (!enabled || !listener) { + try { + const instance = new PusherListener(pusherConfig); + instance.initialize(); + instanceRef.current = instance; + setReady(true); + } catch (error) { + console.error("[usePusherListenerInstance] Failed to initialize:", error); return; } - - listener.subscribe(subscription); - - // Unsubscribe on cleanup or when subscription changes return () => { - listener.unsubscribe(); + instanceRef.current?.destroy(); + instanceRef.current = null; + setReady(false); }; - }, [enabled, subscription]); + }, [pusherConfig]); + + return ready ? instanceRef.current : null; } diff --git a/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts new file mode 100644 index 0000000000..52841e4f25 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherSubscribe.ts @@ -0,0 +1,21 @@ +import { useEffect } from "react"; +import { usePusherListener } from "./usePusherListener"; +import { SubscriptionConfig } from "../utils/PusherListener"; + +/** + * Manages the full Pusher listener lifecycle: config fetching, initialization, + * and subscription. Resubscribes automatically when subscription changes. + */ +export function usePusherSubscribe(subscription?: SubscriptionConfig): void { + const listener = usePusherListener(); + + useEffect(() => { + if (!listener || !subscription) { + return; + } + listener.subscribe(subscription); + return () => { + listener.unsubscribe(); + }; + }, [listener, subscription]); +} diff --git a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts index 0da56ed040..0f7d3cda10 100644 --- a/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts +++ b/packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts @@ -27,7 +27,7 @@ export class PusherListener { * Initialize Pusher connection * Should be called once on widget mount */ - async initialize(): Promise { + initialize(): void { if (this.pusher) { return; // Already initialized } diff --git a/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts new file mode 100644 index 0000000000..ed97fbe650 --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/utils/fetchPusherConfig.ts @@ -0,0 +1,53 @@ +import { PusherConfig } from "./PusherListener"; + +interface KeyData { + key: string; + cluster: string; +} + +/** + * Fetches Pusher configuration from the backend. + * Returns a PusherConfig on success, or null on error / invalid response. + */ +export async function fetchPusherConfig(signal: AbortSignal): Promise { + const baseUrl = (window as any).mx?.remoteUrl || (window as any).mx?.appUrl || ""; + const endpoint = `${baseUrl}rest/pusher/key`; + const csrfToken = (window as any).mx?.sessionData?.csrftoken || ""; + const authEndpoint = `${baseUrl}rest/pusher/auth`; + + let response: Response; + try { + response = await fetch(endpoint, { + method: "GET", + credentials: "same-origin", + headers: { "X-Csrf-Token": csrfToken }, + signal + }); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + return null; + } + console.error("[fetchPusherConfig] Network error:", error); + return null; + } + + if (response.status !== 200) { + console.error(`[fetchPusherConfig] Unexpected response: HTTP ${response.status}`); + return null; + } + + let keyData: KeyData; + try { + keyData = JSON.parse(await response.text()) as KeyData; + } catch (error) { + console.error("[fetchPusherConfig] Failed to parse response:", error); + return null; + } + + if (!keyData.key || !keyData.cluster) { + console.error("[fetchPusherConfig] Invalid response: missing key or cluster"); + return null; + } + + return { key: keyData.key, cluster: keyData.cluster, authEndpoint, csrfToken }; +} From abe155f2089db2cd87d078649d4369ea5e302ee5 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Fri, 22 May 2026 15:23:02 +0200 Subject: [PATCH 6/6] chore(pusher-web): mark as private --- packages/pluggableWidgets/pusher-web/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pluggableWidgets/pusher-web/package.json b/packages/pluggableWidgets/pusher-web/package.json index 21b64c822e..a28408cd8c 100644 --- a/packages/pluggableWidgets/pusher-web/package.json +++ b/packages/pluggableWidgets/pusher-web/package.json @@ -5,6 +5,7 @@ "description": "Pusher.com integration widget for real-time communication", "copyright": "© Mendix Technology BV 2026. All rights reserved.", "license": "Apache-2.0", + "private": true, "repository": { "type": "git", "url": "https://github.com/mendix/web-widgets.git"