@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.
- package/dist/assets/index.css +1 -1
- package/dist/atable.d.ts +511 -25
- package/dist/atable.js +1301 -1039
- package/dist/atable.js.map +1 -1
- package/dist/atable.umd.cjs +2 -2
- package/dist/atable.umd.cjs.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/stores/table.d.ts +370 -16
- package/dist/src/stores/table.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +138 -10
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/stores/table.js +111 -13
- package/package.json +3 -3
- package/src/components/AGanttCell.vue +444 -170
- package/src/components/AGanttConnection.vue +164 -0
- package/src/components/ARow.vue +1 -1
- package/src/components/ATable.vue +96 -71
- package/src/components/ATableHeader.vue +1 -1
- package/src/index.ts +4 -0
- package/src/stores/table.ts +129 -13
- package/src/types/index.ts +159 -10
|
@@ -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>
|
package/src/components/ARow.vue
CHANGED
|
@@ -1,103 +1,111 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
:is="
|
|
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
|
-
|
|
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
|
-
</
|
|
68
|
+
</ATableModal>
|
|
48
69
|
</slot>
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
|
|
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,
|
|
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
|
|
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
|
|
145
|
-
if (store.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
package/src/stores/table.ts
CHANGED
|
@@ -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
|
|
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
|
|
172
|
+
return isGanttView.value && row.gantt !== undefined
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
const isRowVisible = (rowIndex: number) => {
|
|
165
|
-
return
|
|
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 !== '
|
|
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 (
|
|
196
|
+
if (isTreeView.value) {
|
|
182
197
|
display.value[rowIndex].childrenOpen = !display.value[rowIndex].childrenOpen
|
|
183
|
-
|
|
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 =
|
|
186
|
-
if (
|
|
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
|
})
|