@stonecrop/atable 0.4.23 → 0.4.24

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.
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <div class="gantt-connection-overlay">
3
+ <svg
4
+ class="connection-svg"
5
+ :style="{
6
+ position: 'absolute',
7
+ top: 0,
8
+ left: 0,
9
+ width: '100%',
10
+ height: '100%',
11
+ pointerEvents: 'none',
12
+ zIndex: 1,
13
+ }">
14
+ <defs>
15
+ <!-- Define arrowhead marker for connections -->
16
+ <path id="arrowhead" d="M 0 -7 L 20 0 L 0 7Z" stroke="black" stroke-width="1" fill="currentColor"></path>
17
+ <marker
18
+ id="arrowhead-marker"
19
+ markerWidth="10"
20
+ markerHeight="7"
21
+ refX="5"
22
+ refY="3.5"
23
+ orient="auto"
24
+ markerUnits="strokeWidth">
25
+ <polygon points="0 0, 10 3.5, 0 7" fill="currentColor" />
26
+ </marker>
27
+ </defs>
28
+
29
+ <!-- Invisible wider path for easier double-click interaction -->
30
+ <path
31
+ v-for="connection in visibleConnections"
32
+ :key="`${connection.id}-hitbox`"
33
+ :d="getPathData(connection)"
34
+ stroke="transparent"
35
+ :stroke-width="(connection.style?.width || 2) + 10"
36
+ fill="none"
37
+ class="connection-hitbox"
38
+ @dblclick="handleConnectionDelete(connection)" />
39
+
40
+ <!-- Visible connection path -->
41
+ <path
42
+ v-for="connection in visibleConnections"
43
+ :key="connection.id"
44
+ :d="getPathData(connection)"
45
+ :stroke="connection.style?.color || '#666'"
46
+ :stroke-width="connection.style?.width || 2"
47
+ fill="none"
48
+ marker-mid="url(#arrowhead-marker)"
49
+ :id="connection.id"
50
+ class="connection-path animated-path"
51
+ @dblclick="handleConnectionDelete(connection)" />
52
+ </svg>
53
+ </div>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { computed } from 'vue'
58
+
59
+ import { createTableStore } from '../stores/table'
60
+ import type { ConnectionPath } from '../types'
61
+
62
+ const { store } = defineProps<{
63
+ store: ReturnType<typeof createTableStore>
64
+ }>()
65
+
66
+ const emit = defineEmits<{
67
+ 'connection:delete': [connection: ConnectionPath]
68
+ }>()
69
+
70
+ const BEZIER_CURVE_FACTOR = 0.25 // Control point offset factor for bezier curves
71
+ const CONNECTION_HANDLE_SIZE = 16 // Width of the connection handles; this should match the handle size in the AGanttCell component
72
+
73
+ const visibleConnections = computed(() => {
74
+ return store.connectionPaths.filter(connection => {
75
+ const fromBar = store.ganttBars.find(bar => bar.id === connection.from.barId)
76
+ const toBar = store.ganttBars.find(bar => bar.id === connection.to.barId)
77
+ return fromBar && toBar
78
+ })
79
+ })
80
+
81
+ const getPathData = (connection: ConnectionPath, isMarker: Boolean = false) => {
82
+ const fromHandle = store.connectionHandles.find(
83
+ handle => handle.barId === connection.from.barId && handle.side === connection.from.side
84
+ )
85
+ const toHandle = store.connectionHandles.find(
86
+ handle => handle.barId === connection.to.barId && handle.side === connection.to.side
87
+ )
88
+
89
+ if (!fromHandle || !toHandle) return ''
90
+
91
+ const fromX = fromHandle.position.x + CONNECTION_HANDLE_SIZE / 2 // Center of the handle
92
+ const fromY = fromHandle.position.y + CONNECTION_HANDLE_SIZE / 2
93
+ const toX = toHandle.position.x + CONNECTION_HANDLE_SIZE / 2
94
+ const toY = toHandle.position.y + CONNECTION_HANDLE_SIZE / 2
95
+
96
+ // Calculate control points for smooth bezier curve
97
+ const deltaX = Math.abs(toX - fromX)
98
+ const controlPointOffset = Math.max(deltaX * BEZIER_CURVE_FACTOR, 50) // Minimum offset for better curves
99
+ const cp1X = fromX + (connection.from.side === 'left' ? -controlPointOffset : controlPointOffset)
100
+ const cp2X = toX + (connection.to.side === 'left' ? -controlPointOffset : controlPointOffset)
101
+
102
+ // Use cubic bezier curve for smooth connections
103
+
104
+ //calculate the mid point of the curve
105
+ const m0 = { x: 0.5 * fromX + 0.5 * cp1X, y: 0.5 * fromY + 0.5 * fromY }
106
+ const m1 = { x: 0.5 * cp1X + 0.5 * cp2X, y: 0.5 * fromY + 0.5 * toY }
107
+ const m2 = { x: 0.5 * cp2X + 0.5 * toX, y: 0.5 * toY + 0.5 * toY }
108
+ const m3 = { x: 0.5 * m0.x + 0.5 * m1.x, y: 0.5 * m0.y + 0.5 * m1.y }
109
+ const m4 = { x: 0.5 * m1.x + 0.5 * m2.x, y: 0.5 * m1.y + 0.5 * m2.y }
110
+ const midpoint = { x: 0.5 * m3.x + 0.5 * m4.x, y: 0.5 * m3.y + 0.5 * m4.y }
111
+
112
+ // Calculate the bezier curve using two arcs
113
+ return `M ${fromX} ${fromY} Q ${cp1X} ${fromY}, ${midpoint.x} ${midpoint.y} Q ${cp2X} ${toY}, ${toX} ${toY}`
114
+ }
115
+
116
+ const handleConnectionDelete = (connection: ConnectionPath) => {
117
+ if (store.deleteConnection(connection.id)) {
118
+ emit('connection:delete', connection)
119
+ }
120
+ }
121
+ </script>
122
+
123
+ <style scoped>
124
+ .gantt-connection-overlay {
125
+ position: absolute;
126
+ top: 0;
127
+ left: 0;
128
+ width: 100%;
129
+ height: 100%;
130
+ pointer-events: none;
131
+ z-index: 1;
132
+ }
133
+
134
+ .connection-path {
135
+ transition: stroke-width 0.2s ease;
136
+ pointer-events: auto;
137
+ cursor: pointer;
138
+ stroke-dasharray: 5px;
139
+ stroke: var(--sc-cell-text-color);
140
+ }
141
+ #arrowhead-marker polygon {
142
+ fill: var(--sc-cell-text-color);
143
+ }
144
+ .animated-path {
145
+ animation: animated-dash infinite 1.5s linear;
146
+ }
147
+
148
+ .connection-path:hover {
149
+ stroke-width: 3px;
150
+ }
151
+
152
+ .connection-hitbox {
153
+ pointer-events: auto;
154
+ cursor: pointer;
155
+ }
156
+ @keyframes animated-dash {
157
+ 0% {
158
+ stroke-dashoffset: 0px;
159
+ }
160
+ 100% {
161
+ stroke-dashoffset: -10px;
162
+ }
163
+ }
164
+ </style>
@@ -10,7 +10,7 @@
10
10
  {{ rowIndex + 1 }}
