@nuraly/lumenui 0.3.5 → 0.3.6

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.
Files changed (80) hide show
  1. package/dist/nuralyui.bundle.js +2575 -1483
  2. package/dist/nuralyui.bundle.js.gz +0 -0
  3. package/dist/src/components/button/bundle.js +130 -39
  4. package/dist/src/components/button/bundle.js.gz +0 -0
  5. package/dist/src/components/button/button.component.js +7 -4
  6. package/dist/src/components/button/button.style.js +92 -2
  7. package/dist/src/components/canvas/base-canvas.component.d.ts +8 -0
  8. package/dist/src/components/canvas/base-canvas.component.js +75 -3
  9. package/dist/src/components/canvas/bundle.js +2525 -1185
  10. package/dist/src/components/canvas/bundle.js.gz +0 -0
  11. package/dist/src/components/canvas/controllers/collaboration.controller.d.ts +24 -11
  12. package/dist/src/components/canvas/controllers/collaboration.controller.js +176 -120
  13. package/dist/src/components/canvas/controllers/selection.controller.js +20 -0
  14. package/dist/src/components/canvas/interfaces/collaboration.interface.d.ts +8 -0
  15. package/dist/src/components/canvas/templates/index.d.ts +1 -0
  16. package/dist/src/components/canvas/templates/index.js +2 -0
  17. package/dist/src/components/canvas/templates/lock-overlay.template.d.ts +20 -0
  18. package/dist/src/components/canvas/templates/lock-overlay.template.js +33 -0
  19. package/dist/src/components/canvas/workflow-canvas.component.js +52 -24
  20. package/dist/src/components/canvas/workflow-canvas.style.js +62 -1
  21. package/dist/src/components/canvas/workflow-canvas.types.js +50 -4
  22. package/dist/src/components/chat-panel/bundle.js +10 -10
  23. package/dist/src/components/chat-panel/bundle.js.gz +0 -0
  24. package/dist/src/components/chat-panel/chat-panel.component.js +8 -8
  25. package/dist/src/components/chatbot/bundle.js +401 -242
  26. package/dist/src/components/chatbot/bundle.js.gz +0 -0
  27. package/dist/src/components/chatbot/chatbot.style.js +156 -21
  28. package/dist/src/components/chatbot/chatbot.types.d.ts +1 -0
  29. package/dist/src/components/chatbot/core/chatbot-core.controller.js +13 -7
  30. package/dist/src/components/chatbot/providers/workflow-socket-provider.js +1 -2
  31. package/dist/src/components/chatbot/templates/input-box.template.js +58 -30
  32. package/dist/src/components/chatbot/templates/message.template.js +41 -31
  33. package/dist/src/components/chatbot/templates/thread-sidebar.template.js +38 -39
  34. package/dist/src/components/colorpicker/bundle.js +15 -10
  35. package/dist/src/components/colorpicker/bundle.js.gz +0 -0
  36. package/dist/src/components/colorpicker/color-picker.component.js +15 -10
  37. package/dist/src/components/datepicker/bundle.js +10 -10
  38. package/dist/src/components/datepicker/bundle.js.gz +0 -0
  39. package/dist/src/components/datepicker/datepicker.component.js +14 -22
  40. package/dist/src/components/dropdown/bundle.js +13 -12
  41. package/dist/src/components/dropdown/bundle.js.gz +0 -0
  42. package/dist/src/components/dropdown/dropdown.component.js +10 -9
  43. package/dist/src/components/file-upload/bundle.js +15 -14
  44. package/dist/src/components/file-upload/bundle.js.gz +0 -0
  45. package/dist/src/components/file-upload/file-upload.component.js +15 -14
  46. package/dist/src/components/icon/bundle.js +7 -7
  47. package/dist/src/components/icon/bundle.js.gz +0 -0
  48. package/dist/src/components/icon/icon-paths.js +15 -0
  49. package/dist/src/components/iconpicker/bundle.js +214 -122
  50. package/dist/src/components/iconpicker/bundle.js.gz +0 -0
  51. package/dist/src/components/iconpicker/icon-picker.component.js +4 -4
  52. package/dist/src/components/menu/bundle.js +5 -2
  53. package/dist/src/components/menu/bundle.js.gz +0 -0
  54. package/dist/src/components/menu/menu.component.js +5 -2
  55. package/dist/src/components/modal/bundle.js +16 -13
  56. package/dist/src/components/modal/bundle.js.gz +0 -0
  57. package/dist/src/components/modal/modal.component.js +16 -13
  58. package/dist/src/components/panel/bundle.js +28 -28
  59. package/dist/src/components/panel/bundle.js.gz +0 -0
  60. package/dist/src/components/popconfirm/bundle.js +135 -41
  61. package/dist/src/components/popconfirm/bundle.js.gz +0 -0
  62. package/dist/src/components/popconfirm/popconfirm.component.d.ts +15 -119
  63. package/dist/src/components/popconfirm/popconfirm.component.js +158 -162
  64. package/dist/src/components/popconfirm/popconfirm.style.js +94 -0
  65. package/dist/src/components/presence/bundle.js +2 -1
  66. package/dist/src/components/presence/bundle.js.gz +0 -0
  67. package/dist/src/components/presence/presence.component.js +2 -1
  68. package/dist/src/components/table/bundle.js +3 -2
  69. package/dist/src/components/table/bundle.js.gz +0 -0
  70. package/dist/src/components/table/table.component.js +3 -2
  71. package/dist/src/components/tabs/bundle.js +3 -3
  72. package/dist/src/components/tabs/bundle.js.gz +0 -0
  73. package/dist/src/components/timepicker/bundle.js +3 -3
  74. package/dist/src/components/timepicker/bundle.js.gz +0 -0
  75. package/package.json +1 -1
  76. package/packages/common/dist/VERSIONS.md +1 -1
  77. package/packages/common/dist/shared/controllers/dropdown.controller.d.ts +4 -0
  78. package/packages/common/dist/shared/controllers/dropdown.controller.d.ts.map +1 -1
  79. package/packages/common/dist/shared/controllers/dropdown.controller.js +29 -3
  80. package/packages/common/dist/shared/controllers/dropdown.controller.js.map +1 -1
