@stonecrop/atable 0.4.23 → 0.4.25

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.
@@ -3,6 +3,9 @@ import { type CSSProperties, computed, ref } from 'vue'
3
3
 
4
4
  import type {
5
5
  CellContext,
6
+ ConnectionHandle,
7
+ ConnectionPath,
8
+ GanttBarInfo,
6
9
  GanttDragEvent,
7
10
  TableColumn,
8
11
  TableConfig,
@@ -23,45 +26,24 @@ export const createTableStore = (initData: {
23
26
  rows: TableRow[]
24
27
  id?: string
25
28
  config?: TableConfig
26
- table?: { [key: string]: any }
27
- display?: TableDisplay[]
28
29
  modal?: TableModal
29
30
  }) => {
30
31
  const id = initData.id || generateHash()
31
32
  const createStore = defineStore(`table-${id}`, () => {
32
- // util functions
33
- const createTableObject = () => {
34
- const table = {}
35
- for (const [colIndex, column] of columns.value.entries()) {
36
- for (const [rowIndex, row] of rows.value.entries()) {
37
- table[`${colIndex}:${rowIndex}`] = row[column.name]
38
- }
39
- }
40
- return table
41
- }
42
-
43
- const createDisplayObject = (display?: TableDisplay[]) => {
33
+ const createDisplayObject = () => {
44
34
  const defaultDisplay: TableDisplay[] = [Object.assign({}, { rowModified: false })]
45
35
 
46
- // TODO: (typing) what is the type of `display` here?
47
- if (display) {
48
- if ('0:0' in display) {
49
- return display
50
- }
51
- // else if ('default' in display) {
52
- // // TODO: (typing) what is the possible input here for 'default'?
53
- // defaultDisplay = display.default
54
- // }
55
- }
56
-
57
36
  // TODO: (typing) is this type correct for the parent set?
58
37
  const parents = new Set<string | number>()
59
- for (let rowIndex = rows.value.length - 1; rowIndex >= 0; rowIndex--) {
38
+ for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
60
39
  const row = rows.value[rowIndex]
61
- if (row.parent) {
40
+ if (row.parent !== null && row.parent !== undefined) {
62
41
  parents.add(row.parent)
63
42
  }
43
+ }
64
44
 
45
+ for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
46
+ const row = rows.value[rowIndex]
65
47
  defaultDisplay[rowIndex] = {
66
48
  childrenOpen: false,
67
49
  expanded: false,
@@ -81,13 +63,74 @@ export const createTableStore = (initData: {
81
63
  const columns = ref(initData.columns)
82
64
  const rows = ref(initData.rows)
83
65
  const config = ref(initData.config || {})
84
- const table = ref(initData.table || createTableObject())
85
- const display = ref(createDisplayObject(initData.display))
66
+
67
+ // Track row modifications and expand states separately from the computed display
68
+ const rowModifications = ref<Record<number, boolean>>({})
69
+ const rowExpandStates = ref<Record<number, { childrenOpen?: boolean; expanded?: boolean }>>({})
70
+
71
+ const table = computed(() => {
72
+ const table = {}
73
+ for (const [colIndex, column] of columns.value.entries()) {
74
+ for (const [rowIndex, row] of rows.value.entries()) {
75
+ table[`${colIndex}:${rowIndex}`] = row[column.name]
76
+ }
77
+ }
78
+ return table
79
+ })
80
+
81
+ const display = computed({
82
+ get: () => {
83
+ const baseDisplay = createDisplayObject()
84
+
85
+ // Apply persistent modifications and expand states
86
+ for (let i = 0; i < baseDisplay.length; i++) {
87
+ if (rowModifications.value[i]) {
88
+ baseDisplay[i].rowModified = rowModifications.value[i]
89
+ }
90
+ if (rowExpandStates.value[i]) {
91
+ if (rowExpandStates.value[i].childrenOpen !== undefined) {
92
+ baseDisplay[i].childrenOpen = rowExpandStates.value[i].childrenOpen!
93
+ }
94
+ if (rowExpandStates.value[i].expanded !== undefined) {
95
+ baseDisplay[i].expanded = rowExpandStates.value[i].expanded!
96
+ }
97
+ }
98
+ }
99
+
100
+ // Calculate 'open' property for tree view based on parent's childrenOpen state
101
+ if (isTreeView.value) {
102
+ for (let i = 0; i < baseDisplay.length; i++) {
103
+ const row = baseDisplay[i]
104
+ if (!row.isRoot && row.parent !== null && row.parent !== undefined) {
105
+ // Child row is 'open' if its parent's childrenOpen is true
106
+ const parentIndex = row.parent
107
+ if (parentIndex >= 0 && parentIndex < baseDisplay.length) {
108
+ baseDisplay[i].open = baseDisplay[parentIndex].childrenOpen || false
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ return baseDisplay
115
+ },
116
+ set: (newDisplay: TableDisplay[]) => {
117
+ // Only update if the new display is different from the current one; also avoids recursive updates
118
+ if (JSON.stringify(newDisplay) !== JSON.stringify(display.value)) {
119
+ display.value = newDisplay
120
+ }
121
+ },
122
+ })
123
+
86
124
  const modal = ref<TableModal>(initData.modal || { visible: false })
87
125
  const updates = ref<Record<string, string>>({})
126
+ const ganttBars = ref<GanttBarInfo[]>([])
127
+ const connectionHandles = ref<ConnectionHandle[]>([])
128
+ const connectionPaths = ref<ConnectionPath[]>([])
88
129
 
89
130
  // getters
90
131
  const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned))
132
+ const isGanttView = computed(() => config.value.view === 'gantt' || config.value.view === 'tree-gantt')
133
+ const isTreeView = computed(() => config.value.view === 'tree' || config.value.view === 'tree-gantt')
91
134
 
92
135
  const numberedRowWidth = computed(() => {
93
136
  const indent = Math.ceil(rows.value.length / 100 + 1)
@@ -95,7 +138,7 @@ export const createTableStore = (initData: {
95
138
  })
96
139
 
97
140
  const zeroColumn = computed(() =>
98
- config.value.view ? ['list', 'tree', 'list-expansion'].includes(config.value.view) : false
141
+ config.value.view ? ['list', 'tree', 'tree-gantt', 'list-expansion'].includes(config.value.view) : false
99
142
  )
100
143
 
101
144
  // actions
@@ -105,7 +148,7 @@ export const createTableStore = (initData: {
105
148
  const col = columns.value[colIndex]
106
149
 
107
150
  if (table.value[index] !== value) {
108
- display.value[rowIndex].rowModified = true
151
+ rowModifications.value[rowIndex] = true
109
152
  }
110
153
 
111
154
  table.value[index] = value
@@ -116,11 +159,15 @@ export const createTableStore = (initData: {
116
159
  }
117
160
  }
118
161
 
162
+ const updateRows = (newRows: TableRow[]) => {
163
+ rows.value = newRows
164
+ }
165
+
119
166
  const setCellText = (colIndex: number, rowIndex: number, value: string) => {
120
167
  const index = `${colIndex}:${rowIndex}`
121
168
 
122
169
  if (table.value[index] !== value) {
123
- display.value[rowIndex].rowModified = true
170
+ rowModifications.value[rowIndex] = true
124
171
  updates.value[index] = value
125
172
  }
126
173
  }
@@ -158,38 +205,60 @@ export const createTableStore = (initData: {
158
205
 
159
206
  const isRowGantt = (rowIndex: number) => {
160
207
  const row = rows.value[rowIndex]
161
- return config.value.view === 'gantt' && row.indent === 0
208
+ return isGanttView.value && row.gantt !== undefined
162
209
  }
163
210
 
164
211
  const isRowVisible = (rowIndex: number) => {
165
- return config.value.view !== 'tree' || display.value[rowIndex].isRoot || display.value[rowIndex].open
212
+ return !isTreeView.value || display.value[rowIndex].isRoot || display.value[rowIndex].open
166
213
  }
167
214
 
168
215
  const getRowExpandSymbol = (rowIndex: number) => {
169
- if (config.value.view !== 'tree') {
216
+ if (!isTreeView.value && config.value.view !== 'list-expansion') {
170
217
  return ''
171
218
  }
172
219
 
173
- if (display.value[rowIndex].isRoot || display.value[rowIndex].isParent) {
174
- return display.value[rowIndex].childrenOpen ? '-' : '+'
220
+ if (isTreeView.value && (display.value[rowIndex].isRoot || display.value[rowIndex].isParent)) {
221
+ return display.value[rowIndex].childrenOpen ? '' : ''
222
+ }
223
+
224
+ if (config.value.view === 'list-expansion') {
225
+ return display.value[rowIndex].expanded ? '▼' : '►'
175
226
  }
176
227
 
177
228
  return ''
178
229
  }
179
230
 
180
231
  const toggleRowExpand = (rowIndex: number) => {
181
- if (config.value.view === 'tree') {
182
- display.value[rowIndex].childrenOpen = !display.value[rowIndex].childrenOpen
183
- for (let index = rows.value.length - 1; index >= 0; index--) {
184
- if (display.value[index].parent === rowIndex) {
185
- display.value[index].open = !display.value[index].open
186
- if (display.value[index].childrenOpen) {
232
+ if (isTreeView.value) {
233
+ const currentState = rowExpandStates.value[rowIndex] || {}
234
+ const currentChildrenOpen = currentState.childrenOpen ?? display.value[rowIndex].childrenOpen
235
+ const newChildrenOpen = !currentChildrenOpen
236
+
237
+ rowExpandStates.value[rowIndex] = {
238
+ ...currentState,
239
+ childrenOpen: newChildrenOpen,
240
+ }
241
+
242
+ // If we're closing, recursively close all descendant nodes
243
+ if (!newChildrenOpen) {
244
+ for (let index = 0; index < rows.value.length; index++) {
245
+ if (display.value[index].parent === rowIndex) {
246
+ const childState = rowExpandStates.value[index] || {}
247
+ rowExpandStates.value[index] = {
248
+ ...childState,
249
+ childrenOpen: false,
250
+ }
187
251
  toggleRowExpand(index)
188
252
  }
189
253
  }
190
254
  }
191
255
  } else if (config.value.view === 'list-expansion') {
192
- display.value[rowIndex].expanded = !display.value[rowIndex].expanded
256
+ const currentState = rowExpandStates.value[rowIndex] || {}
257
+ const currentExpanded = currentState.expanded ?? display.value[rowIndex].expanded
258
+ rowExpandStates.value[rowIndex] = {
259
+ ...currentState,
260
+ expanded: !currentExpanded,
261
+ }
193
262
  }
194
263
  }
195
264
 
@@ -259,11 +328,99 @@ export const createTableStore = (initData: {
259
328
  }
260
329
  }
261
330
 
331
+ const registerGanttBar = (barInfo: GanttBarInfo) => {
332
+ const existingIndex = ganttBars.value.findIndex(bar => bar.id === barInfo.id)
333
+ if (existingIndex >= 0) {
334
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
335
+ ganttBars.value[existingIndex] = barInfo
336
+ } else {
337
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
338
+ ganttBars.value.push(barInfo)
339
+ }
340
+ }
341
+
342
+ const unregisterGanttBar = (barId: string) => {
343
+ const index = ganttBars.value.findIndex(bar => bar.id === barId)
344
+ if (index >= 0) {
345
+ ganttBars.value.splice(index, 1)
346
+ }
347
+ }
348
+
349
+ const registerConnectionHandle = (handleInfo: ConnectionHandle) => {
350
+ const existingIndex = connectionHandles.value.findIndex(handle => handle.id === handleInfo.id)
351
+ if (existingIndex >= 0) {
352
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
353
+ connectionHandles.value[existingIndex] = handleInfo
354
+ } else {
355
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
356
+ connectionHandles.value.push(handleInfo)
357
+ }
358
+ }
359
+
360
+ const unregisterConnectionHandle = (handleId: string) => {
361
+ const index = connectionHandles.value.findIndex(handle => handle.id === handleId)
362
+ if (index >= 0) {
363
+ connectionHandles.value.splice(index, 1)
364
+ }
365
+ }
366
+
367
+ const createConnection = (
368
+ fromHandleId: string,
369
+ toHandleId: string,
370
+ options?: { style?: ConnectionPath['style']; label?: string }
371
+ ) => {
372
+ const fromHandle = connectionHandles.value.find(h => h.id === fromHandleId)
373
+ const toHandle = connectionHandles.value.find(h => h.id === toHandleId)
374
+
375
+ if (!fromHandle || !toHandle) {
376
+ // eslint-disable-next-line no-console
377
+ console.warn('Cannot create connection: handle not found')
378
+ return null
379
+ }
380
+
381
+ const connection: ConnectionPath = {
382
+ id: `connection-${fromHandleId}-${toHandleId}`,
383
+ from: {
384
+ barId: fromHandle.barId,
385
+ side: fromHandle.side,
386
+ },
387
+ to: {
388
+ barId: toHandle.barId,
389
+ side: toHandle.side,
390
+ },
391
+ style: options?.style,
392
+ label: options?.label,
393
+ }
394
+
395
+ connectionPaths.value.push(connection)
396
+ return connection
397
+ }
398
+
399
+ const deleteConnection = (connectionId: string) => {
400
+ const index = connectionPaths.value.findIndex(conn => conn.id === connectionId)
401
+ if (index >= 0) {
402
+ connectionPaths.value.splice(index, 1)
403
+ return true
404
+ }
405
+ return false
406
+ }
407
+
408
+ const getConnectionsForBar = (barId: string) => {
409
+ return connectionPaths.value.filter(conn => conn.from.barId === barId || conn.to.barId === barId)
410
+ }
411
+
412
+ const getHandlesForBar = (barId: string) => {
413
+ return connectionHandles.value.filter(handle => handle.barId === barId)
414
+ }
415
+
262
416
  return {
263
417
  // state
264
418
  columns,
265
419
  config,
420
+ connectionHandles,
421
+ connectionPaths,
266
422
  display,
423
+ ganttBars,
267
424
  modal,
268
425
  rows,
269
426
  table,
@@ -271,24 +428,35 @@ export const createTableStore = (initData: {
271
428
 
272
429
  // getters
273
430
  hasPinnedColumns,
431
+ isGanttView,
432
+ isTreeView,
274
433
  numberedRowWidth,
275
434
  zeroColumn,
276
435
 
277
436
  // actions
278
437
  closeModal,
438
+ createConnection,
439
+ deleteConnection,
279
440
  getCellData,
280
441
  getCellDisplayValue,
442
+ getConnectionsForBar,
281
443
  getFormattedValue,
444
+ getHandlesForBar,
282
445
  getHeaderCellStyle,
283
- resizeColumn,
284
446
  getIndent,
285
447
  getRowExpandSymbol,
286
448
  isRowGantt,
287
449
  isRowVisible,
450
+ registerConnectionHandle,
451
+ registerGanttBar,
452
+ resizeColumn,
288
453
  setCellData,
289
454
  setCellText,
290
455
  toggleRowExpand,
456
+ unregisterConnectionHandle,
457
+ unregisterGanttBar,
291
458
  updateGanttBar,
459
+ updateRows,
292
460
  }
293
461
  })
294
462
 
@@ -1,4 +1,5 @@
1
1
  import { useElementBounding } from '@vueuse/core'
2
+ import type { Ref, ShallowRef } from 'vue'
2
3
 
3
4
  import { createTableStore } from '../stores/table'
4
5
 
@@ -133,8 +134,8 @@ export interface TableColumn {
133
134
  ganttComponent?: string
134
135
 
135
136
  /**
136
- * The colspan of the Gantt-bar for the column. This is used to determine how many columns
137
- * the Gantt-bar should span.
137
+ * The colspan of the Gantt bar for the column. This determines how many columns
138
+ * the Gantt bar should span across.
138
139
  *
139
140
  * Only applicable for Gantt tables.
140
141
  *
@@ -143,8 +144,8 @@ export interface TableColumn {
143
144
  colspan?: number
144
145
 
145
146
  /**
146
- * The starting column index for the Gantt-bar, excluding any pinned columns. This is
147
- * evaluated automatically while rendering the table.
147
+ * The original column index for the Gantt bar, excluding any pinned columns.
148
+ * This is evaluated automatically while rendering the table.
148
149
  *
149
150
  * Only applicable for Gantt tables.
150
151
  *
@@ -185,8 +186,10 @@ export interface TableConfig {
185
186
  * - `list` - row numbers are displayed in the table
186
187
  * - `list-expansion` - carets are displayed in the number column that expand/collapse the row inline
187
188
  * - `tree` - carets are displayed in the number column that expand/collapse grouped rows
189
+ * - `gantt` - view that allows specific rows to be displayed with Gantt functionality
190
+ * - `tree-gantt` - similar to `gantt`, but allows for tree functionality as well
188
191
  */
189
- view?: 'uncounted' | 'list' | 'list-expansion' | 'tree' | 'gantt'
192
+ view?: 'uncounted' | 'list' | 'list-expansion' | 'tree' | 'gantt' | 'tree-gantt'
190
193
 
191
194
  /**
192
195
  * Control whether the table should be allowed to use the full width of its container.
@@ -303,29 +306,33 @@ export interface TableRow {
303
306
  }
304
307
 
305
308
  /**
306
- * This interface defines the options for a row when it is being viewed as a Gantt chart.
309
+ * Gantt chart options for table rows.
307
310
  * @public
308
311
  */
309
312
  export interface GanttOptions {
310
313
  /**
311
- * The colour to be applied to the row's gantt bar.
314
+ * The color to be applied to the row's gantt bar.
315
+ *
316
+ * @defaultValue '#cccccc'
312
317
  */
313
318
  color?: string
314
319
 
315
320
  /**
316
321
  * The starting column index for the gantt bar.
322
+ *
323
+ * @defaultValue 0
317
324
  */
318
325
  startIndex?: number
319
326
 
320
327
  /**
321
- * The ending column index for the gantt bar. If the endIndex and colspan are not provided,
328
+ * The ending column index for the gantt bar. If endIndex and colspan are not provided,
322
329
  * the bar will stretch to the end of the table.
323
330
  */
324
331
  endIndex?: number
325
332
 
326
333
  /**
327
- * The length of the gantt bar. Useful when only the start index is provided. If the
328
- * colspan and endIndex are not provided, the bar will stretch to the end of the table.
334
+ * The length of the gantt bar in columns. Useful when only the start index is provided.
335
+ * If colspan and endIndex are not provided, the bar will stretch to the end of the table.
329
336
  */
330
337
  colspan?: number
331
338
  }
@@ -468,3 +475,145 @@ export interface TableModalProps {
468
475
  */
469
476
  store: ReturnType<typeof createTableStore>
470
477
  }
478
+
479
+ /**
480
+ * Gantt bar information for VueFlow integration.
481
+ * @public
482
+ */
483
+ export interface GanttBarInfo {
484
+ /**
485
+ * Unique identifier for the gantt bar.
486
+ */
487
+ id: string
488
+
489
+ /**
490
+ * The row index of the gantt bar.
491
+ */
492
+ rowIndex: number
493
+
494
+ /**
495
+ * The primary column index of the gantt bar (typically the start index).
496
+ */
497
+ colIndex: number
498
+
499
+ /**
500
+ * Starting column index of the gantt bar.
501
+ */
502
+ startIndex: Ref<number>
503
+
504
+ /**
505
+ * Ending column index of the gantt bar.
506
+ */
507
+ endIndex: Ref<number>
508
+
509
+ /**
510
+ * Color of the gantt bar.
511
+ */
512
+ color: Ref<string>
513
+
514
+ /**
515
+ * The position of the gantt bar in the ATable component.
516
+ */
517
+ position: {
518
+ x: ShallowRef<number>
519
+ y: ShallowRef<number>
520
+ }
521
+
522
+ /**
523
+ * Display label for the gantt bar.
524
+ */
525
+ label?: string
526
+ }
527
+
528
+ /**
529
+ * Connection handle information for gantt bar connections.
530
+ * @public
531
+ */
532
+ export interface ConnectionHandle {
533
+ /**
534
+ * Unique identifier for the connection handle.
535
+ */
536
+ id: string
537
+
538
+ /**
539
+ * The row index of the gantt bar this handle belongs to.
540
+ */
541
+ rowIndex: number
542
+
543
+ /**
544
+ * The column index of the gantt bar this handle belongs to.
545
+ */
546
+ colIndex: number
547
+
548
+ /**
549
+ * The side of the gantt bar where this handle is located.
550
+ */
551
+ side: 'left' | 'right'
552
+
553
+ /**
554
+ * The position of the connection handle.
555
+ */
556
+ position: {
557
+ x: ShallowRef<number>
558
+ y: ShallowRef<number>
559
+ }
560
+
561
+ /**
562
+ * Whether the handle is currently visible (on hover).
563
+ */
564
+ visible: Ref<boolean>
565
+
566
+ /**
567
+ * Reference to the gantt bar this handle belongs to.
568
+ */
569
+ barId: string
570
+ }
571
+
572
+ /**
573
+ * Connection path between two gantt bars.
574
+ * @public
575
+ */
576
+ export interface ConnectionPath {
577
+ /**
578
+ * Unique identifier for the connection path.
579
+ */
580
+ id: string
581
+
582
+ /**
583
+ * The source connection handle.
584
+ */
585
+ from: {
586
+ barId: string
587
+ side: 'left' | 'right'
588
+ }
589
+
590
+ /**
591
+ * The target connection handle.
592
+ */
593
+ to: {
594
+ barId: string
595
+ side: 'left' | 'right'
596
+ }
597
+
598
+ /**
599
+ * Optional styling for the connection path.
600
+ */
601
+ style?: {
602
+ color?: string
603
+ width?: number
604
+ }
605
+
606
+ /**
607
+ * Optional label for the connection.
608
+ */
609
+ label?: string
610
+ }
611
+
612
+ /**
613
+ * Connection event for handling connection creation/deletion.
614
+ * @public
615
+ */
616
+ export type ConnectionEvent = {
617
+ type: 'create' | 'delete'
618
+ connection: ConnectionPath
619
+ }