11
11
  </td>
12
12
  <td
13
- v-else-if="store.config.view === 'tree'"
13
+ v-else-if="store.isTreeView"
14
14
  :tabIndex="-1"
15
15
  class="tree-index"
16
16
  :class="store.hasPinnedColumns ? 'sticky-index' : ''"
@@ -1,103 +1,111 @@
1
1
  <template>
2
- <table
3
- ref="table"
4
- class="atable"
5
- :style="{
6
- width: store.config.fullWidth ? '100%' : 'auto',
7
- }"
8
- v-on-click-outside="store.closeModal">
9
- <slot name="header" :data="store">
10
- <ATableHeader :columns="store.columns" :store="store" />
11
- </slot>
12
-
13
- <tbody>
14
- <slot name="body" :data="store">
15
- <ARow v-for="(row, rowIndex) in store.rows" :key="row.id" :row="row" :rowIndex="rowIndex" :store="store">
16
- <template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
17
- <component
18
- v-if="column.isGantt"
19
- :is="column.ganttComponent || 'AGanttCell'"
20
- :store="store"
21
- :columnsCount="store.columns.length - pinnedColumnCount"
22
- :color="row.gantt?.color"
23
- :start="row.gantt?.startIndex"
24
- :end="row.gantt?.endIndex"
25
- :colspan="column.colspan"
26
- :pinned="column.pinned"
27
- :rowIndex="rowIndex"
28
- :colIndex="column.originalIndex ?? colIndex"
29
- :style="{
30
- textAlign: column?.align || 'center',
31
- minWidth: column?.width || '40ch',
32
- width: store.config.fullWidth ? 'auto' : null,
33
- }" />
2
+ <div class="atable-container" style="position: relative">
3
+ <!-- Main table view -->
4
+ <table
5
+ ref="table"
6
+ class="atable"
7
+ :style="{
8
+ width: store.config.fullWidth ? '100%' : 'auto',
9
+ }"
10
+ v-on-click-outside="store.closeModal">
11
+ <slot name="header" :data="store">
12
+ <ATableHeader :columns="store.columns" :store="store" />
13
+ </slot>
14
+
15
+ <tbody>
16
+ <slot name="body" :data="store">
17
+ <ARow v-for="(row, rowIndex) in store.rows" :key="row.id" :row="row" :rowIndex="rowIndex" :store="store">
18
+ <template v-for="(column, colIndex) in getProcessedColumnsForRow(row)" :key="column.name">
19
+ <component
20
+ v-if="column.isGantt"
21
+ :is="column.ganttComponent || 'AGanttCell'"
22
+ :store="store"
23
+ :columnsCount="store.columns.length - pinnedColumnCount"
24
+ :color="row.gantt?.color"
25
+ :start="row.gantt?.startIndex"
26
+ :end="row.gantt?.endIndex"
27
+ :colspan="column.colspan"
28
+ :pinned="column.pinned"
29
+ :rowIndex="rowIndex"
30
+ :colIndex="column.originalIndex ?? colIndex"
31
+ :style="{
32
+ textAlign: column?.align || 'center',
33
+ minWidth: column?.width || '40ch',
34
+ width: store.config.fullWidth ? 'auto' : null,
35
+ }"
36
+ @connection:create="handleConnectionCreate" />
37
+ <component
38
+ v-else
39
+ :is="column.cellComponent || 'ACell'"
40
+ :store="store"
41
+ :pinned="column.pinned"
42
+ :rowIndex="rowIndex"
43
+ :colIndex="colIndex"
44
+ :style="{
45
+ textAlign: column?.align || 'center',
46
+ width: store.config.fullWidth ? 'auto' : null,
47
+ }"
48
+ spellcheck="false" />
49
+ </template>
50
+ </ARow>
51
+ </slot>
52
+ </tbody>
53
+
54
+ <slot name="footer" :data="store" />
55
+
56
+ <!-- Modal overlay -->
57
+ <slot name="modal" :data="store">
58
+ <ATableModal v-show="store.modal.visible" :store="store">
59
+ <template #default>
34
60
  <component
