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..a28408cd8c --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/package.json @@ -0,0 +1,63 @@ +{ + "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", + "private": true, + "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..ff85ea8768 --- /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.notifyActionName}]`; +} 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..8269158b43 --- /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 { usePusherSubscribe } from "./hooks/usePusherSubscribe"; +import "./ui/Pusher.scss"; +import { useMxObjectInfo } from "./utils/useMxObjectInfo"; + +export default function Pusher(props: PusherContainerProps): ReactElement { + 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. + + // Event callback - triggered when Pusher event is received + const handleEvent = useCallback( + (data: unknown) => { + console.debug("[Pusher] Event received:", data); + + // Execute configured action + executeAction(notifyEventAction); + }, + [notifyEventAction] + ); + + // 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: notifyActionName, + onEvent: handleEvent, + onError: handleError + }; + }, [mxObjectInfo, handleEvent, handleError, notifyActionName]); + + // Initialize Pusher listener + usePusherSubscribe(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..f17f0a8f7a --- /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 action name + The name should match the with the 'Notify' parameter `ActionName` + + + + 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/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/usePusherListener.ts b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts new file mode 100644 index 0000000000..d91d7c4ecf --- /dev/null +++ b/packages/pluggableWidgets/pusher-web/src/hooks/usePusherListener.ts @@ -0,0 +1,35 @@ +import { useEffect, useRef, useState } from "react"; +import { useFetchPusherConfig } from "./useFetchPusherConfig"; +import { PusherListener } from "../utils/PusherListener"; + +/** + * Creates and initializes a PusherListener + */ +export function usePusherListener(): PusherListener | null { + const instanceRef = useRef(null); + const [ready, setReady] = useState(false); + + const pusherConfig = useFetchPusherConfig(); + + useEffect(() => { + if (!pusherConfig) { + return; + } + try { + const instance = new PusherListener(pusherConfig); + instance.initialize(); + instanceRef.current = instance; + setReady(true); + } catch (error) { + console.error("[usePusherListenerInstance] Failed to initialize:", error); + return; + } + return () => { + instanceRef.current?.destroy(); + instanceRef.current = null; + setReady(false); + }; + }, [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/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..0f7d3cda10 --- /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 + */ + initialize(): void { + 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/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 }; +} 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..66aa96b78a --- /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; + notifyActionName: string; + notifyEventAction?: 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; + notifyActionName: string; + notifyEventAction: {} | 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