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
5 changes: 5 additions & 0 deletions packages/pluggableWidgets/pusher-web/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const base = require("@mendix/prettier-config-web-widgets");

module.exports = {
...base
};
11 changes: 11 additions & 0 deletions packages/pluggableWidgets/pusher-web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions packages/pluggableWidgets/pusher-web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Pusher Widget
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/pusher-web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";

export default config;
62 changes: 62 additions & 0 deletions packages/pluggableWidgets/pusher-web/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
31 changes: 31 additions & 0 deletions packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts
Original file line number Diff line number Diff line change
@@ -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}]`;
}
Original file line number Diff line number Diff line change
@@ -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 <div className={classNames("widget-pusher-preview")}>{getCaption(props)}</div>;
}
50 changes: 50 additions & 0 deletions packages/pluggableWidgets/pusher-web/src/Pusher.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={classnames("widget-pusher", className)} />;
}
24 changes: 24 additions & 0 deletions packages/pluggableWidgets/pusher-web/src/Pusher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<widget id="com.mendix.widget.web.pusher.Pusher" pluginWidget="true" offlineCapable="false" supportedPlatform="Web" xmlns="http://www.mendix.com/widget/1.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../node_modules/mendix/custom_widget.xsd">
<name>Pusher</name>
<description>Listen to Notify server action and perform client side action</description>
<helpUrl>https://docs.mendix.com/appstore/widgets/pusher</helpUrl>
<properties>
<propertyGroup caption="General">
<property key="objectSource" type="datasource" isList="false">
<caption>Object to listen</caption>
<description />
</property>

<property key="notifyActionName" type="string" required="true" defaultValue="change">
<caption>Notify action name</caption>
<description>The name should match the with the 'Notify' parameter `ActionName`</description>
</property>

<property key="notifyEventAction" type="action" required="false">
<caption>Action</caption>
<description />
</property>
</propertyGroup>
</properties>
</widget>
Original file line number Diff line number Diff line change
@@ -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
});
Original file line number Diff line number Diff line change
@@ -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<PusherConfig | null>(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;
}
Original file line number Diff line number Diff line change
@@ -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<PusherListener>(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;
}
Original file line number Diff line number Diff line change
@@ -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]);
}
11 changes: 11 additions & 0 deletions packages/pluggableWidgets/pusher-web/src/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="PusherWidget" version="2.0.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="Pusher.xml" />
</widgetFiles>
<files>
<file path="com/mendix/widget/web/pusher/" />
</files>
</clientModule>
</package>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.widget-pusher-preview {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
}
Loading
Loading