@nocobase/flow-engine 2.0.0-alpha.3 → 2.0.0-alpha.4

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.
@@ -49,14 +49,14 @@ var import_core = require("@dnd-kit/core");
49
49
  var import_react = __toESM(require("react"));
50
50
  var import_react_dom = require("react-dom");
51
51
  var import_provider = require("../../provider");
52
- __reExport(dnd_exports, require("./getMousePositionOnElement"), module.exports);
53
- __reExport(dnd_exports, require("./moveBlock"), module.exports);
52
+ __reExport(dnd_exports, require("./findModelUidPosition"), module.exports);
53
+ __reExport(dnd_exports, require("./gridDragPlanner"), module.exports);
54
54
  const EMPTY_COLUMN_UID = "EMPTY_COLUMN";
55
55
  const DragHandler = /* @__PURE__ */ __name(({
56
56
  model,
57
57
  children = /* @__PURE__ */ import_react.default.createElement(import_icons.DragOutlined, null)
58
58
  }) => {
59
- const { attributes, listeners, setNodeRef, isDragging } = (0, import_core.useDraggable)({ id: model.uid });
59
+ const { attributes, listeners, setNodeRef } = (0, import_core.useDraggable)({ id: model.uid });
60
60
  return /* @__PURE__ */ import_react.default.createElement(
61
61
  "span",
62
62
  {
@@ -159,6 +159,6 @@ const DndProvider = /* @__PURE__ */ __name(({
159
159
  DragHandler,
160
160
  Droppable,
161
161
  EMPTY_COLUMN_UID,
162
- ...require("./getMousePositionOnElement"),
163
- ...require("./moveBlock")
162
+ ...require("./findModelUidPosition"),
163
+ ...require("./gridDragPlanner")
164
164
  });
@@ -1043,10 +1043,11 @@ const _FlowModelContext = class _FlowModelContext extends BaseFlowModelContext {
1043
1043
  this.defineProperty("model", {
1044
1044
  value: model
1045
1045
  });
1046
+ const stableRef = (0, import_react.createRef)();
1046
1047
  this.defineProperty("ref", {
1047
1048
  get: /* @__PURE__ */ __name(() => {
1048
1049
  this.model["_refCreated"] = true;
1049
- return (0, import_react.createRef)();
1050
+ return stableRef;
1050
1051
  }, "get")
1051
1052
  });
1052
1053
  this.defineMethod("openView", async function(uid, options) {
@@ -1156,10 +1157,11 @@ const _FlowForkModelContext = class _FlowForkModelContext extends BaseFlowModelC
1156
1157
  this.defineProperty("model", {
1157
1158
  get: /* @__PURE__ */ __name(() => this.fork, "get")
1158
1159
  });
1160
+ const stableRef = (0, import_react.createRef)();
1159
1161
  this.defineProperty("ref", {
1160
1162
  get: /* @__PURE__ */ __name(() => {
1161
1163
  this.fork["_refCreated"] = true;
1162
- return (0, import_react.createRef)();
1164
+ return stableRef;
1163
1165
  }, "get")
1164
1166
  });
1165
1167
  this.defineMethod("runjs", async (code, variables, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.3",
3
+ "version": "2.0.0-alpha.4",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -33,5 +33,5 @@
33
33
  ],
34
34
  "author": "NocoBase Team",
35
35
  "license": "AGPL-3.0",
36
- "gitHead": "8efc23aa78058d871e98a91419f3c4a61762cc15"
36
+ "gitHead": "54f3cab47e7efbdc73377014d05f5fc66a4affbb"
37
37
  }
@@ -0,0 +1,494 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import {
12
+ GridLayoutData,
13
+ LayoutSlot,
14
+ simulateLayoutForSlot,
15
+ getSlotKey,
16
+ resolveDropIntent,
17
+ Point,
18
+ } from '../dnd/gridDragPlanner';
19
+
20
+ const rect = { top: 0, left: 0, width: 100, height: 100 };
21
+
22
+ const createLayout = (rows: Record<string, string[][]>, sizes: Record<string, number[]>): GridLayoutData => ({
23
+ rows,
24
+ sizes,
25
+ });
26
+
27
+ describe('getSlotKey', () => {
28
+ it('should generate unique key for column slot', () => {
29
+ const slot: LayoutSlot = {
30
+ type: 'column',
31
+ rowId: 'row1',
32
+ columnIndex: 0,
33
+ insertIndex: 1,
34
+ position: 'before',
35
+ rect,
36
+ };
37
+
38
+ const key = getSlotKey(slot);
39
+ expect(key).toBe('column:row1:0:1:before');
40
+ });
41
+
42
+ it('should generate unique key for column-edge slot', () => {
43
+ const slot: LayoutSlot = {
44
+ type: 'column-edge',
45
+ rowId: 'row1',
46
+ columnIndex: 2,
47
+ direction: 'left',
48
+ rect,
49
+ };
50
+
51
+ const key = getSlotKey(slot);
52
+ expect(key).toBe('column-edge:row1:2:left');
53
+ });
54
+
55
+ it('should generate unique key for row-gap slot', () => {
56
+ const slot: LayoutSlot = {
57
+ type: 'row-gap',
58
+ targetRowId: 'row2',
59
+ position: 'above',
60
+ rect,
61
+ };
62
+
63
+ const key = getSlotKey(slot);
64
+ expect(key).toBe('row-gap:row2:above');
65
+ });
66
+
67
+ it('should generate unique key for empty-row slot', () => {
68
+ const slot: LayoutSlot = {
69
+ type: 'empty-row',
70
+ rect,
71
+ };
72
+
73
+ const key = getSlotKey(slot);
74
+ expect(key).toBe('empty-row');
75
+ });
76
+
77
+ it('should generate unique key for empty-column slot', () => {
78
+ const slot: LayoutSlot = {
79
+ type: 'empty-column',
80
+ rowId: 'row1',
81
+ columnIndex: 0,
82
+ rect,
83
+ };
84
+
85
+ const key = getSlotKey(slot);
86
+ expect(key).toBe('empty-column:row1:0');
87
+ });
88
+ });
89
+
90
+ describe('resolveDropIntent', () => {
91
+ it('should return closest slot when point is outside all slots', () => {
92
+ const slots: LayoutSlot[] = [
93
+ {
94
+ type: 'column',
95
+ rowId: 'row1',
96
+ columnIndex: 0,
97
+ insertIndex: 0,
98
+ position: 'before',
99
+ rect: { top: 100, left: 100, width: 50, height: 50 },
100
+ },
101
+ ];
102
+
103
+ const point: Point = { x: 0, y: 0 };
104
+ const result = resolveDropIntent(point, slots);
105
+ // Should return the closest slot, not null
106
+ expect(result).toEqual(slots[0]);
107
+ });
108
+
109
+ it('should return the slot that contains the point', () => {
110
+ const targetSlot: LayoutSlot = {
111
+ type: 'column',
112
+ rowId: 'row1',
113
+ columnIndex: 0,
114
+ insertIndex: 0,
115
+ position: 'before',
116
+ rect: { top: 100, left: 100, width: 50, height: 50 },
117
+ };
118
+
119
+ const slots: LayoutSlot[] = [
120
+ {
121
+ type: 'row-gap',
122
+ targetRowId: 'row0',
123
+ position: 'above',
124
+ rect: { top: 50, left: 100, width: 50, height: 20 },
125
+ },
126
+ targetSlot,
127
+ {
128
+ type: 'column-edge',
129
+ rowId: 'row1',
130
+ columnIndex: 1,
131
+ direction: 'right',
132
+ rect: { top: 100, left: 200, width: 20, height: 50 },
133
+ },
134
+ ];
135
+
136
+ const point: Point = { x: 125, y: 125 };
137
+ const result = resolveDropIntent(point, slots);
138
+ expect(result).toEqual(targetSlot);
139
+ });
140
+
141
+ it('should return first matching slot when multiple slots contain the point', () => {
142
+ const firstSlot: LayoutSlot = {
143
+ type: 'empty-row',
144
+ rect: { top: 0, left: 0, width: 500, height: 500 },
145
+ };
146
+
147
+ const secondSlot: LayoutSlot = {
148
+ type: 'column',
149
+ rowId: 'row1',
150
+ columnIndex: 0,
151
+ insertIndex: 0,
152
+ position: 'before',
153
+ rect: { top: 100, left: 100, width: 50, height: 30 },
154
+ };
155
+
156
+ const slots: LayoutSlot[] = [firstSlot, secondSlot];
157
+
158
+ const point: Point = { x: 125, y: 110 };
159
+ const result = resolveDropIntent(point, slots);
160
+ // Returns first slot that contains the point
161
+ expect(result).toEqual(firstSlot);
162
+ });
163
+
164
+ it('should handle empty-column slot correctly', () => {
165
+ const emptyColumnSlot: LayoutSlot = {
166
+ type: 'empty-column',
167
+ rowId: 'row1',
168
+ columnIndex: 0,
169
+ rect: { top: 100, left: 100, width: 100, height: 200 },
170
+ };
171
+
172
+ const slots: LayoutSlot[] = [emptyColumnSlot];
173
+
174
+ const point: Point = { x: 150, y: 150 };
175
+ const result = resolveDropIntent(point, slots);
176
+ expect(result).toEqual(emptyColumnSlot);
177
+ });
178
+
179
+ it('should return null when slots array is empty', () => {
180
+ const slots: LayoutSlot[] = [];
181
+ const point: Point = { x: 100, y: 100 };
182
+ const result = resolveDropIntent(point, slots);
183
+ expect(result).toBeNull();
184
+ });
185
+
186
+ it('should find closest slot when point is outside', () => {
187
+ const slot1: LayoutSlot = {
188
+ type: 'column',
189
+ rowId: 'row1',
190
+ columnIndex: 0,
191
+ insertIndex: 0,
192
+ position: 'before',
193
+ rect: { top: 100, left: 100, width: 50, height: 50 },
194
+ };
195
+
196
+ const slot2: LayoutSlot = {
197
+ type: 'column',
198
+ rowId: 'row2',
199
+ columnIndex: 0,
200
+ insertIndex: 0,
201
+ position: 'before',
202
+ rect: { top: 200, left: 200, width: 50, height: 50 },
203
+ };
204
+
205
+ const slots: LayoutSlot[] = [slot1, slot2];
206
+
207
+ // Point closer to slot1
208
+ const point: Point = { x: 90, y: 90 };
209
+ const result = resolveDropIntent(point, slots);
210
+ expect(result).toEqual(slot1);
211
+ });
212
+ });
213
+
214
+ describe('simulateLayoutForSlot', () => {
215
+ it('removes source from original position before inserting into column slot', () => {
216
+ const layout = createLayout(
217
+ {
218
+ rowA: [['block-1', 'block-2']],
219
+ },
220
+ {
221
+ rowA: [24],
222
+ },
223
+ );
224
+
225
+ const slot: LayoutSlot = {
226
+ type: 'column',
227
+ rowId: 'rowA',
228
+ columnIndex: 0,
229
+ insertIndex: 0,
230
+ position: 'before',
231
+ rect,
232
+ };
233
+
234
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'block-2', layout });
235
+
236
+ expect(result.rows.rowA).toEqual([['block-2', 'block-1']]);
237
+ expect(layout.rows.rowA).toEqual([['block-1', 'block-2']]);
238
+ });
239
+
240
+ it('inserts new column when dropping on column edge and redistributes sizes', () => {
241
+ const layout = createLayout(
242
+ {
243
+ rowA: [['a'], ['b']],
244
+ },
245
+ {
246
+ rowA: [12, 12],
247
+ },
248
+ );
249
+
250
+ const slot: LayoutSlot = {
251
+ type: 'column-edge',
252
+ rowId: 'rowA',
253
+ columnIndex: 0,
254
+ direction: 'left',
255
+ rect,
256
+ };
257
+
258
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'c', layout });
259
+
260
+ expect(result.rows.rowA.length).toBe(3);
261
+ expect(result.rows.rowA[0]).toEqual(['c']);
262
+ expect(result.rows.rowA[1]).toEqual(['a']);
263
+ expect(result.rows.rowA[2]).toEqual(['b']);
264
+ expect(result.sizes.rowA.length).toBe(3);
265
+ expect(result.sizes.rowA.reduce((sum, value) => sum + value, 0)).toBe(24);
266
+ });
267
+
268
+ it('creates a new row above target row when dropping on row-gap slot', () => {
269
+ const layout = createLayout(
270
+ {
271
+ rowA: [['a']],
272
+ rowB: [['b']],
273
+ },
274
+ {
275
+ rowA: [24],
276
+ rowB: [24],
277
+ },
278
+ );
279
+
280
+ const slot: LayoutSlot = {
281
+ type: 'row-gap',
282
+ targetRowId: 'rowB',
283
+ position: 'above',
284
+ rect,
285
+ };
286
+
287
+ const result = simulateLayoutForSlot({
288
+ slot,
289
+ sourceUid: 'c',
290
+ layout,
291
+ generateRowId: () => 'row-inserted',
292
+ });
293
+
294
+ expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
295
+ expect(result.rows['row-inserted']).toEqual([['c']]);
296
+ expect(result.sizes['row-inserted']).toEqual([24]);
297
+ });
298
+
299
+ it('creates a new row when dropping into empty container slot', () => {
300
+ const layout = createLayout({}, {});
301
+
302
+ const slot: LayoutSlot = {
303
+ type: 'empty-row',
304
+ rect,
305
+ };
306
+
307
+ const result = simulateLayoutForSlot({
308
+ slot,
309
+ sourceUid: 'block-x',
310
+ layout,
311
+ generateRowId: () => 'row-new',
312
+ });
313
+
314
+ expect(result.rows['row-new']).toEqual([['block-x']]);
315
+ expect(result.sizes['row-new']).toEqual([24]);
316
+ });
317
+
318
+ it('handles column slot with after position', () => {
319
+ const layout = createLayout(
320
+ {
321
+ rowA: [['block-1', 'block-2']],
322
+ },
323
+ {
324
+ rowA: [24],
325
+ },
326
+ );
327
+
328
+ const slot: LayoutSlot = {
329
+ type: 'column',
330
+ rowId: 'rowA',
331
+ columnIndex: 0,
332
+ insertIndex: 1,
333
+ position: 'after',
334
+ rect,
335
+ };
336
+
337
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'block-3', layout });
338
+
339
+ expect(result.rows.rowA).toEqual([['block-1', 'block-3', 'block-2']]);
340
+ });
341
+
342
+ it('handles column-edge slot on the right', () => {
343
+ const layout = createLayout(
344
+ {
345
+ rowA: [['a'], ['b']],
346
+ },
347
+ {
348
+ rowA: [12, 12],
349
+ },
350
+ );
351
+
352
+ const slot: LayoutSlot = {
353
+ type: 'column-edge',
354
+ rowId: 'rowA',
355
+ columnIndex: 1,
356
+ direction: 'right',
357
+ rect,
358
+ };
359
+
360
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'c', layout });
361
+
362
+ expect(result.rows.rowA.length).toBe(3);
363
+ expect(result.rows.rowA[2]).toEqual(['c']);
364
+ });
365
+
366
+ it('handles row-gap slot below position', () => {
367
+ const layout = createLayout(
368
+ {
369
+ rowA: [['a']],
370
+ rowB: [['b']],
371
+ },
372
+ {
373
+ rowA: [24],
374
+ rowB: [24],
375
+ },
376
+ );
377
+
378
+ const slot: LayoutSlot = {
379
+ type: 'row-gap',
380
+ targetRowId: 'rowA',
381
+ position: 'below',
382
+ rect,
383
+ };
384
+
385
+ const result = simulateLayoutForSlot({
386
+ slot,
387
+ sourceUid: 'c',
388
+ layout,
389
+ generateRowId: () => 'row-inserted',
390
+ });
391
+
392
+ expect(Object.keys(result.rows)).toEqual(['rowA', 'row-inserted', 'rowB']);
393
+ });
394
+
395
+ it('handles empty-column slot by replacing empty column', () => {
396
+ const layout = createLayout(
397
+ {
398
+ rowA: [['EMPTY_COLUMN'], ['block-b']],
399
+ },
400
+ {
401
+ rowA: [12, 12],
402
+ },
403
+ );
404
+
405
+ const slot: LayoutSlot = {
406
+ type: 'empty-column',
407
+ rowId: 'rowA',
408
+ columnIndex: 0,
409
+ rect,
410
+ };
411
+
412
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'new-block', layout });
413
+
414
+ expect(result.rows.rowA[0]).toEqual(['new-block']);
415
+ expect(result.rows.rowA[1]).toEqual(['block-b']);
416
+ });
417
+
418
+ it('removes source from multiple locations', () => {
419
+ const layout = createLayout(
420
+ {
421
+ rowA: [['block-1', 'block-2']],
422
+ rowB: [['block-3', 'block-4']],
423
+ },
424
+ {
425
+ rowA: [24],
426
+ rowB: [24],
427
+ },
428
+ );
429
+
430
+ const slot: LayoutSlot = {
431
+ type: 'column',
432
+ rowId: 'rowA',
433
+ columnIndex: 0,
434
+ insertIndex: 0,
435
+ position: 'before',
436
+ rect,
437
+ };
438
+
439
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'block-3', layout });
440
+
441
+ expect(result.rows.rowA[0]).toContain('block-3');
442
+ expect(result.rows.rowB[0]).not.toContain('block-3');
443
+ });
444
+
445
+ it('preserves column sizes when inserting into existing column', () => {
446
+ const layout = createLayout(
447
+ {
448
+ rowA: [['a'], ['b'], ['c']],
449
+ },
450
+ {
451
+ rowA: [8, 8, 8],
452
+ },
453
+ );
454
+
455
+ const slot: LayoutSlot = {
456
+ type: 'column',
457
+ rowId: 'rowA',
458
+ columnIndex: 1,
459
+ insertIndex: 0,
460
+ position: 'before',
461
+ rect,
462
+ };
463
+
464
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'd', layout });
465
+
466
+ expect(result.sizes.rowA).toEqual([8, 8, 8]);
467
+ });
468
+
469
+ it('redistributes sizes proportionally when adding new column via edge', () => {
470
+ const layout = createLayout(
471
+ {
472
+ rowA: [['a'], ['b'], ['c']],
473
+ },
474
+ {
475
+ rowA: [6, 12, 6],
476
+ },
477
+ );
478
+
479
+ const slot: LayoutSlot = {
480
+ type: 'column-edge',
481
+ rowId: 'rowA',
482
+ columnIndex: 1,
483
+ direction: 'right',
484
+ rect,
485
+ };
486
+
487
+ const result = simulateLayoutForSlot({ slot, sourceUid: 'd', layout });
488
+
489
+ expect(result.rows.rowA.length).toBe(4);
490
+ expect(result.sizes.rowA.length).toBe(4);
491
+ const total = result.sizes.rowA.reduce((sum, size) => sum + size, 0);
492
+ expect(total).toBe(24);
493
+ });
494
+ });