35
- v-else
36
- :is="column.cellComponent || 'ACell'"
61
+ :key="`${store.modal.rowIndex}:${store.modal.colIndex}`"
62
+ :is="store.modal.component"
63
+ :colIndex="store.modal.colIndex"
64
+ :rowIndex="store.modal.rowIndex"
37
65
  :store="store"
38
- :pinned="column.pinned"
39
- :rowIndex="rowIndex"
40
- :colIndex="colIndex"
41
- :style="{
42
- textAlign: column?.align || 'center',
43
- width: store.config.fullWidth ? 'auto' : null,
44
- }"
45
- spellcheck="false" />
66
+ v-bind="store.modal.componentProps" />
46
67
  </template>
47
- </ARow>
68
+ </ATableModal>
48
69
  </slot>
49
- </tbody>
50
-
51
- <slot name="footer" :data="store" />
52
- <slot name="modal" :data="store">
53
- <ATableModal v-show="store.modal.visible" :store="store">
54
- <template #default>
55
- <component
56
- :key="`${store.modal.rowIndex}:${store.modal.colIndex}`"
57
- :is="store.modal.component"
58
- :colIndex="store.modal.colIndex"
59
- :rowIndex="store.modal.rowIndex"
60
- :store="store"
61
- v-bind="store.modal.componentProps" />
62
- </template>
63
- </ATableModal>
64
- </slot>
65
- </table>
70
+ </table>
71
+
72
+ <!-- Connection overlay for gantt connections -->
73
+ <AGanttConnection v-if="store.isGanttView" :store="store" @connection:delete="handleConnectionDelete" />
74
+ </div>
66
75
  </template>
