@nuraly/lumenui 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nuralyui.bundle.js +2577 -1486
- package/dist/nuralyui.bundle.js.gz +0 -0
- package/dist/src/components/button/bundle.js +130 -39
- package/dist/src/components/button/bundle.js.gz +0 -0
- package/dist/src/components/button/button.component.js +7 -4
- package/dist/src/components/button/button.style.js +92 -2
- package/dist/src/components/canvas/base-canvas.component.d.ts +8 -0
- package/dist/src/components/canvas/base-canvas.component.js +75 -3
- package/dist/src/components/canvas/bundle.js +2540 -1201
- package/dist/src/components/canvas/bundle.js.gz +0 -0
- package/dist/src/components/canvas/controllers/collaboration.controller.d.ts +24 -11
- package/dist/src/components/canvas/controllers/collaboration.controller.js +176 -120
- package/dist/src/components/canvas/controllers/selection.controller.js +20 -0
- package/dist/src/components/canvas/interfaces/collaboration.interface.d.ts +8 -0
- package/dist/src/components/canvas/templates/index.d.ts +1 -0
- package/dist/src/components/canvas/templates/index.js +2 -0
- package/dist/src/components/canvas/templates/lock-overlay.template.d.ts +20 -0
- package/dist/src/components/canvas/templates/lock-overlay.template.js +33 -0
- package/dist/src/components/canvas/workflow-canvas.component.js +52 -24
- package/dist/src/components/canvas/workflow-canvas.style.js +62 -1
- package/dist/src/components/canvas/workflow-canvas.types.js +50 -4
- package/dist/src/components/chat-panel/bundle.js +10 -10
- package/dist/src/components/chat-panel/bundle.js.gz +0 -0
- package/dist/src/components/chat-panel/chat-panel.component.js +8 -8
- package/dist/src/components/chatbot/bundle.js +400 -242
- package/dist/src/components/chatbot/bundle.js.gz +0 -0
- package/dist/src/components/chatbot/chatbot.style.js +156 -22
- package/dist/src/components/chatbot/chatbot.types.d.ts +1 -0
- package/dist/src/components/chatbot/core/chatbot-core.controller.js +13 -7
- package/dist/src/components/chatbot/plugins/artifact-plugin.js +0 -19
- package/dist/src/components/chatbot/plugins/flight-card-plugin.js +0 -35
- package/dist/src/components/chatbot/plugins/markdown-plugin.js +0 -4
- package/dist/src/components/chatbot/plugins/print-job-card-plugin.js +0 -36
- package/dist/src/components/chatbot/plugins/selection-card-plugin.js +0 -34
- package/dist/src/components/chatbot/providers/workflow-socket-provider.js +1 -2
- package/dist/src/components/chatbot/templates/input-box.template.js +58 -30
- package/dist/src/components/chatbot/templates/message.template.js +41 -31
- package/dist/src/components/chatbot/templates/thread-sidebar.template.js +38 -39
- package/dist/src/components/colorpicker/bundle.js +15 -10
- package/dist/src/components/colorpicker/bundle.js.gz +0 -0
- package/dist/src/components/colorpicker/color-picker.component.js +15 -10
- package/dist/src/components/datepicker/bundle.js +10 -10
- package/dist/src/components/datepicker/bundle.js.gz +0 -0
- package/dist/src/components/datepicker/datepicker.component.js +14 -22
- package/dist/src/components/dropdown/bundle.js +15 -14
- package/dist/src/components/dropdown/bundle.js.gz +0 -0
- package/dist/src/components/dropdown/dropdown.component.js +10 -9
- package/dist/src/components/dropdown/dropdown.style.js +2 -2
- package/dist/src/components/file-upload/bundle.js +15 -14
- package/dist/src/components/file-upload/bundle.js.gz +0 -0
- package/dist/src/components/file-upload/file-upload.component.js +15 -14
- package/dist/src/components/icon/bundle.js +7 -7
- package/dist/src/components/icon/bundle.js.gz +0 -0
- package/dist/src/components/icon/icon-paths.js +15 -0
- package/dist/src/components/iconpicker/bundle.js +216 -124
- package/dist/src/components/iconpicker/bundle.js.gz +0 -0
- package/dist/src/components/iconpicker/icon-picker.component.js +4 -4
- package/dist/src/components/menu/bundle.js +5 -2
- package/dist/src/components/menu/bundle.js.gz +0 -0
- package/dist/src/components/menu/menu.component.js +5 -2
- package/dist/src/components/modal/bundle.js +16 -13
- package/dist/src/components/modal/bundle.js.gz +0 -0
- package/dist/src/components/modal/modal.component.js +16 -13
- package/dist/src/components/panel/bundle.js +28 -28
- package/dist/src/components/panel/bundle.js.gz +0 -0
- package/dist/src/components/popconfirm/bundle.js +135 -41
- package/dist/src/components/popconfirm/bundle.js.gz +0 -0
- package/dist/src/components/popconfirm/popconfirm.component.d.ts +15 -119
- package/dist/src/components/popconfirm/popconfirm.component.js +158 -162
- package/dist/src/components/popconfirm/popconfirm.style.js +94 -0
- package/dist/src/components/presence/bundle.js +2 -1
- package/dist/src/components/presence/bundle.js.gz +0 -0
- package/dist/src/components/presence/presence.component.js +2 -1
- package/dist/src/components/table/bundle.js +3 -2
- package/dist/src/components/table/bundle.js.gz +0 -0
- package/dist/src/components/table/table.component.js +3 -2
- package/dist/src/components/tabs/bundle.js +3 -3
- package/dist/src/components/tabs/bundle.js.gz +0 -0
- package/dist/src/components/timepicker/bundle.js +3 -3
- package/dist/src/components/timepicker/bundle.js.gz +0 -0
- package/package.json +1 -1
- package/packages/common/dist/VERSIONS.md +1 -1
- package/packages/common/dist/shared/controllers/dropdown.controller.d.ts +4 -0
- package/packages/common/dist/shared/controllers/dropdown.controller.d.ts.map +1 -1
- package/packages/common/dist/shared/controllers/dropdown.controller.js +29 -3
- package/packages/common/dist/shared/controllers/dropdown.controller.js.map +1 -1
|
Binary file
|
|
@@ -4,36 +4,49 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
import { BaseCanvasController } from './base.controller.js';
|
|
7
|
-
import type { CanvasOperationType, CollaborationState, CollaborationUser, RemoteCursor } from '../interfaces/collaboration.interface.js';
|
|
7
|
+
import type { CanvasOperationType, CollaborationState, CollaborationUser, NodeLock, RemoteCursor } from '../interfaces/collaboration.interface.js';
|
|
8
8
|
/**
|
|
9
9
|
* CollaborationController manages real-time collaboration via Socket.IO.
|
|
10
|
-
*
|
|
10
|
+
* Uses the LumenJS page socket (path `/__nk_socketio/`, namespace
|
|
11
|
+
* `/nk<pathname>`). Wire format: all server→client traffic arrives on the
|
|
12
|
+
* single `nk:data` event carrying `{ event, data }`; client→server uses
|
|
13
|
+
* `nk:<event>` which the server binds via `on('<event>', …)`.
|
|
11
14
|
*/
|
|
12
15
|
export declare class CollaborationController extends BaseCanvasController {
|
|
13
16
|
private socket;
|
|
14
17
|
private staleCursorInterval;
|
|
18
|
+
private keepaliveInterval;
|
|
15
19
|
private lastCursorBroadcast;
|
|
16
20
|
private cursorThrottleTimer;
|
|
21
|
+
private myUserId;
|
|
17
22
|
private state;
|
|
18
23
|
private opCounter;
|
|
19
|
-
|
|
24
|
+
private myHeldLocks;
|
|
25
|
+
/**
|
|
26
|
+
* Connect via the LumenJS page socket.
|
|
27
|
+
* @param canvasId workflow/whiteboard id (for local state tracking only)
|
|
28
|
+
* @param namespace LumenJS namespace, e.g. `/nk/apps/workflows/<id>`
|
|
29
|
+
* @param userId current user id (used to identify ownership of locks)
|
|
30
|
+
*/
|
|
31
|
+
connect(canvasId: string, namespace: string, userId: string): void;
|
|
20
32
|
disconnect(): void;
|
|
21
33
|
hostDisconnected(): void;
|
|
22
|
-
private
|
|
34
|
+
private handleWireMessage;
|
|
23
35
|
broadcastCursorMove(x: number, y: number): void;
|
|
24
36
|
private emitCursorMove;
|
|
25
37
|
broadcastSelectionChange(elementIds: string[]): void;
|
|
26
38
|
broadcastTypingStart(elementId: string): void;
|
|
27
39
|
broadcastTypingStop(elementId: string): void;
|
|
28
40
|
broadcastOperation(type: CanvasOperationType, elementId: string, data: Record<string, unknown>): void;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
/** Acquire an edit lock for a node. Server broadcasts lock:state on grant. */
|
|
42
|
+
acquireLock(nodeId: string): void;
|
|
43
|
+
/** Release a lock this user holds. */
|
|
44
|
+
releaseLock(nodeId: string): void;
|
|
45
|
+
/** Whether a remote user is currently editing this node. */
|
|
46
|
+
getRemoteLock(nodeId: string): NodeLock | null;
|
|
47
|
+
private ensureKeepalive;
|
|
48
|
+
private handleLockState;
|
|
35
49
|
private handleOperationReceived;
|
|
36
|
-
private handleOperationAck;
|
|
37
50
|
private applyRemoteOperation;
|
|
38
51
|
getState(): CollaborationState;
|
|
39
52
|
isConnected(): boolean;
|
|
@@ -8,17 +8,23 @@ import { BaseCanvasController } from './base.controller.js';
|
|
|
8
8
|
const CURSOR_THROTTLE_MS = 33;
|
|
9
9
|
const STALE_CURSOR_MS = 10000;
|
|
10
10
|
const STALE_CURSOR_CHECK_MS = 5000;
|
|
11
|
+
const LOCK_KEEPALIVE_MS = 10000;
|
|
11
12
|
/**
|
|
12
13
|
* CollaborationController manages real-time collaboration via Socket.IO.
|
|
13
|
-
*
|
|
14
|
+
* Uses the LumenJS page socket (path `/__nk_socketio/`, namespace
|
|
15
|
+
* `/nk<pathname>`). Wire format: all server→client traffic arrives on the
|
|
16
|
+
* single `nk:data` event carrying `{ event, data }`; client→server uses
|
|
17
|
+
* `nk:<event>` which the server binds via `on('<event>', …)`.
|
|
14
18
|
*/
|
|
15
19
|
export class CollaborationController extends BaseCanvasController {
|
|
16
20
|
constructor() {
|
|
17
21
|
super(...arguments);
|
|
18
22
|
this.socket = null;
|
|
19
23
|
this.staleCursorInterval = null;
|
|
24
|
+
this.keepaliveInterval = null;
|
|
20
25
|
this.lastCursorBroadcast = 0;
|
|
21
26
|
this.cursorThrottleTimer = null;
|
|
27
|
+
this.myUserId = '';
|
|
22
28
|
this.state = {
|
|
23
29
|
connected: false,
|
|
24
30
|
canvasId: null,
|
|
@@ -26,42 +32,101 @@ export class CollaborationController extends BaseCanvasController {
|
|
|
26
32
|
cursors: new Map(),
|
|
27
33
|
selections: new Map(),
|
|
28
34
|
typingIndicators: new Map(),
|
|
35
|
+
lockedNodes: new Map(),
|
|
29
36
|
serverVersion: 0,
|
|
30
37
|
pendingOps: new Map(),
|
|
31
38
|
};
|
|
32
39
|
this.opCounter = 0;
|
|
40
|
+
this.myHeldLocks = new Set();
|
|
33
41
|
}
|
|
34
42
|
// ==================== Lifecycle ====================
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Connect via the LumenJS page socket.
|
|
45
|
+
* @param canvasId workflow/whiteboard id (for local state tracking only)
|
|
46
|
+
* @param namespace LumenJS namespace, e.g. `/nk/apps/workflows/<id>`
|
|
47
|
+
* @param userId current user id (used to identify ownership of locks)
|
|
48
|
+
*/
|
|
49
|
+
connect(canvasId, namespace, userId) {
|
|
50
|
+
var _a, _b, _c;
|
|
37
51
|
if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) && this.state.canvasId === canvasId)
|
|
38
52
|
return;
|
|
53
|
+
console.log('[collab] connect()', { canvasId, namespace, userId });
|
|
39
54
|
this.disconnect();
|
|
40
55
|
this.state.canvasId = canvasId;
|
|
56
|
+
this.myUserId = userId;
|
|
57
|
+
if (!namespace || !userId)
|
|
58
|
+
return;
|
|
41
59
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
this.socket = io(namespace, {
|
|
61
|
+
path: '/__nk_socketio/',
|
|
62
|
+
query: {
|
|
63
|
+
__params: JSON.stringify({
|
|
64
|
+
workflowId: canvasId,
|
|
65
|
+
resourceId: canvasId,
|
|
66
|
+
userId,
|
|
67
|
+
source: 'canvas',
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
// Force a dedicated connection — the LumenJS router already opens a
|
|
71
|
+
// socket to this same namespace; without forceNew, Socket.IO would
|
|
72
|
+
// hand us a cached instance whose `connect` event has already fired
|
|
73
|
+
// (we'd never hear it) and whose handshake query is from the router,
|
|
74
|
+
// not ours (server would treat this socket as not a canvas source).
|
|
75
|
+
forceNew: true,
|
|
45
76
|
transports: ['websocket', 'polling'],
|
|
46
77
|
reconnection: true,
|
|
47
78
|
reconnectionAttempts: Infinity,
|
|
48
79
|
reconnectionDelay: 1000,
|
|
49
80
|
});
|
|
50
|
-
|
|
81
|
+
const onConnected = () => {
|
|
82
|
+
console.log('[collab] socket connected', { canvasId });
|
|
51
83
|
this.state.connected = true;
|
|
52
|
-
this.safeExecute(() => this.socket.emit('canvas:
|
|
84
|
+
this.safeExecute(() => this.socket.emit('nk:canvas:lock:request'), 'connect: lock:request');
|
|
53
85
|
this._host.requestUpdate();
|
|
54
|
-
}
|
|
55
|
-
this.socket.on('
|
|
86
|
+
};
|
|
87
|
+
this.socket.on('connect', onConnected);
|
|
88
|
+
// If somehow the socket is already connected by the time we attach
|
|
89
|
+
// (Socket.IO reuse edge case), fire the handler manually.
|
|
90
|
+
if (this.socket.connected) {
|
|
91
|
+
console.log('[collab] socket already connected at attach time');
|
|
92
|
+
onConnected();
|
|
93
|
+
}
|
|
94
|
+
this.socket.on('disconnect', (reason) => {
|
|
95
|
+
console.log('[collab] socket disconnected', reason);
|
|
56
96
|
this.state.connected = false;
|
|
57
97
|
this._host.requestUpdate();
|
|
58
98
|
});
|
|
59
|
-
this.socket.on('
|
|
60
|
-
|
|
99
|
+
this.socket.on('connect_error', (err) => {
|
|
100
|
+
console.warn('[collab] connect_error', err === null || err === void 0 ? void 0 : err.message);
|
|
101
|
+
});
|
|
102
|
+
this.socket.on('nk:data', (payload) => {
|
|
103
|
+
console.log('[collab] nk:data', payload === null || payload === void 0 ? void 0 : payload.event, payload === null || payload === void 0 ? void 0 : payload.data);
|
|
104
|
+
this.safeExecute(() => this.handleWireMessage(payload), 'handleWireMessage');
|
|
105
|
+
});
|
|
106
|
+
// Diagnostic probes — remove once the connection works.
|
|
107
|
+
console.log('[collab] post-io()', {
|
|
108
|
+
connected: this.socket.connected,
|
|
109
|
+
id: this.socket.id,
|
|
110
|
+
active: (_c = (_b = this.socket.io) === null || _b === void 0 ? void 0 : _b.engine) === null || _c === void 0 ? void 0 : _c.readyState,
|
|
61
111
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
114
|
+
console.log('[collab] 1s probe', {
|
|
115
|
+
connected: (_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected,
|
|
116
|
+
id: (_b = this.socket) === null || _b === void 0 ? void 0 : _b.id,
|
|
117
|
+
active: (_e = (_d = (_c = this.socket) === null || _c === void 0 ? void 0 : _c.io) === null || _d === void 0 ? void 0 : _d.engine) === null || _e === void 0 ? void 0 : _e.readyState,
|
|
118
|
+
transport: (_j = (_h = (_g = (_f = this.socket) === null || _f === void 0 ? void 0 : _f.io) === null || _g === void 0 ? void 0 : _g.engine) === null || _h === void 0 ? void 0 : _h.transport) === null || _j === void 0 ? void 0 : _j.name,
|
|
119
|
+
});
|
|
120
|
+
}, 1000);
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
123
|
+
console.log('[collab] 3s probe', {
|
|
124
|
+
connected: (_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected,
|
|
125
|
+
id: (_b = this.socket) === null || _b === void 0 ? void 0 : _b.id,
|
|
126
|
+
active: (_e = (_d = (_c = this.socket) === null || _c === void 0 ? void 0 : _c.io) === null || _d === void 0 ? void 0 : _d.engine) === null || _e === void 0 ? void 0 : _e.readyState,
|
|
127
|
+
transport: (_j = (_h = (_g = (_f = this.socket) === null || _f === void 0 ? void 0 : _f.io) === null || _g === void 0 ? void 0 : _g.engine) === null || _h === void 0 ? void 0 : _h.transport) === null || _j === void 0 ? void 0 : _j.name,
|
|
128
|
+
});
|
|
129
|
+
}, 3000);
|
|
65
130
|
this.staleCursorInterval = setInterval(() => this.cleanStaleCursors(), STALE_CURSOR_CHECK_MS);
|
|
66
131
|
}
|
|
67
132
|
catch (error) {
|
|
@@ -71,9 +136,6 @@ export class CollaborationController extends BaseCanvasController {
|
|
|
71
136
|
disconnect() {
|
|
72
137
|
if (this.socket) {
|
|
73
138
|
this.safeExecute(() => {
|
|
74
|
-
if (this.state.canvasId) {
|
|
75
|
-
this.socket.emit('canvas:leave', { canvasId: this.state.canvasId });
|
|
76
|
-
}
|
|
77
139
|
this.socket.removeAllListeners();
|
|
78
140
|
this.socket.disconnect();
|
|
79
141
|
}, 'disconnect');
|
|
@@ -83,6 +145,10 @@ export class CollaborationController extends BaseCanvasController {
|
|
|
83
145
|
clearInterval(this.staleCursorInterval);
|
|
84
146
|
this.staleCursorInterval = null;
|
|
85
147
|
}
|
|
148
|
+
if (this.keepaliveInterval) {
|
|
149
|
+
clearInterval(this.keepaliveInterval);
|
|
150
|
+
this.keepaliveInterval = null;
|
|
151
|
+
}
|
|
86
152
|
if (this.cursorThrottleTimer) {
|
|
87
153
|
clearTimeout(this.cursorThrottleTimer);
|
|
88
154
|
this.cursorThrottleTimer = null;
|
|
@@ -94,41 +160,35 @@ export class CollaborationController extends BaseCanvasController {
|
|
|
94
160
|
cursors: new Map(),
|
|
95
161
|
selections: new Map(),
|
|
96
162
|
typingIndicators: new Map(),
|
|
163
|
+
lockedNodes: new Map(),
|
|
97
164
|
serverVersion: 0,
|
|
98
165
|
pendingOps: new Map(),
|
|
99
166
|
};
|
|
167
|
+
this.myHeldLocks.clear();
|
|
100
168
|
}
|
|
101
169
|
hostDisconnected() {
|
|
102
170
|
this.disconnect();
|
|
103
171
|
}
|
|
104
|
-
// ====================
|
|
105
|
-
|
|
106
|
-
if (!
|
|
172
|
+
// ==================== Inbound routing ====================
|
|
173
|
+
handleWireMessage(payload) {
|
|
174
|
+
if (!(payload === null || payload === void 0 ? void 0 : payload.event))
|
|
107
175
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.safeExecute(() => this.handleTypingIndicator(data), 'handleTypingIndicator');
|
|
125
|
-
});
|
|
126
|
-
this.socket.on('operation:received', (data) => {
|
|
127
|
-
this.safeExecute(() => this.handleOperationReceived(data), 'handleOperationReceived');
|
|
128
|
-
});
|
|
129
|
-
this.socket.on('operation:ack', (data) => {
|
|
130
|
-
this.safeExecute(() => this.handleOperationAck(data), 'handleOperationAck');
|
|
131
|
-
});
|
|
176
|
+
switch (payload.event) {
|
|
177
|
+
case 'canvas:op':
|
|
178
|
+
this.handleOperationReceived(payload.data);
|
|
179
|
+
break;
|
|
180
|
+
case 'canvas:lock:state':
|
|
181
|
+
this.handleLockState(payload.data);
|
|
182
|
+
break;
|
|
183
|
+
case 'canvas:lock:ack':
|
|
184
|
+
// Server-authoritative reply to an acquire attempt; lock:state follows,
|
|
185
|
+
// so no state change needed here — left as an extension point.
|
|
186
|
+
break;
|
|
187
|
+
default:
|
|
188
|
+
// Non-collaboration events (presence:*, execution:*) are handled by
|
|
189
|
+
// other listeners/components on the page; ignore here.
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
132
192
|
}
|
|
133
193
|
// ==================== Outbound Methods ====================
|
|
134
194
|
broadcastCursorMove(x, y) {
|
|
@@ -153,125 +213,121 @@ export class CollaborationController extends BaseCanvasController {
|
|
|
153
213
|
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || !this.state.canvasId)
|
|
154
214
|
return;
|
|
155
215
|
this.lastCursorBroadcast = Date.now();
|
|
156
|
-
this.safeExecute(() => this.socket.emit('cursor:move', {
|
|
216
|
+
this.safeExecute(() => this.socket.emit('nk:cursor:move', { x, y }), 'emitCursorMove');
|
|
157
217
|
}
|
|
158
218
|
broadcastSelectionChange(elementIds) {
|
|
159
219
|
var _a;
|
|
160
220
|
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || !this.state.canvasId)
|
|
161
221
|
return;
|
|
162
|
-
this.safeExecute(() => this.socket.emit('selection:change', {
|
|
222
|
+
this.safeExecute(() => this.socket.emit('nk:selection:change', { elementIds }), 'broadcastSelectionChange');
|
|
163
223
|
}
|
|
164
224
|
broadcastTypingStart(elementId) {
|
|
165
225
|
var _a;
|
|
166
226
|
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || !this.state.canvasId)
|
|
167
227
|
return;
|
|
168
|
-
this.safeExecute(() => this.socket.emit('typing:start', {
|
|
228
|
+
this.safeExecute(() => this.socket.emit('nk:typing:start', { elementId }), 'broadcastTypingStart');
|
|
169
229
|
}
|
|
170
230
|
broadcastTypingStop(elementId) {
|
|
171
231
|
var _a;
|
|
172
232
|
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || !this.state.canvasId)
|
|
173
233
|
return;
|
|
174
|
-
this.safeExecute(() => this.socket.emit('typing:stop', {
|
|
234
|
+
this.safeExecute(() => this.socket.emit('nk:typing:stop', { elementId }), 'broadcastTypingStop');
|
|
175
235
|
}
|
|
176
236
|
broadcastOperation(type, elementId, data) {
|
|
177
237
|
var _a;
|
|
178
238
|
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) || !this.state.canvasId)
|
|
179
239
|
return;
|
|
180
240
|
const opId = `op_${Date.now()}_${++this.opCounter}`;
|
|
181
|
-
|
|
241
|
+
this.state.pendingOps.set(opId, {
|
|
182
242
|
id: opId,
|
|
183
243
|
type,
|
|
184
244
|
elementId,
|
|
185
245
|
data,
|
|
186
|
-
userId:
|
|
246
|
+
userId: this.myUserId,
|
|
187
247
|
timestamp: Date.now(),
|
|
188
248
|
version: this.state.serverVersion,
|
|
189
|
-
};
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
operationType: type,
|
|
249
|
+
});
|
|
250
|
+
this.safeExecute(() => this.socket.emit('nk:canvas:op', {
|
|
251
|
+
id: opId,
|
|
252
|
+
type,
|
|
194
253
|
elementId,
|
|
195
254
|
data,
|
|
196
|
-
baseVersion: this.state.serverVersion,
|
|
197
255
|
}), 'broadcastOperation');
|
|
198
256
|
}
|
|
199
|
-
// ====================
|
|
200
|
-
|
|
201
|
-
|
|
257
|
+
// ==================== Lock methods ====================
|
|
258
|
+
/** Acquire an edit lock for a node. Server broadcasts lock:state on grant. */
|
|
259
|
+
acquireLock(nodeId) {
|
|
260
|
+
var _a, _b;
|
|
261
|
+
console.log('[collab] acquireLock()', { nodeId, connected: (_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected });
|
|
262
|
+
if (!((_b = this.socket) === null || _b === void 0 ? void 0 : _b.connected) || !nodeId)
|
|
202
263
|
return;
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
this._host.requestUpdate();
|
|
264
|
+
this.myHeldLocks.add(nodeId);
|
|
265
|
+
this.safeExecute(() => this.socket.emit('nk:canvas:lock:acquire', { nodeId }), 'acquireLock');
|
|
266
|
+
this.ensureKeepalive();
|
|
208
267
|
}
|
|
209
|
-
|
|
210
|
-
|
|
268
|
+
/** Release a lock this user holds. */
|
|
269
|
+
releaseLock(nodeId) {
|
|
270
|
+
var _a, _b;
|
|
271
|
+
if (!nodeId)
|
|
211
272
|
return;
|
|
212
|
-
|
|
213
|
-
this.
|
|
273
|
+
console.log('[collab] releaseLock()', { nodeId, connected: (_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected });
|
|
274
|
+
const had = this.myHeldLocks.delete(nodeId);
|
|
275
|
+
if (((_b = this.socket) === null || _b === void 0 ? void 0 : _b.connected) && had) {
|
|
276
|
+
this.safeExecute(() => this.socket.emit('nk:canvas:lock:release', { nodeId }), 'releaseLock');
|
|
277
|
+
}
|
|
278
|
+
if (this.myHeldLocks.size === 0 && this.keepaliveInterval) {
|
|
279
|
+
clearInterval(this.keepaliveInterval);
|
|
280
|
+
this.keepaliveInterval = null;
|
|
281
|
+
}
|
|
214
282
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
283
|
+
/** Whether a remote user is currently editing this node. */
|
|
284
|
+
getRemoteLock(nodeId) {
|
|
285
|
+
const lock = this.state.lockedNodes.get(nodeId);
|
|
286
|
+
if (!lock)
|
|
287
|
+
return null;
|
|
288
|
+
if (lock.userId === this.myUserId)
|
|
289
|
+
return null;
|
|
290
|
+
return lock;
|
|
223
291
|
}
|
|
224
|
-
|
|
225
|
-
if (
|
|
292
|
+
ensureKeepalive() {
|
|
293
|
+
if (this.keepaliveInterval || this.myHeldLocks.size === 0)
|
|
226
294
|
return;
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
});
|
|
235
|
-
this._host.requestUpdate();
|
|
295
|
+
this.keepaliveInterval = setInterval(() => {
|
|
296
|
+
var _a;
|
|
297
|
+
if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected))
|
|
298
|
+
return;
|
|
299
|
+
for (const nodeId of this.myHeldLocks) {
|
|
300
|
+
this.safeExecute(() => this.socket.emit('nk:canvas:lock:keepalive', { nodeId }), 'lockKeepalive');
|
|
301
|
+
}
|
|
302
|
+
}, LOCK_KEEPALIVE_MS);
|
|
236
303
|
}
|
|
237
|
-
|
|
238
|
-
if (data
|
|
304
|
+
handleLockState(data) {
|
|
305
|
+
if (!(data === null || data === void 0 ? void 0 : data.locks))
|
|
239
306
|
return;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
else {
|
|
244
|
-
this.state.selections.set(data.userId, {
|
|
245
|
-
userId: data.userId,
|
|
246
|
-
elementIds: data.elementIds,
|
|
247
|
-
});
|
|
307
|
+
this.state.lockedNodes.clear();
|
|
308
|
+
for (const [nodeId, lock] of Object.entries(data.locks)) {
|
|
309
|
+
this.state.lockedNodes.set(nodeId, lock);
|
|
248
310
|
}
|
|
249
311
|
this._host.requestUpdate();
|
|
250
312
|
}
|
|
251
|
-
|
|
252
|
-
|
|
313
|
+
// ==================== Inbound Handlers ====================
|
|
314
|
+
handleOperationReceived(payload) {
|
|
315
|
+
var _a, _b, _c;
|
|
316
|
+
if (!payload || !payload.type)
|
|
253
317
|
return;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
userId: data.userId,
|
|
257
|
-
elementId: data.elementId,
|
|
258
|
-
isTyping: true,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
this.state.typingIndicators.delete(data.userId);
|
|
263
|
-
}
|
|
264
|
-
this._host.requestUpdate();
|
|
265
|
-
}
|
|
266
|
-
handleOperationReceived(data) {
|
|
267
|
-
if (data.canvasId !== this.state.canvasId)
|
|
318
|
+
// Ignore the echo of our own op — we already applied it locally.
|
|
319
|
+
if (payload.userId && payload.userId === this.myUserId)
|
|
268
320
|
return;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
321
|
+
const op = {
|
|
322
|
+
id: (_a = payload.id) !== null && _a !== void 0 ? _a : `remote_${Date.now()}`,
|
|
323
|
+
type: payload.type,
|
|
324
|
+
elementId: payload.elementId,
|
|
325
|
+
data: payload.data || {},
|
|
326
|
+
userId: (_b = payload.userId) !== null && _b !== void 0 ? _b : '',
|
|
327
|
+
timestamp: (_c = payload.timestamp) !== null && _c !== void 0 ? _c : Date.now(),
|
|
328
|
+
version: this.state.serverVersion,
|
|
329
|
+
};
|
|
330
|
+
this.applyRemoteOperation(op);
|
|
275
331
|
}
|
|
276
332
|
// ==================== Remote Operation Application ====================
|
|
277
333
|
applyRemoteOperation(op) {
|
|
@@ -115,6 +115,26 @@ export class SelectionController extends BaseCanvasController {
|
|
|
115
115
|
this._host.setWorkflow(Object.assign(Object.assign({}, this._host.workflow), { nodes: this._host.workflow.nodes.filter(n => !nodeIdsToDelete.has(n.id)), edges: this._host.workflow.edges.filter(e => !edgeIdsToDelete.has(e.id) &&
|
|
116
116
|
!nodeIdsToDelete.has(e.sourceNodeId) &&
|
|
117
117
|
!nodeIdsToDelete.has(e.targetNodeId)) }));
|
|
118
|
+
// Close config panel if the currently configured node is being deleted —
|
|
119
|
+
// otherwise it lingers pointing at a removed node.
|
|
120
|
+
if (this._host.configuredNode && nodeIdsToDelete.has(this._host.configuredNode.id)) {
|
|
121
|
+
this._host.configuredNode = null;
|
|
122
|
+
}
|
|
123
|
+
// Broadcast deletions so other collaborators' canvases apply the change.
|
|
124
|
+
// Node DELETE cascades edges on the receiver, so only broadcast explicit
|
|
125
|
+
// edges that were selected independently — cascade-deleted edges need no
|
|
126
|
+
// separate event.
|
|
127
|
+
const hostAny = this._host;
|
|
128
|
+
if (hostAny.collaborative && hostAny.collaborationController) {
|
|
129
|
+
for (const node of nodesToDelete) {
|
|
130
|
+
hostAny.collaborationController.broadcastOperation('DELETE', node.id, {});
|
|
131
|
+
}
|
|
132
|
+
for (const edge of edgesToDelete) {
|
|
133
|
+
if (edgeIdsToDelete.has(edge.id)) {
|
|
134
|
+
hostAny.collaborationController.broadcastOperation('DELETE_CONNECTOR', edge.id, {});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
118
138
|
this.clearSelection();
|
|
119
139
|
this._host.dispatchWorkflowChanged();
|
|
120
140
|
}
|
|
@@ -37,6 +37,13 @@ export interface CollaborationUser {
|
|
|
37
37
|
isTyping?: boolean;
|
|
38
38
|
typingElementId?: string;
|
|
39
39
|
}
|
|
40
|
+
export interface NodeLock {
|
|
41
|
+
userId: string;
|
|
42
|
+
displayName: string;
|
|
43
|
+
color: string;
|
|
44
|
+
acquiredAt: number;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
}
|
|
40
47
|
export interface CollaborationState {
|
|
41
48
|
connected: boolean;
|
|
42
49
|
canvasId: string | null;
|
|
@@ -51,6 +58,7 @@ export interface CollaborationState {
|
|
|
51
58
|
elementId: string;
|
|
52
59
|
isTyping: boolean;
|
|
53
60
|
}>;
|
|
61
|
+
lockedNodes: Map<string, NodeLock>;
|
|
54
62
|
serverVersion: number;
|
|
55
63
|
pendingOps: Map<string, CanvasOperation>;
|
|
56
64
|
}
|
|
@@ -11,6 +11,7 @@ export { renderConfigPanelTemplate, type ConfigPanelTemplateData, type ConfigPan
|
|
|
11
11
|
export { renderEdgesTemplate, renderEdgeTemplate, renderConnectionLineTemplate, type EdgesTemplateData, type EdgeCallbacks, } from './edges.template.js';
|
|
12
12
|
export { renderWbSidebarTemplate, type WbSidebarTemplateData, } from './wb-sidebar.template.js';
|
|
13
13
|
export { renderRemoteCursorsTemplate, type RemoteCursorsTemplateData, } from './remote-cursors.template.js';
|
|
14
|
+
export { renderLockOverlayTemplate, type LockedNodeEntry, } from './lock-overlay.template.js';
|
|
14
15
|
export { renderPresenceBarTemplate, type PresenceBarTemplateData, } from './presence-bar.template.js';
|
|
15
16
|
export { renderChatbotPanelTemplate, type ChatbotPanelTemplateData, type ChatbotPanelCallbacks, } from './chatbot-panel.template.js';
|
|
16
17
|
export { renderExpandedFrameTemplate, type FrameTemplateData, type FrameTemplateCallbacks, } from './frame.template.js';
|
|
@@ -19,6 +19,8 @@ export { renderEdgesTemplate, renderEdgeTemplate, renderConnectionLineTemplate,
|
|
|
19
19
|
export { renderWbSidebarTemplate, } from './wb-sidebar.template.js';
|
|
20
20
|
// Remote cursors template
|
|
21
21
|
export { renderRemoteCursorsTemplate, } from './remote-cursors.template.js';
|
|
22
|
+
// Node lock overlay template
|
|
23
|
+
export { renderLockOverlayTemplate, } from './lock-overlay.template.js';
|
|
22
24
|
// Presence bar template
|
|
23
25
|
export { renderPresenceBarTemplate, } from './presence-bar.template.js';
|
|
24
26
|
// Chatbot panel template
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Nuraly, Laabidi Aymen
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { nothing } from 'lit';
|
|
7
|
+
import type { NodeLock } from '../interfaces/collaboration.interface.js';
|
|
8
|
+
export interface LockedNodeEntry {
|
|
9
|
+
nodeId: string;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
lock: NodeLock;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Renders "{displayName} is editing" pills above each remotely-locked node.
|
|
16
|
+
* Rendered inside `.nodes-layer`, which is already viewport-transformed, so
|
|
17
|
+
* coordinates are in canvas space — no zoom/pan math needed here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderLockOverlayTemplate(entries: LockedNodeEntry[]): typeof nothing | import("lit-html").TemplateResult<1>[];
|
|
20
|
+
//# sourceMappingURL=lock-overlay.template.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2024 Nuraly, Laabidi Aymen
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { html, nothing } from 'lit';
|
|
7
|
+
import { styleMap } from 'lit/directives/style-map.js';
|
|
8
|
+
/**
|
|
9
|
+
* Renders "{displayName} is editing" pills above each remotely-locked node.
|
|
10
|
+
* Rendered inside `.nodes-layer`, which is already viewport-transformed, so
|
|
11
|
+
* coordinates are in canvas space — no zoom/pan math needed here.
|
|
12
|
+
*/
|
|
13
|
+
export function renderLockOverlayTemplate(entries) {
|
|
14
|
+
if (entries.length === 0)
|
|
15
|
+
return nothing;
|
|
16
|
+
return entries.map(({ nodeId, x, y, lock }) => {
|
|
17
|
+
const pillStyles = {
|
|
18
|
+
left: `${x}px`,
|
|
19
|
+
top: `${y - 28}px`,
|
|
20
|
+
background: lock.color,
|
|
21
|
+
};
|
|
22
|
+
return html `
|
|
23
|
+
<div class="node-lock-pill" data-for-node-id=${nodeId} style=${styleMap(pillStyles)}>
|
|
24
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
25
|
+
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
26
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
27
|
+
</svg>
|
|
28
|
+
<span>${lock.displayName} is editing</span>
|
|
29
|
+
</div>
|
|
30
|
+
`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=lock-overlay.template.js.map
|