Skip to content

Commit 250f50e

Browse files
committed
feat: make pusher widget accept list of actions
1 parent c0dca5c commit 250f50e

5 files changed

Lines changed: 119 additions & 59 deletions

File tree

packages/pluggableWidgets/pusher-web/src/Pusher.editorConfig.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Properties } from "@mendix/pluggable-widgets-tools";
1+
import { Problem, Properties } from "@mendix/pluggable-widgets-tools";
22
import {
33
container,
44
rowLayout,
@@ -12,6 +12,26 @@ export function getProperties(_values: PusherPreviewProps, defaultProperties: Pr
1212
return defaultProperties;
1313
}
1414

15+
export function check(values: PusherPreviewProps): Problem[] {
16+
const errors: Problem[] = [];
17+
18+
// Check for duplicate action names
19+
if (values.eventHandlers && values.eventHandlers.length > 0) {
20+
const actionNames = values.eventHandlers.map(handler => handler.actionName).filter(name => name);
21+
const duplicates = actionNames.filter((name, index) => actionNames.indexOf(name) !== index);
22+
23+
if (duplicates.length > 0) {
24+
const uniqueDuplicates = Array.from(new Set(duplicates));
25+
errors.push({
26+
property: "eventHandlers",
27+
message: `Duplicate action names found: ${uniqueDuplicates.join(", ")}. Each action name must be unique.`
28+
});
29+
}
30+
}
31+
32+
return errors;
33+
}
34+
1535
export function getPreview(values: PusherPreviewProps, isDarkMode: boolean): StructurePreviewProps {
1636
const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"];
1737

@@ -27,5 +47,6 @@ export function getCustomCaption(values: PusherPreviewProps): string {
2747
}
2848

2949
export function getCaption(values: PusherPreviewProps): string {
30-
return `Pusher widget [${values.notifyActionName}]`;
50+
const handlerCount = values.eventHandlers?.length ?? 0;
51+
return handlerCount > 0 ? `Pusher widget [${handlerCount} handler${handlerCount > 1 ? "s" : ""}]` : "Pusher widget";
3152
}

packages/pluggableWidgets/pusher-web/src/Pusher.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,7 @@ import "./ui/Pusher.scss";
77
import { getChannelName } from "./utils/getChannelName";
88

99
export default function Pusher(props: PusherContainerProps): ReactElement {
10-
const { class: className, objectSource, notifyActionName, notifyEventAction } = props;
11-
12-
// Event callback - triggered when Pusher event is received
13-
const handleEvent = useCallback(
14-
(data: unknown) => {
15-
console.debug("[Pusher] Event received:", data);
16-
17-
// Execute configured action
18-
executeAction(notifyEventAction);
19-
},
20-
[notifyEventAction]
21-
);
10+
const { class: className, objectSource, eventHandlers } = props;
2211

2312
// Error callback
2413
const handleError = useCallback((error: Error) => {
@@ -34,13 +23,28 @@ export default function Pusher(props: PusherContainerProps): ReactElement {
3423
return undefined;
3524
}
3625

26+
// Build event bindings from configured handlers
27+
const eventBindings = eventHandlers
28+
.filter(handler => handler.actionName && handler.action?.canExecute)
29+
.map(handler => ({
30+
eventName: handler.actionName,
31+
onEvent: () => {
32+
console.debug(`[Pusher] Event received: ${handler.actionName}`);
33+
executeAction(handler.action);
34+
}
35+
}));
36+
37+
// If no valid handlers, return undefined (no subscription)
38+
if (eventBindings.length === 0) {
39+
return undefined;
40+
}
41+
3742
return {
3843
channelName,
39-
eventName: notifyActionName,
40-
onEvent: handleEvent,
44+
eventBindings,
4145
onError: handleError
4246
};
43-
}, [channelName, notifyActionName, handleEvent, handleError]);
47+
}, [channelName, eventHandlers, handleError]);
4448

4549
usePusherSubscribe(subscription);
4650

packages/pluggableWidgets/pusher-web/src/Pusher.xml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@
1010
<description />
1111
</property>
1212

13-
<property key="notifyActionName" type="string" required="true" defaultValue="change">
14-
<caption>Action name</caption>
15-
<description>The name should match the with the 'Notify' action parameter `ActionName`</description>
16-
</property>
17-
18-
<property key="notifyEventAction" type="action" required="false">
19-
<caption>Action</caption>
20-
<description />
13+
<property key="eventHandlers" type="object" isList="true" required="false">
14+
<caption>Event Handlers</caption>
15+
<description>Configure multiple event handlers for different Pusher events</description>
16+
<properties>
17+
<propertyGroup caption="General">
18+
<property key="actionName" type="string" required="true">
19+
<caption>Action name</caption>
20+
<description>The name should match the 'Notify' action parameter `ActionName`</description>
21+
</property>
22+
<property key="action" type="action" required="false">
23+
<caption>Action</caption>
24+
<description>Client action to execute when this event is received</description>
25+
</property>
26+
</propertyGroup>
27+
</properties>
2128
</property>
2229
</propertyGroup>
2330
</properties>

packages/pluggableWidgets/pusher-web/src/utils/PusherListener.ts

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@ export interface PusherConfig {
77
csrfToken: string;
88
}
99

10+
export interface EventBinding {
11+
eventName: string;
12+
onEvent: () => void;
13+
}
14+
1015
export interface SubscriptionConfig {
1116
channelName: string;
12-
eventName: string;
13-
onEvent: (data: unknown) => void;
17+
eventBindings: EventBinding[];
1418
onError?: (error: Error) => void;
1519
}
1620

1721
export class PusherListener {
1822
private pusher: Pusher;
1923
private currentChannel: Channel | null = null;
20-
private currentSubscription: SubscriptionConfig | null = null;
24+
private currentChannelName: string | null = null;
25+
private eventHandlersMap: Map<string, () => void> = new Map();
26+
private globalCallback: ((eventName: string, data: unknown) => void) | null = null;
27+
private onError?: (error: Error) => void;
2128

2229
constructor(private config: PusherConfig) {
2330
this.pusher = new Pusher(this.config.key, {
@@ -36,46 +43,59 @@ export class PusherListener {
3643
}
3744

3845
/**
39-
* Subscribe to channel for specific object and event
40-
* Automatically unsubscribes from previous channel if different
46+
* Subscribe to channel for specific object and bind multiple events
47+
* Only resubscribes when channel name changes, not when handlers change
4148
*/
4249
subscribe(config: SubscriptionConfig): void {
43-
// If already subscribed to same channel and event, do nothing
44-
if (config === this.currentSubscription) {
45-
return;
46-
}
50+
// Only resubscribe if channel name changes
51+
if (config.channelName !== this.currentChannelName) {
52+
this.unsubscribe();
53+
this.currentChannelName = config.channelName;
54+
this.currentChannel = this.pusher.subscribe(config.channelName);
4755

48-
// Unsubscribe from previous channel if exists
49-
this.unsubscribe();
50-
51-
// Subscribe to new channel
52-
this.currentSubscription = config;
53-
this.currentChannel = this.pusher.subscribe(config.channelName);
56+
// Setup global handler once per channel
57+
this.globalCallback = (eventName: string, _data: unknown) => {
58+
const handler = this.eventHandlersMap.get(eventName);
59+
if (handler) {
60+
handler();
61+
}
62+
};
63+
this.currentChannel.bind_global(this.globalCallback);
5464

55-
// Bind event handler
56-
this.currentChannel.bind(config.eventName, config.onEvent);
65+
// Bind error handler
66+
this.currentChannel.bind("pusher:subscription_error", (error: unknown) => {
67+
console.error(error);
68+
const errorMsg =
69+
error === 515
70+
? "Authentication failed. Please verify Pusher configuration constants."
71+
: `Subscription error: ${String(error)}`;
72+
this.onError?.(new Error(errorMsg));
73+
});
74+
}
5775

58-
// Bind error handler
59-
this.currentChannel.bind("pusher:subscription_error", (error: unknown) => {
60-
console.error(error);
61-
const errorMsg =
62-
error === 515
63-
? "Authentication failed. Please verify Pusher configuration constants."
64-
: `Subscription error: ${String(error)}`;
65-
config.onError?.(new Error(errorMsg));
76+
// Always update handler map (no rebinding needed)
77+
this.eventHandlersMap.clear();
78+
config.eventBindings.forEach(binding => {
79+
this.eventHandlersMap.set(binding.eventName, binding.onEvent);
6680
});
81+
82+
// Store error handler for reference
83+
this.onError = config.onError;
6784
}
6885

6986
/**
7087
* Unsubscribe from current channel
7188
*/
7289
unsubscribe(): void {
73-
if (this.currentChannel && this.currentSubscription) {
74-
// Unbind all channel events
75-
this.currentChannel.unbind();
76-
this.pusher.unsubscribe(this.currentSubscription.channelName);
90+
if (this.currentChannel && this.currentChannelName) {
91+
// Unbind all channel events (both global and specific)
92+
this.currentChannel.unbind_all();
93+
this.pusher.unsubscribe(this.currentChannelName);
7794
this.currentChannel = null;
78-
this.currentSubscription = null;
95+
this.currentChannelName = null;
96+
this.globalCallback = null;
97+
this.eventHandlersMap.clear();
98+
this.onError = undefined;
7999
}
80100
}
81101

packages/pluggableWidgets/pusher-web/typings/PusherProps.d.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@
66
import { ActionValue, DynamicValue, ObjectItem } from "mendix";
77
import { CSSProperties } from "react";
88

9+
export interface EventHandlersType {
10+
actionName: string;
11+
action?: ActionValue;
12+
}
13+
14+
export interface EventHandlersPreviewType {
15+
actionName: string;
16+
action: {} | null;
17+
}
18+
919
export interface PusherContainerProps {
1020
name: string;
1121
class: string;
1222
style?: CSSProperties;
1323
tabIndex?: number;
1424
objectSource: DynamicValue<ObjectItem>;
15-
notifyActionName: string;
16-
notifyEventAction?: ActionValue;
25+
eventHandlers: EventHandlersType[];
1726
}
1827

1928
export interface PusherPreviewProps {
@@ -28,6 +37,5 @@ export interface PusherPreviewProps {
2837
renderMode: "design" | "xray" | "structure";
2938
translate: (text: string) => string;
3039
objectSource: {} | { caption: string } | { type: string } | null;
31-
notifyActionName: string;
32-
notifyEventAction: {} | null;
40+
eventHandlers: EventHandlersPreviewType[];
3341
}

0 commit comments

Comments
 (0)