67
76
 
68
77
  <script setup lang="ts">
69
78
  import { vOnClickOutside } from '@vueuse/components'
70
79
  import { useMutationObserver } from '@vueuse/core'
71
- import { nextTick, onMounted, useTemplateRef, computed, watch } from 'vue'
80
+ import { computed, nextTick, onMounted, useTemplateRef, watch } from 'vue'
72
81
 
82
+ import AGanttConnection from './AGanttConnection.vue'
73
83
  import ARow from './ARow.vue'
74
84
  import ATableHeader from './ATableHeader.vue'
75
85
  import ATableModal from './ATableModal.vue'
76
86
  import { createTableStore } from '../stores/table'
77
- import type { GanttDragEvent, TableColumn, TableConfig, TableRow } from '../types'
87
+ import type { ConnectionEvent, ConnectionPath, GanttDragEvent, TableColumn, TableConfig, TableRow } from '../types'
78
88
 
79
89
  const modelValue = defineModel<TableRow[]>({ required: true })
80
90
 
81
91
  const {
82
92
  id,
83
93
  columns,
84
- rows = [],
85
94
  config = new Object(),
86
95
  } = defineProps<{
87
96
  id?: string
88
97
  columns: TableColumn[]
89
- rows?: TableRow[]
90
98
  config?: TableConfig
91
99
  }>()
92
100
 
93
101
  const emit = defineEmits<{
94
102
  cellUpdate: [{ colIndex: number; rowIndex: number; newValue: any; oldValue: any }]
95
103
  'gantt:drag': [event: GanttDragEvent]
104
+ 'connection:event': [event: ConnectionEvent]
96
105
  }>()
97
106
 
98
107
  const tableRef = useTemplateRef<HTMLTableElement>('table')
99
- const rowsValue = modelValue.value.length > 0 ? modelValue.value : rows
100
- const store = createTableStore({ columns, rows: rowsValue, id, config })
108
+ const store = createTableStore({ columns, rows: modelValue.value, id, config })
101
109
 