@@ -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
- * Connects to the existing Canvas Gateway at /socket.io/presence.
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
- connect(canvasId: string, canvasType?: 'WORKFLOW' | 'WHITEBOARD'): void;
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 registerEventHandlers;
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
- private handleUsersSync;
30
- private handleUserJoined;
31
- private handleUserLeft;
32
- private handleCursorUpdate;
33
- private handleSelectionUpdate;
34
- private handleTypingIndicator;
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
- * Connects to the existing Canvas Gateway at /socket.io/presence.
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
- connect(canvasId, canvasType = 'WHITEBOARD') {
36
- var _a;
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
- const origin = globalThis.location.origin;
43
- this.socket = io(origin, {
44
- path: '/socket.io/presence',
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
- this.socket.on('connect', () => {
81
+ const onConnected = () => {
82
+ console.log('[collab] socket connected', { canvasId });
51
83
  this.state.connected = true;
52
- this.safeExecute(() => this.socket.emit('canvas:join', { canvasId, canvasType }), 'connect: canvas:join');
84
+ this.safeExecute(() => this.socket.emit('nk:canvas:lock:request'), 'connect: lock:request');
53
85
  this._host.requestUpdate();
54
- });
55
- this.socket.on('disconnect', () => {
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('reconnect', () => {
60
- this.safeExecute(() => this.socket.emit('canvas:join', { canvasId, canvasType }), 'reconnect: canvas:join');
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
- // Register inbound event handlers
63
- this.registerEventHandlers();
64
- // Start stale cursor cleanup
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
- // ==================== Event Registration ====================
105
- registerEventHandlers() {
106
- if (!this.socket)
172
+ // ==================== Inbound routing ====================
173
+ handleWireMessage(payload) {
174
+ if (!(payload === null || payload === void 0 ? void 0 : payload.event))
107
175
  return;
108
- this.socket.on('canvas:users:sync', (data) => {
109
- this.safeExecute(() => this.handleUsersSync(data), 'handleUsersSync');
110
- });
111
- this.socket.on('canvas:user:joined', (data) => {
112
- this.safeExecute(() => this.handleUserJoined(data), 'handleUserJoined');
113
- });
114
- this.socket.on('canvas:user:left', (data) => {
115
- this.safeExecute(() => this.handleUserLeft(data), 'handleUserLeft');
116
- });
117
- this.socket.on('cursor:update', (data) => {
118
- this.safeExecute(() => this.handleCursorUpdate(data), 'handleCursorUpdate');
119
- });
120
- this.socket.on('selection:update', (data) => {
121
- this.safeExecute(() => this.handleSelectionUpdate(data), 'handleSelectionUpdate');
122
- });
123
- this.socket.on('typing:indicator', (data) => {
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', { canvasId: this.state.canvasId, x, y }), 'emitCursorMove');
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', { canvasId: this.state.canvasId, elementIds }), 'broadcastSelectionChange');
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', { canvasId: this.state.canvasId, elementId }), 'broadcastTypingStart');
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', { canvasId: this.state.canvasId, elementId }), 'broadcastTypingStop');
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
- const operation = {
241
+ this.state.pendingOps.set(opId, {
182
242
  id: opId,
183
243
  type,
184
244
  elementId,
185
245
  data,
186
- userId: '', // server fills this
246
+ userId: this.myUserId,
187
247
  timestamp: Date.now(),
188
248
  version: this.state.serverVersion,
189
- };
190
- this.state.pendingOps.set(opId, operation);
191
- this.safeExecute(() => this.socket.emit('operation:apply', {
192
- canvasId: this.state.canvasId,
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
- // ==================== Inbound Handlers ====================
200
- handleUsersSync(data) {
201
- if (data.canvasId !== this.state.canvasId)
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.state.users.clear();
204
- for (const user of data.users) {
205
- this.state.users.set(user.userId, user);
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
- handleUserJoined(data) {
210
- if (data.canvasId !== this.state.canvasId)
268
+ /** Release a lock this user holds. */
269
+ releaseLock(nodeId) {
270
+ var _a, _b;
271
+ if (!nodeId)
211
272
  return;
212
- this.state.users.set(data.user.userId, data.user);
213
- this._host.requestUpdate();
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
- handleUserLeft(data) {
216
- if (data.canvasId !== this.state.canvasId)
217
- return;
218
- this.state.users.delete(data.userId);
219
- this.state.cursors.delete(data.userId);
220
- this.state.selections.delete(data.userId);
221
- this.state.typingIndicators.delete(data.userId);
222
- this._host.requestUpdate();
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
- handleCursorUpdate(data) {
225
- if (data.canvasId !== this.state.canvasId)
292
+ ensureKeepalive() {
293
+ if (this.keepaliveInterval || this.myHeldLocks.size === 0)
226
294
  return;
227
- this.state.cursors.set(data.userId, {
228
- userId: data.userId,
229
- username: data.username,
230
- color: data.color,
231
- x: data.x,
232
- y: data.y,
233
- lastUpdate: Date.now(),
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
- handleSelectionUpdate(data) {
238
- if (data.canvasId !== this.state.canvasId)
304
+ handleLockState(data) {
305
+ if (!(data === null || data === void 0 ? void 0 : data.locks))
239
306
  return;
240
- if (data.elementIds.length === 0) {
241
- this.state.selections.delete(data.userId);
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
- handleTypingIndicator(data) {
252
- if (data.canvasId !== this.state.canvasId)
313
+ // ==================== Inbound Handlers ====================
314
+ handleOperationReceived(payload) {
315
+ var _a, _b, _c;
316
+ if (!payload || !payload.type)
253
317
  return;
254
- if (data.isTyping) {
255
- this.state.typingIndicators.set(data.userId, {
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
- this.state.serverVersion = Math.max(this.state.serverVersion, data.operation.version);
270
- this.applyRemoteOperation(data.operation);
271
- }
272
- handleOperationAck(data) {
273
- this.state.pendingOps.delete(data.operationId);
274
- this.state.serverVersion = Math.max(this.state.serverVersion, data.serverVersion);
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