102
110
  store.$onAction(({ name, store, args, after }) => {
103
111
  if (name === 'setCellData' || name === 'setCellText') {
@@ -141,8 +149,8 @@ onMounted(() => {
141
149
  if (columns.some(column => column.pinned)) {
142
150
  assignStickyCellWidths()
143
151
 
144
- // in tree view, also add a mutation observer to capture and adjust expanded rows
145
- if (store.config.view === 'tree') {
152
+ // in tree views, also add a mutation observer to capture and adjust expanded rows
153
+ if (store.isTreeView) {
146
154
  useMutationObserver(tableRef, assignStickyCellWidths, { childList: true, subtree: true })
147
155
  }
148
156
  }
@@ -204,8 +212,7 @@ window.addEventListener('keydown', (event: KeyboardEvent) => {
204
212
  })
205
213
 
206
214
  const getProcessedColumnsForRow = (row: TableRow) => {
207
- const isGanttRow = row.indent === 0
208
- if (!isGanttRow || pinnedColumnCount.value === 0) {
215
+ if (!row.gantt || pinnedColumnCount.value === 0) {
209
216
  return store.columns
210
217
  }
211
218
 
@@ -229,10 +236,28 @@ const getProcessedColumnsForRow = (row: TableRow) => {
229
236
  return result
230
237
  }
231
238
 
232
- defineExpose({ store })
239
+ const handleConnectionCreate = (connection: ConnectionPath) => {
240
+ emit('connection:event', { type: 'create', connection })
241
+ }
242
+
243
+ const handleConnectionDelete = (connection: ConnectionPath) => {
244
+ emit('connection:event', { type: 'delete', connection })
245
+ }
246
+
247
+ defineExpose({
248
+ store,
249
+ createConnection: store.createConnection,
250
+ deleteConnection: store.deleteConnection,
251
+ getConnectionsForBar: store.getConnectionsForBar,
252
+ getHandlesForBar: store.getHandlesForBar,
253
+ })
233
254
  </script>
234
255
 
235
256
  <style>
257
+ .atable-container {
258
+ position: relative;
259
+ }
260
+
236
261
  .sticky-index {
237
262
  position: sticky;
238
263
  left: 0px;
@@ -6,7 +6,7 @@
6
6
  id="header-index"
7
7
  :class="[
8
8
  store.hasPinnedColumns ? 'sticky-index' : '',
9
- store.config.view === 'tree' ? 'tree-index' : '',
9
+ store.isTreeView ? 'tree-index' : '',
10
10
  store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
11
11
  ]"
12
12
  class="list-index" />
package/src/index.ts CHANGED
@@ -12,6 +12,10 @@ import ATableModal from './components/ATableModal.vue'
12
12
  export { createTableStore } from './stores/table'
13
13
  export type {
14
14
  CellContext,
15
+ ConnectionEvent,
16
+ ConnectionHandle,
17
+ ConnectionPath,
18
+ GanttBarInfo,
15
19
  GanttDragEvent,
16
20
  GanttOptions,
17
21
  TableColumn,
@@ -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,
@@ -56,12 +59,15 @@ export const createTableStore = (initData: {
56
59
 
57
60
  // TODO: (typing) is this type correct for the parent set?
58
61
  const parents = new Set<string | number>()
59
- for (let rowIndex = rows.value.length - 1; rowIndex >= 0; rowIndex--) {
62
+ for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
60
63
  const row = rows.value[rowIndex]
61
- if (row.parent) {
64
+ if (row.parent !== null && row.parent !== undefined) {
62
65
  parents.add(row.parent)
63
66
  }
67
+ }
64
68
 
69
+ for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
70
+ const row = rows.value[rowIndex]
65
71
  defaultDisplay[rowIndex] = {
66
72
  childrenOpen: false,
67
73
  expanded: false,
@@ -85,9 +91,14 @@ export const createTableStore = (initData: {
85
91
  const display = ref(createDisplayObject(initData.display))
86
92
  const modal = ref<TableModal>(initData.modal || { visible: false })
87
93
  const updates = ref<Record<string, string>>({})
94
+ const ganttBars = ref<GanttBarInfo[]>([])
95
+ const connectionHandles = ref<ConnectionHandle[]>([])
96
+ const connectionPaths = ref<ConnectionPath[]>([])
88
97
 
89
98
  // getters
90
99
  const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned))
100
+ const isGanttView = computed(() => config.value.view === 'gantt' || config.value.view === 'tree-gantt')
101
+ const isTreeView = computed(() => config.value.view === 'tree' || config.value.view === 'tree-gantt')
91
102
 
92
103
  const numberedRowWidth = computed(() => {
93
104
  const indent = Math.ceil(rows.value.length / 100 + 1)
@@ -95,7 +106,7 @@ export const createTableStore = (initData: {
95
106
  })
96
107
 
97
108
  const zeroColumn = computed(() =>
98
- config.value.view ? ['list', 'tree', 'list-expansion'].includes(config.value.view) : false
109
+ config.value.view ? ['list', 'tree', 'tree-gantt', 'list-expansion'].includes(config.value.view) : false
99
110
  )
100
111
 
101
112
  // actions
@@ -158,32 +169,40 @@ export const createTableStore = (initData: {
158
169
 
159
170
  const isRowGantt = (rowIndex: number) => {
160
171
  const row = rows.value[rowIndex]
161
- return config.value.view === 'gantt' && row.indent === 0
172
+ return isGanttView.value && row.gantt !== undefined
162
173
  }
163
174
 
164
175
  const isRowVisible = (rowIndex: number) => {
165
- return config.value.view !== 'tree' || display.value[rowIndex].isRoot || display.value[rowIndex].open
176
+ return !isTreeView.value || display.value[rowIndex].isRoot || display.value[rowIndex].open
166
177
  }
167
178
 
168
179
  const getRowExpandSymbol = (rowIndex: number) => {
169
- if (config.value.view !== 'tree') {
180
+ if (!isTreeView.value && config.value.view !== 'list-expansion') {
170
181
  return ''
171
182
  }
172
183
 
173
- if (display.value[rowIndex].isRoot || display.value[rowIndex].isParent) {
174
- return display.value[rowIndex].childrenOpen ? '-' : '+'
184
+ if (isTreeView.value && (display.value[rowIndex].isRoot || display.value[rowIndex].isParent)) {
185
+ return display.value[rowIndex].childrenOpen ? '' : ''
186
+ }
187
+
188
+ if (config.value.view === 'list-expansion') {
189
+ return display.value[rowIndex].expanded ? '▼' : '►'
175
190
  }
176
191
 
177
192
  return ''
178
193
  }
179
194
 
180
195
  const toggleRowExpand = (rowIndex: number) => {
181
- if (config.value.view === 'tree') {
196
+ if (isTreeView.value) {
182
197
  display.value[rowIndex].childrenOpen = !display.value[rowIndex].childrenOpen
183
- for (let index = rows.value.length - 1; index >= 0; index--) {
198
+ const isOpen = display.value[rowIndex].childrenOpen
199
+
200
+ for (let index = 0; index < rows.value.length; index++) {
184
201
  if (display.value[index].parent === rowIndex) {
185
- display.value[index].open = !display.value[index].open
186
- if (display.value[index].childrenOpen) {
202
+ display.value[index].open = isOpen
203
+ if (!isOpen) {
204
+ // If we're closing, also close any children recursively
205
+ display.value[index].childrenOpen = false
187
206
  toggleRowExpand(index)
188
207
  }
189
208
  }
@@ -259,11 +278,98 @@ export const createTableStore = (initData: {
259
278
  }
260
279
  }
261
280
 
281
+ const registerGanttBar = (barInfo: GanttBarInfo) => {
282
+ const existingIndex = ganttBars.value.findIndex(bar => bar.id === barInfo.id)
283
+ if (existingIndex >= 0) {
284
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
285
+ ganttBars.value[existingIndex] = barInfo
286
+ } else {
287
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
288
+ ganttBars.value.push(barInfo)
289
+ }
290
+ }
291
+
292
+ const unregisterGanttBar = (barId: string) => {
293
+ const index = ganttBars.value.findIndex(bar => bar.id === barId)
294
+ if (index >= 0) {
295
+ ganttBars.value.splice(index, 1)
296
+ }
297
+ }
298
+
299
+ const registerConnectionHandle = (handleInfo: ConnectionHandle) => {
300
+ const existingIndex = connectionHandles.value.findIndex(handle => handle.id === handleInfo.id)
301
+ if (existingIndex >= 0) {
302
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
303
+ connectionHandles.value[existingIndex] = handleInfo
304
+ } else {
305
+ // @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
306
+ connectionHandles.value.push(handleInfo)
307
+ }
308
+ }
309
+
310
+ const unregisterConnectionHandle = (handleId: string) => {
311
+ const index = connectionHandles.value.findIndex(handle => handle.id === handleId)
312
+ if (index >= 0) {
313
+ connectionHandles.value.splice(index, 1)
314
+ }
315
+ }
316
+
317
+ const createConnection = (
318
+ fromHandleId: string,
319
+ toHandleId: string,
320
+ options?: { style?: ConnectionPath['style']; label?: string }
321
+ ) => {
322
+ const fromHandle = connectionHandles.value.find(h => h.id === fromHandleId)
323
+ const toHandle = connectionHandles.value.find(h => h.id === toHandleId)
324
+
325
+ if (!fromHandle || !toHandle) {
326
+ console.warn('Cannot create connection: handle not found')
327
+ return null
328
+ }
329
+
330
+ const connection: ConnectionPath = {
331
+ id: `connection-${fromHandleId}-${toHandleId}`,
332
+ from: {
333
+ barId: fromHandle.barId,
334
+ side: fromHandle.side,
335
+ },
336
+ to: {
337
+ barId: toHandle.barId,
338
+ side: toHandle.side,
339
+ },
340
+ style: options?.style,
341
+ label: options?.label,
342
+ }
343
+
344
+ connectionPaths.value.push(connection)
345
+ return connection
346
+ }
347
+
348
+ const deleteConnection = (connectionId: string) => {
349
+ const index = connectionPaths.value.findIndex(conn => conn.id === connectionId)
350
+ if (index >= 0) {
351
+ connectionPaths.value.splice(index, 1)
352
+ return true
353
+ }
354
+ return false
355
+ }
356
+
357
+ const getConnectionsForBar = (barId: string) => {
358
+ return connectionPaths.value.filter(conn => conn.from.barId === barId || conn.to.barId === barId)
359
+ }
360
+
361
+ const getHandlesForBar = (barId: string) => {
362
+ return connectionHandles.value.filter(handle => handle.barId === barId)
363
+ }
364
+
262
365
  return {
263
366
  // state
264
367
  columns,
265
368
  config,
369
+ connectionHandles,
370
+ connectionPaths,
266
371
  display,
372
+ ganttBars,
267
373
  modal,
268
374
  rows,
269
375
  table,
@@ -271,23 +377,33 @@ export const createTableStore = (initData: {
271
377
 
272
378
  // getters
273
379
  hasPinnedColumns,
380
+ isGanttView,
381
+ isTreeView,
274
382
  numberedRowWidth,
275
383
  zeroColumn,
276
384
 
277
385
  // actions
278
386
  closeModal,
387
+ createConnection,
388
+ deleteConnection,
279
389
  getCellData,
280
390
  getCellDisplayValue,
391
+ getConnectionsForBar,
281
392
  getFormattedValue,
393
+ getHandlesForBar,
282
394
  getHeaderCellStyle,
283
- resizeColumn,
284
395
  getIndent,
285
396
  getRowExpandSymbol,
286
397
  isRowGantt,
287
398
  isRowVisible,
399
+ registerConnectionHandle,
400
+ registerGanttBar,
401
+ resizeColumn,
288
402
  setCellData,
289
403
  setCellText,
290
404
  toggleRowExpand,
405
+ unregisterConnectionHandle,
406
+ unregisterGanttBar,
291
407
  updateGanttBar,
292
408
  }
293
409
  })