@stonecrop/atable 0.30.0 → 0.32.0
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 +804 -1
- package/dist/atable.js +2809 -1944
- package/dist/atable.js.map +1 -1
- package/dist/src/components/ACell.vue.d.ts +17 -0
- package/dist/src/components/ACell.vue.d.ts.map +1 -0
- package/dist/src/components/AGanttCell.vue.d.ts +34 -0
- package/dist/src/components/AGanttCell.vue.d.ts.map +1 -0
- package/dist/src/components/AGanttConnection.vue.d.ts +13 -0
- package/dist/src/components/AGanttConnection.vue.d.ts.map +1 -0
- package/dist/src/components/ARow.vue.d.ts +2693 -0
- package/dist/src/components/ARow.vue.d.ts.map +1 -0
- package/dist/src/components/ARowActions.vue.d.ts +16 -0
- package/dist/src/components/ARowActions.vue.d.ts.map +1 -0
- package/dist/src/components/ATable.vue.d.ts +13358 -0
- package/dist/src/components/ATable.vue.d.ts.map +1 -0
- package/dist/src/components/ATableColumnFilter.vue.d.ts +11 -0
- package/dist/src/components/ATableColumnFilter.vue.d.ts.map +1 -0
- package/dist/src/components/ATableHeader.vue.d.ts +20 -0
- package/dist/src/components/ATableHeader.vue.d.ts.map +1 -0
- package/dist/src/components/ATableLoading.vue.d.ts +14 -0
- package/dist/src/components/ATableLoading.vue.d.ts.map +1 -0
- package/dist/src/components/ATableLoadingBar.vue.d.ts +14 -0
- package/dist/src/components/ATableLoadingBar.vue.d.ts.map +1 -0
- package/dist/src/components/ATableModal.vue.d.ts +18 -0
- package/dist/src/components/ATableModal.vue.d.ts.map +1 -0
- package/dist/src/components/ATablePaginationFooter.vue.d.ts +17 -0
- package/dist/src/components/ATablePaginationFooter.vue.d.ts.map +1 -0
- package/dist/src/icons/index.d.ts +14 -7
- package/dist/src/icons/index.d.ts.map +1 -1
- package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +35 -29
- package/dist/atable.d.ts +0 -3560
- package/dist/atable.tsbuildinfo +0 -1
- package/dist/icons/stonecrop-ui-icon-add.svg +0 -5
- package/dist/icons/stonecrop-ui-icon-delete.svg +0 -5
- package/dist/icons/stonecrop-ui-icon-duplicate.svg +0 -5
- package/dist/icons/stonecrop-ui-icon-insert-above.svg +0 -15
- package/dist/icons/stonecrop-ui-icon-insert-below.svg +0 -15
- package/dist/icons/stonecrop-ui-icon-move.svg +0 -4
- package/dist/icons/stonecrop-ui-icon-open.svg +0 -5
- package/dist/src/composables/table-pagination.js +0 -108
- package/dist/src/icons/index.js +0 -32
- package/dist/src/index.js +0 -33
- package/dist/src/linkSearchableText.js +0 -44
- package/dist/src/resolveFilterType.js +0 -27
- package/dist/src/schemaToColumns.js +0 -59
- package/dist/src/stores/table.js +0 -833
- package/dist/src/types/index.js +0 -0
- package/dist/src/utils.js +0 -39
- package/src/components/ACell.vue +0 -410
- package/src/components/AGanttCell.vue +0 -638
- package/src/components/AGanttConnection.vue +0 -164
- package/src/components/ARow.vue +0 -251
- package/src/components/ARowActions.vue +0 -407
- package/src/components/ATable.vue +0 -538
- package/src/components/ATableColumnFilter.vue +0 -216
- package/src/components/ATableHeader.vue +0 -141
- package/src/components/ATableLoading.vue +0 -86
- package/src/components/ATableLoadingBar.vue +0 -81
- package/src/components/ATableModal.vue +0 -52
- package/src/components/ATablePaginationFooter.vue +0 -100
- package/src/composables/table-pagination.ts +0 -172
- package/src/icons/index.ts +0 -38
- package/src/icons/stonecrop-ui-icon-add.svg +0 -5
- package/src/icons/stonecrop-ui-icon-delete.svg +0 -5
- package/src/icons/stonecrop-ui-icon-duplicate.svg +0 -5
- package/src/icons/stonecrop-ui-icon-insert-above.svg +0 -15
- package/src/icons/stonecrop-ui-icon-insert-below.svg +0 -15
- package/src/icons/stonecrop-ui-icon-move.svg +0 -4
- package/src/icons/stonecrop-ui-icon-open.svg +0 -5
- package/src/index.ts +0 -62
- package/src/linkSearchableText.ts +0 -42
- package/src/resolveFilterType.ts +0 -32
- package/src/schemaToColumns.ts +0 -67
- package/src/shims-vue.d.ts +0 -10
- package/src/stores/table.ts +0 -978
- package/src/types/index.ts +0 -794
- package/src/utils.ts +0 -40
package/src/stores/table.ts
DELETED
|
@@ -1,978 +0,0 @@
|
|
|
1
|
-
import { defineStore } from 'pinia'
|
|
2
|
-
import { componentCategory } from '@stonecrop/schema'
|
|
3
|
-
import type { BadgeDescriptor } from '@stonecrop/schema'
|
|
4
|
-
import { type CSSProperties, computed, ref } from 'vue'
|
|
5
|
-
|
|
6
|
-
import { linkSearchableText } from '../linkSearchableText'
|
|
7
|
-
import type {
|
|
8
|
-
CellContext,
|
|
9
|
-
ConnectionHandle,
|
|
10
|
-
ConnectionPath,
|
|
11
|
-
GanttBarInfo,
|
|
12
|
-
GanttDragEvent,
|
|
13
|
-
TableColumn,
|
|
14
|
-
TableConfig,
|
|
15
|
-
TableDisplay,
|
|
16
|
-
TableModal,
|
|
17
|
-
TableRow,
|
|
18
|
-
} from '../types'
|
|
19
|
-
import { resolveFilterType } from '../resolveFilterType'
|
|
20
|
-
import { formatCurrency, formatQuantity, generateHash } from '../utils'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Represents the state of a single filter
|
|
24
|
-
* @public
|
|
25
|
-
*/
|
|
26
|
-
export interface FilterState {
|
|
27
|
-
/** The main filter value */
|
|
28
|
-
value: any
|
|
29
|
-
/** Start value for date range filters */
|
|
30
|
-
startValue?: any
|
|
31
|
-
/** End value for date range filters */
|
|
32
|
-
endValue?: any
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Record mapping column indices to their filter states
|
|
37
|
-
* @public
|
|
38
|
-
*/
|
|
39
|
-
export type FilterStateRecord = Record<number, FilterState>
|
|
40
|
-
|
|
41
|
-
// Quantity columns hold a composite `{ qty, uom, ... }` value (see `QuantityValue` in
|
|
42
|
-
// `@stonecrop/aform`); numeric comparisons (filtering, sorting) operate on `qty`.
|
|
43
|
-
//
|
|
44
|
-
// Currency columns hold a composite `{ amount, currency, baseAmount, ... }` value (see
|
|
45
|
-
// `CurrencyValue` in `@stonecrop/aform`), and a single column routinely mixes currencies —
|
|
46
|
-
// comparing raw `amount` would rank 5 EUR equal to 5 USD and match 100 JPY against a `> 100`
|
|
47
|
-
// filter. `baseAmount` is the whole reason the value carries one: it restates every row in the
|
|
48
|
-
// record's base currency, which is the only axis on which they are comparable. Fall back to
|
|
49
|
-
// `amount` for a value that predates the base conversion (or was authored without one).
|
|
50
|
-
function toComparableNumber(cellValue: unknown): number {
|
|
51
|
-
if (cellValue !== null && typeof cellValue === 'object') {
|
|
52
|
-
if ('qty' in cellValue) return Number(cellValue.qty)
|
|
53
|
-
if ('baseAmount' in cellValue && (cellValue as { baseAmount: unknown }).baseAmount != null) {
|
|
54
|
-
return Number((cellValue as { baseAmount: unknown }).baseAmount)
|
|
55
|
-
}
|
|
56
|
-
if ('amount' in cellValue) return Number((cellValue as { amount: unknown }).amount)
|
|
57
|
-
}
|
|
58
|
-
return Number(cellValue)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isNodeOpen(rowIndex: number, treeDisplay: TableDisplay[]): boolean {
|
|
62
|
-
const row = treeDisplay[rowIndex]
|
|
63
|
-
if (row.isRoot) {
|
|
64
|
-
return true // Root nodes are always open
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (row.parent === null || row.parent === undefined) {
|
|
68
|
-
return true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const parentIndex = row.parent
|
|
72
|
-
if (parentIndex < 0 || parentIndex >= treeDisplay.length) {
|
|
73
|
-
return false
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const parent = treeDisplay[parentIndex]
|
|
77
|
-
// Node is open if parent's children are open AND parent itself is open
|
|
78
|
-
return (parent.childrenOpen || false) && isNodeOpen(parentIndex, treeDisplay)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function applyFilter(cellValue: any, filter: FilterState, column: TableColumn): boolean {
|
|
82
|
-
const filterType = resolveFilterType(column)
|
|
83
|
-
const value = filter.value
|
|
84
|
-
|
|
85
|
-
if (!value && filterType !== 'dateRange' && filterType !== 'checkbox') return true
|
|
86
|
-
|
|
87
|
-
switch (filterType) {
|
|
88
|
-
case 'text': {
|
|
89
|
-
const searchableText = column.linkDoctype
|
|
90
|
-
? linkSearchableText(cellValue)
|
|
91
|
-
: typeof cellValue === 'object' && cellValue !== null
|
|
92
|
-
? Object.values(cellValue).join(' ')
|
|
93
|
-
: String(cellValue || '')
|
|
94
|
-
return searchableText.toLowerCase().includes(String(value).toLowerCase())
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
case 'number': {
|
|
98
|
-
const numValue = toComparableNumber(cellValue)
|
|
99
|
-
const filterNum = Number(value)
|
|
100
|
-
return !isNaN(numValue) && !isNaN(filterNum) && numValue === filterNum
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
case 'select':
|
|
104
|
-
return cellValue === value
|
|
105
|
-
|
|
106
|
-
case 'checkbox':
|
|
107
|
-
// For checkbox filter, if checked (true), show only truthy values
|
|
108
|
-
// If unchecked (false/undefined), show all values
|
|
109
|
-
if (value === true) {
|
|
110
|
-
return !!cellValue
|
|
111
|
-
}
|
|
112
|
-
return true
|
|
113
|
-
|
|
114
|
-
case 'date': {
|
|
115
|
-
// Handle both timestamp numbers and date strings
|
|
116
|
-
let cellDate: Date
|
|
117
|
-
if (typeof cellValue === 'number') {
|
|
118
|
-
// Apply the same year transformation as in the format function
|
|
119
|
-
const originalDate = new Date(cellValue)
|
|
120
|
-
const currentYear = new Date().getFullYear()
|
|
121
|
-
cellDate = new Date(currentYear, originalDate.getMonth(), originalDate.getDate())
|
|
122
|
-
} else {
|
|
123
|
-
cellDate = new Date(String(cellValue))
|
|
124
|
-
}
|
|
125
|
-
const filterDate = new Date(String(value))
|
|
126
|
-
return cellDate.toDateString() === filterDate.toDateString()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
case 'dateRange': {
|
|
130
|
-
const startValue = filter.startValue
|
|
131
|
-
const endValue = filter.endValue
|
|
132
|
-
if (!startValue && !endValue) return true
|
|
133
|
-
|
|
134
|
-
// Handle both timestamp numbers and date strings
|
|
135
|
-
let cellDateRange: Date
|
|
136
|
-
if (typeof cellValue === 'number') {
|
|
137
|
-
// Apply the same year transformation as in the format function
|
|
138
|
-
const originalDate = new Date(cellValue)
|
|
139
|
-
const currentYear = new Date().getFullYear()
|
|
140
|
-
cellDateRange = new Date(currentYear, originalDate.getMonth(), originalDate.getDate())
|
|
141
|
-
} else {
|
|
142
|
-
cellDateRange = new Date(String(cellValue))
|
|
143
|
-
}
|
|
144
|
-
if (startValue && cellDateRange < new Date(String(startValue))) return false
|
|
145
|
-
if (endValue && cellDateRange > new Date(String(endValue))) return false
|
|
146
|
-
|
|
147
|
-
return true
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
default:
|
|
151
|
-
return true
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Compute the CSS indentation for a tree-table cell. Pure helper (captures no store state);
|
|
157
|
-
* imported directly by cell components rather than exposed on the store's public return.
|
|
158
|
-
* @internal
|
|
159
|
-
*/
|
|
160
|
-
export function getIndent(colIndex: number, indentLevel?: number): string {
|
|
161
|
-
if (indentLevel && colIndex === 0 && indentLevel > 0) {
|
|
162
|
-
return `${indentLevel}ch`
|
|
163
|
-
} else {
|
|
164
|
-
return 'inherit'
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Create a table store
|
|
170
|
-
* @param initData - Initial data for the table store
|
|
171
|
-
* @returns table store instance
|
|
172
|
-
* @public
|
|
173
|
-
*/
|
|
174
|
-
export const createTableStore = (initData: {
|
|
175
|
-
columns: TableColumn[]
|
|
176
|
-
rows: TableRow[]
|
|
177
|
-
id?: string
|
|
178
|
-
config?: TableConfig
|
|
179
|
-
modal?: TableModal
|
|
180
|
-
linkResolver?: ((doctype: string, id: string) => Promise<string | undefined>) | null
|
|
181
|
-
}) => {
|
|
182
|
-
const id = initData.id || generateHash()
|
|
183
|
-
const linkResolver = initData.linkResolver ?? null
|
|
184
|
-
const createStore = defineStore(`table-${id}`, () => {
|
|
185
|
-
const createDisplayObject = () => {
|
|
186
|
-
const defaultDisplay: TableDisplay[] = [Object.assign({}, { rowModified: false })]
|
|
187
|
-
|
|
188
|
-
// TODO: (typing) is this type correct for the parent set?
|
|
189
|
-
const parents = new Set<string | number>()
|
|
190
|
-
for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
|
|
191
|
-
const row = rows.value[rowIndex]
|
|
192
|
-
if (row.parent !== null && row.parent !== undefined) {
|
|
193
|
-
parents.add(row.parent)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Helper function to check if a row has gantt data
|
|
198
|
-
const hasGanttData = (rowIndex: number): boolean => {
|
|
199
|
-
return rows.value[rowIndex]?.gantt !== undefined
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Helper function to check if any descendant has gantt data
|
|
203
|
-
const hasGanttDescendant = (rowIndex: number): boolean => {
|
|
204
|
-
for (let i = 0; i < rows.value.length; i++) {
|
|
205
|
-
if (rows.value[i].parent === rowIndex) {
|
|
206
|
-
if (hasGanttData(i) || hasGanttDescendant(i)) {
|
|
207
|
-
return true
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Helper function to determine if children should be open based on expansion mode
|
|
215
|
-
const shouldChildrenBeOpen = (rowIndex: number): boolean => {
|
|
216
|
-
const currentConfig = config.value
|
|
217
|
-
const expansionMode =
|
|
218
|
-
currentConfig.view === 'tree' || currentConfig.view === 'tree-gantt'
|
|
219
|
-
? currentConfig.defaultTreeExpansion
|
|
220
|
-
: undefined
|
|
221
|
-
|
|
222
|
-
if (!expansionMode) return true // Default behavior - start expanded (leaf mode)
|
|
223
|
-
|
|
224
|
-
switch (expansionMode) {
|
|
225
|
-
case 'root':
|
|
226
|
-
return false // Only root nodes are visible, all children start collapsed
|
|
227
|
-
case 'branch':
|
|
228
|
-
// Only expand if this node leads to gantt nodes OR if this node itself has gantt data AND has gantt children
|
|
229
|
-
return hasGanttDescendant(rowIndex)
|
|
230
|
-
case 'leaf':
|
|
231
|
-
return true // All nodes should be expanded
|
|
232
|
-
default:
|
|
233
|
-
return true // Default to expanded if unknown mode
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
for (let rowIndex = 0; rowIndex < rows.value.length; rowIndex++) {
|
|
238
|
-
const row = rows.value[rowIndex]
|
|
239
|
-
const isRootNode = row.parent === null || row.parent === undefined
|
|
240
|
-
const isParentNode = parents.has(rowIndex)
|
|
241
|
-
|
|
242
|
-
defaultDisplay[rowIndex] = {
|
|
243
|
-
childrenOpen: shouldChildrenBeOpen(rowIndex),
|
|
244
|
-
expanded: false,
|
|
245
|
-
indent: row.indent || 0,
|
|
246
|
-
isParent: isParentNode,
|
|
247
|
-
isRoot: isRootNode,
|
|
248
|
-
rowModified: false,
|
|
249
|
-
open: isRootNode, // This will be recalculated later for non-root nodes
|
|
250
|
-
parent: row.parent,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return defaultDisplay
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// state
|
|
258
|
-
const columns = ref(initData.columns)
|
|
259
|
-
const rows = ref(initData.rows)
|
|
260
|
-
const config = ref<TableConfig>(initData.config || {})
|
|
261
|
-
|
|
262
|
-
// Track row modifications and expand states separately from the computed display
|
|
263
|
-
const rowModifications = ref<Record<number, boolean>>({})
|
|
264
|
-
const rowExpandStates = ref<Record<number, { childrenOpen?: boolean; expanded?: boolean }>>({})
|
|
265
|
-
|
|
266
|
-
const table = computed(() => {
|
|
267
|
-
const tableData: Record<string, any> = {}
|
|
268
|
-
for (const [colIndex, column] of columns.value.entries()) {
|
|
269
|
-
for (const [rowIndex, row] of rows.value.entries()) {
|
|
270
|
-
tableData[`${colIndex}:${rowIndex}`] = row[column.name]
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return tableData
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
const display = computed({
|
|
277
|
-
get: () => {
|
|
278
|
-
const baseDisplay = createDisplayObject()
|
|
279
|
-
|
|
280
|
-
// Apply persistent modifications and expand states
|
|
281
|
-
for (let i = 0; i < baseDisplay.length; i++) {
|
|
282
|
-
if (rowModifications.value[i]) {
|
|
283
|
-
baseDisplay[i].rowModified = rowModifications.value[i]
|
|
284
|
-
}
|
|
285
|
-
if (rowExpandStates.value[i]) {
|
|
286
|
-
if (rowExpandStates.value[i].childrenOpen !== undefined) {
|
|
287
|
-
baseDisplay[i].childrenOpen = rowExpandStates.value[i].childrenOpen!
|
|
288
|
-
}
|
|
289
|
-
if (rowExpandStates.value[i].expanded !== undefined) {
|
|
290
|
-
baseDisplay[i].expanded = rowExpandStates.value[i].expanded!
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Calculate 'open' property for tree view based on parent's childrenOpen state
|
|
296
|
-
if (isTreeView.value) {
|
|
297
|
-
// Helper function to check if all ancestors are open
|
|
298
|
-
|
|
299
|
-
for (let i = 0; i < baseDisplay.length; i++) {
|
|
300
|
-
const row = baseDisplay[i]
|
|
301
|
-
if (!row.isRoot) {
|
|
302
|
-
baseDisplay[i].open = isNodeOpen(i, baseDisplay)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return baseDisplay
|
|
308
|
-
},
|
|
309
|
-
set: (newDisplay: TableDisplay[]) => {
|
|
310
|
-
// Only update if the new display is different from the current one; also avoids recursive updates
|
|
311
|
-
if (JSON.stringify(newDisplay) !== JSON.stringify(display.value)) {
|
|
312
|
-
display.value = newDisplay
|
|
313
|
-
}
|
|
314
|
-
},
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
const modal = ref<TableModal>(initData.modal || { visible: false })
|
|
318
|
-
const updates = ref<Record<string, string>>({})
|
|
319
|
-
const ganttBars = ref<GanttBarInfo[]>([])
|
|
320
|
-
const connectionHandles = ref<ConnectionHandle[]>([])
|
|
321
|
-
const connectionPaths = ref<ConnectionPath[]>([])
|
|
322
|
-
const sortState = ref<{ column: number | null; direction: 'asc' | 'desc' | null }>({
|
|
323
|
-
column: null,
|
|
324
|
-
direction: null,
|
|
325
|
-
})
|
|
326
|
-
const filterState = ref<FilterStateRecord>({})
|
|
327
|
-
|
|
328
|
-
// getters
|
|
329
|
-
const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned))
|
|
330
|
-
const isGanttView = computed(() => config.value.view === 'gantt' || config.value.view === 'tree-gantt')
|
|
331
|
-
const isTreeView = computed(() => config.value.view === 'tree' || config.value.view === 'tree-gantt')
|
|
332
|
-
const isDependencyGraphEnabled = computed(() => {
|
|
333
|
-
const currentConfig = config.value
|
|
334
|
-
return currentConfig.view === 'gantt' || currentConfig.view === 'tree-gantt'
|
|
335
|
-
? currentConfig.dependencyGraph !== false
|
|
336
|
-
: true
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
const numberedRowWidth = computed(() => {
|
|
340
|
-
const indent = Math.ceil(rows.value.length / 100 + 1)
|
|
341
|
-
return `${indent}ch`
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
const zeroColumn = computed(() =>
|
|
345
|
-
config.value.view ? ['list', 'tree', 'tree-gantt', 'list-expansion'].includes(config.value.view) : false
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
const filteredRows = computed(() => {
|
|
349
|
-
let filtered: Array<TableRow & { originalIndex: number }> = rows.value.map((row, originalIndex) => ({
|
|
350
|
-
...row,
|
|
351
|
-
originalIndex,
|
|
352
|
-
}))
|
|
353
|
-
|
|
354
|
-
// Apply filters
|
|
355
|
-
Object.entries(filterState.value).forEach(([colIndexStr, filter]) => {
|
|
356
|
-
const colIndex = parseInt(colIndexStr)
|
|
357
|
-
const column = columns.value[colIndex]
|
|
358
|
-
|
|
359
|
-
if (!column) return
|
|
360
|
-
|
|
361
|
-
// Skip if filter has no value (except for dateRange and checkbox which can have different value structures)
|
|
362
|
-
const hasFilterValue =
|
|
363
|
-
filter.value ||
|
|
364
|
-
filter.startValue ||
|
|
365
|
-
filter.endValue ||
|
|
366
|
-
(resolveFilterType(column) === 'checkbox' && filter.value !== undefined)
|
|
367
|
-
|
|
368
|
-
if (!hasFilterValue) return
|
|
369
|
-
|
|
370
|
-
filtered = filtered.filter(row => {
|
|
371
|
-
const cellValue = row[column.name]
|
|
372
|
-
return applyFilter(cellValue, filter, column)
|
|
373
|
-
})
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
// Apply sorting if active
|
|
377
|
-
if (sortState.value.column !== null && sortState.value.direction) {
|
|
378
|
-
const column = columns.value[sortState.value.column]
|
|
379
|
-
const direction = sortState.value.direction
|
|
380
|
-
|
|
381
|
-
filtered.sort((a, b) => {
|
|
382
|
-
let aVal = a[column.name]
|
|
383
|
-
let bVal = b[column.name]
|
|
384
|
-
|
|
385
|
-
if (aVal === null || aVal === undefined) aVal = ''
|
|
386
|
-
if (bVal === null || bVal === undefined) bVal = ''
|
|
387
|
-
|
|
388
|
-
const aNum = toComparableNumber(aVal)
|
|
389
|
-
const bNum = toComparableNumber(bVal)
|
|
390
|
-
const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aVal !== '' && bVal !== ''
|
|
391
|
-
|
|
392
|
-
if (isNumeric) {
|
|
393
|
-
return direction === 'asc' ? aNum - bNum : bNum - aNum
|
|
394
|
-
} else {
|
|
395
|
-
const aStr = String(aVal).toLowerCase()
|
|
396
|
-
const bStr = String(bVal).toLowerCase()
|
|
397
|
-
return direction === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr)
|
|
398
|
-
}
|
|
399
|
-
})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return filtered
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
// actions
|
|
406
|
-
const getCellData = (colIndex: number, rowIndex: number): any => table.value[`${colIndex}:${rowIndex}`]
|
|
407
|
-
const setCellData = (colIndex: number, rowIndex: number, value: any) => {
|
|
408
|
-
const index = `${colIndex}:${rowIndex}`
|
|
409
|
-
const col = columns.value[colIndex]
|
|
410
|
-
|
|
411
|
-
if (table.value[index] !== value) {
|
|
412
|
-
rowModifications.value[rowIndex] = true
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
table.value[index] = value
|
|
416
|
-
// Create a new row object to ensure reactivity
|
|
417
|
-
rows.value[rowIndex] = {
|
|
418
|
-
...rows.value[rowIndex],
|
|
419
|
-
[col.name]: value,
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const updateRows = (newRows: TableRow[]) => {
|
|
424
|
-
rows.value = newRows
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const setCellText = (colIndex: number, rowIndex: number, value: string) => {
|
|
428
|
-
const index = `${colIndex}:${rowIndex}`
|
|
429
|
-
|
|
430
|
-
if (table.value[index] !== value) {
|
|
431
|
-
rowModifications.value[rowIndex] = true
|
|
432
|
-
updates.value[index] = value
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const getHeaderCellStyle = (column: TableColumn): CSSProperties => {
|
|
437
|
-
const isLastCol = columns.value.indexOf(column) === columns.value.length - 1
|
|
438
|
-
|
|
439
|
-
// if the table is full width, the last column should not be resizable;
|
|
440
|
-
// ref: https://github.com/agritheory/stonecrop/pull/196#issuecomment-2503762641
|
|
441
|
-
|
|
442
|
-
const isResizable = config.value.fullWidth ? column.resizable && !isLastCol : column.resizable
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
width: column.width || '40ch',
|
|
446
|
-
textAlign: column.align || 'center',
|
|
447
|
-
...(isResizable && {
|
|
448
|
-
resize: 'horizontal',
|
|
449
|
-
overflow: 'hidden',
|
|
450
|
-
whiteSpace: 'nowrap',
|
|
451
|
-
}),
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const resizeColumn = (colIndex: number, newWidth: number) => {
|
|
456
|
-
if (colIndex < 0 || colIndex >= columns.value.length) return
|
|
457
|
-
|
|
458
|
-
const minWidth = 40
|
|
459
|
-
const finalWidth = Math.max(newWidth, minWidth)
|
|
460
|
-
|
|
461
|
-
columns.value[colIndex] = {
|
|
462
|
-
...columns.value[colIndex],
|
|
463
|
-
width: `${finalWidth}px`,
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const isRowGantt = (rowIndex: number) => {
|
|
468
|
-
const row = rows.value[rowIndex]
|
|
469
|
-
return isGanttView.value && row.gantt !== undefined
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const isRowVisible = (rowIndex: number) => {
|
|
473
|
-
return !isTreeView.value || display.value[rowIndex].isRoot || display.value[rowIndex].open
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const getRowExpandSymbol = (rowIndex: number) => {
|
|
477
|
-
if (!isTreeView.value && config.value.view !== 'list-expansion') {
|
|
478
|
-
return ''
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (isTreeView.value && (display.value[rowIndex].isRoot || display.value[rowIndex].isParent)) {
|
|
482
|
-
return display.value[rowIndex].childrenOpen ? '▼' : '►'
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (config.value.view === 'list-expansion') {
|
|
486
|
-
return display.value[rowIndex].expanded ? '▼' : '►'
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return ''
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const toggleRowExpand = (rowIndex: number) => {
|
|
493
|
-
if (isTreeView.value) {
|
|
494
|
-
const currentState = rowExpandStates.value[rowIndex] || {}
|
|
495
|
-
const currentChildrenOpen = currentState.childrenOpen ?? display.value[rowIndex].childrenOpen
|
|
496
|
-
const newChildrenOpen = !currentChildrenOpen
|
|
497
|
-
|
|
498
|
-
rowExpandStates.value[rowIndex] = {
|
|
499
|
-
...currentState,
|
|
500
|
-
childrenOpen: newChildrenOpen,
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// If we're closing, recursively close all descendant nodes
|
|
504
|
-
if (!newChildrenOpen) {
|
|
505
|
-
closeDescendants(rowIndex)
|
|
506
|
-
}
|
|
507
|
-
} else if (config.value.view === 'list-expansion') {
|
|
508
|
-
const currentState = rowExpandStates.value[rowIndex] || {}
|
|
509
|
-
const currentExpanded = currentState.expanded ?? display.value[rowIndex].expanded
|
|
510
|
-
rowExpandStates.value[rowIndex] = {
|
|
511
|
-
...currentState,
|
|
512
|
-
expanded: !currentExpanded,
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const closeDescendants = (parentRowIndex: number) => {
|
|
518
|
-
for (let index = 0; index < rows.value.length; index++) {
|
|
519
|
-
if (display.value[index].parent === parentRowIndex) {
|
|
520
|
-
const childState = rowExpandStates.value[index] || {}
|
|
521
|
-
rowExpandStates.value[index] = {
|
|
522
|
-
...childState,
|
|
523
|
-
childrenOpen: false,
|
|
524
|
-
}
|
|
525
|
-
// Recursively close this child's descendants
|
|
526
|
-
closeDescendants(index)
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const getCellDisplayValue = (colIndex: number, rowIndex: number) => {
|
|
532
|
-
const cellData = getCellData(colIndex, rowIndex)
|
|
533
|
-
return getFormattedValue(colIndex, rowIndex, cellData)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const getFormattedValue = (colIndex: number, rowIndex: number, value: any) => {
|
|
537
|
-
const column = columns.value[colIndex]
|
|
538
|
-
const row = rows.value[rowIndex]
|
|
539
|
-
const format = column.format
|
|
540
|
-
|
|
541
|
-
if (!format) {
|
|
542
|
-
// Default cell formatting comes from the component's category; anything without an
|
|
543
|
-
// opinion (including an unknown component) renders the raw value.
|
|
544
|
-
const category = componentCategory(column.component)
|
|
545
|
-
if (category === 'boolean') return value ? '✓' : '✗'
|
|
546
|
-
if (category === 'date') return value != null ? new Date(String(value)).toLocaleDateString() : value
|
|
547
|
-
if (category === 'datetime') return value != null ? new Date(String(value)).toLocaleString() : value
|
|
548
|
-
if (category === 'quantity') return formatQuantity(value)
|
|
549
|
-
if (category === 'currency') return formatCurrency(value)
|
|
550
|
-
return value
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (typeof format === 'function') {
|
|
554
|
-
return format(value, { table: table.value, row, column })
|
|
555
|
-
} else if (typeof format === 'string') {
|
|
556
|
-
// parse format function from string. The alias keeps the declaration on one line so the
|
|
557
|
-
// disable directive below stays attached to the Function call it covers.
|
|
558
|
-
type DeserializedFormat = (value: any, context?: CellContext) => string | BadgeDescriptor
|
|
559
|
-
// oxlint-disable-next-line @typescript-eslint/no-implied-eval
|
|
560
|
-
const formatFn: DeserializedFormat = Function(`"use strict";return (${format})`)()
|
|
561
|
-
return formatFn(value, { table: table.value, row, column })
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return value
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const closeModal = (event: MouseEvent) => {
|
|
568
|
-
if (!(event.target instanceof Node)) {
|
|
569
|
-
// if the target is not a node, it's probably a custom click event to Document or Window
|
|
570
|
-
// err on the side of closing the modal in that case
|
|
571
|
-
if (modal.value.visible) modal.value.visible = false
|
|
572
|
-
} else if (!modal.value.parent?.contains(event.target)) {
|
|
573
|
-
if (modal.value.visible) modal.value.visible = false
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const updateGanttBar = (event: GanttDragEvent) => {
|
|
578
|
-
// update the local gantt bar cache
|
|
579
|
-
const ganttBar = rows.value[event.rowIndex]?.gantt
|
|
580
|
-
if (ganttBar) {
|
|
581
|
-
if (event.type === 'resize') {
|
|
582
|
-
if (event.edge === 'start') {
|
|
583
|
-
ganttBar.startIndex = event.newStart
|
|
584
|
-
ganttBar.endIndex = event.end
|
|
585
|
-
ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex
|
|
586
|
-
} else if (event.edge === 'end') {
|
|
587
|
-
ganttBar.startIndex = event.start
|
|
588
|
-
ganttBar.endIndex = event.newEnd
|
|
589
|
-
ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex
|
|
590
|
-
}
|
|
591
|
-
} else if (event.type === 'bar') {
|
|
592
|
-
ganttBar.startIndex = event.newStart
|
|
593
|
-
ganttBar.endIndex = event.newEnd
|
|
594
|
-
ganttBar.colspan = ganttBar.endIndex - ganttBar.startIndex
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const registerGanttBar = (barInfo: GanttBarInfo) => {
|
|
600
|
-
const existingIndex = ganttBars.value.findIndex(bar => bar.id === barInfo.id)
|
|
601
|
-
if (existingIndex >= 0) {
|
|
602
|
-
// @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
|
|
603
|
-
ganttBars.value[existingIndex] = barInfo
|
|
604
|
-
} else {
|
|
605
|
-
// @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
|
|
606
|
-
ganttBars.value.push(barInfo)
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const unregisterGanttBar = (barId: string) => {
|
|
611
|
-
const index = ganttBars.value.findIndex(bar => bar.id === barId)
|
|
612
|
-
if (index >= 0) {
|
|
613
|
-
ganttBars.value.splice(index, 1)
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const registerConnectionHandle = (handleInfo: ConnectionHandle) => {
|
|
618
|
-
const existingIndex = connectionHandles.value.findIndex(handle => handle.id === handleInfo.id)
|
|
619
|
-
if (existingIndex >= 0) {
|
|
620
|
-
// @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
|
|
621
|
-
connectionHandles.value[existingIndex] = handleInfo
|
|
622
|
-
} else {
|
|
623
|
-
// @ts-expect-error TODO: for some reason, the IDE is expecting an unref'd value
|
|
624
|
-
connectionHandles.value.push(handleInfo)
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const unregisterConnectionHandle = (handleId: string) => {
|
|
629
|
-
const index = connectionHandles.value.findIndex(handle => handle.id === handleId)
|
|
630
|
-
if (index >= 0) {
|
|
631
|
-
connectionHandles.value.splice(index, 1)
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const createConnection = (
|
|
636
|
-
fromHandleId: string,
|
|
637
|
-
toHandleId: string,
|
|
638
|
-
options?: { style?: ConnectionPath['style']; label?: string }
|
|
639
|
-
) => {
|
|
640
|
-
const fromHandle = connectionHandles.value.find(h => h.id === fromHandleId)
|
|
641
|
-
const toHandle = connectionHandles.value.find(h => h.id === toHandleId)
|
|
642
|
-
|
|
643
|
-
if (!fromHandle || !toHandle) {
|
|
644
|
-
console.warn('Cannot create connection: handle not found')
|
|
645
|
-
return null
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const connection: ConnectionPath = {
|
|
649
|
-
id: `connection-${fromHandleId}-${toHandleId}`,
|
|
650
|
-
from: {
|
|
651
|
-
barId: fromHandle.barId,
|
|
652
|
-
side: fromHandle.side,
|
|
653
|
-
},
|
|
654
|
-
to: {
|
|
655
|
-
barId: toHandle.barId,
|
|
656
|
-
side: toHandle.side,
|
|
657
|
-
},
|
|
658
|
-
style: options?.style,
|
|
659
|
-
label: options?.label,
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
connectionPaths.value.push(connection)
|
|
663
|
-
return connection
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const deleteConnection = (connectionId: string) => {
|
|
667
|
-
const index = connectionPaths.value.findIndex(conn => conn.id === connectionId)
|
|
668
|
-
if (index >= 0) {
|
|
669
|
-
connectionPaths.value.splice(index, 1)
|
|
670
|
-
return true
|
|
671
|
-
}
|
|
672
|
-
return false
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const getConnectionsForBar = (barId: string) => {
|
|
676
|
-
return connectionPaths.value.filter(conn => conn.from.barId === barId || conn.to.barId === barId)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const getHandlesForBar = (barId: string) => {
|
|
680
|
-
return connectionHandles.value.filter(handle => handle.barId === barId)
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const sortByColumn = (colIndex: number) => {
|
|
684
|
-
const column = columns.value[colIndex]
|
|
685
|
-
if (column.sortable === false) return
|
|
686
|
-
|
|
687
|
-
let newDirection: 'asc' | 'desc'
|
|
688
|
-
if (sortState.value.column === colIndex) {
|
|
689
|
-
if (sortState.value.direction === 'asc') {
|
|
690
|
-
newDirection = 'desc'
|
|
691
|
-
} else {
|
|
692
|
-
newDirection = 'asc'
|
|
693
|
-
}
|
|
694
|
-
} else {
|
|
695
|
-
newDirection = 'asc'
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
sortState.value.column = colIndex
|
|
699
|
-
sortState.value.direction = newDirection
|
|
700
|
-
|
|
701
|
-
// Note: The actual sorting is now handled in the filteredRows computed property
|
|
702
|
-
// This ensures that sorting works on filtered data without modifying the original rows
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const setFilter = (colIndex: number, filter: FilterState) => {
|
|
706
|
-
if (!filter.value && !filter.startValue && !filter.endValue) {
|
|
707
|
-
// Remove filter if empty
|
|
708
|
-
delete filterState.value[colIndex]
|
|
709
|
-
} else {
|
|
710
|
-
filterState.value[colIndex] = filter
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
const clearFilter = (colIndex: number) => {
|
|
715
|
-
delete filterState.value[colIndex]
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Row action methods
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Add a new row to the table.
|
|
722
|
-
* @param rowData - Optional partial row data to initialize the new row with
|
|
723
|
-
* @param position - Where to insert the row: 'start', 'end', or a specific index
|
|
724
|
-
* @returns The index of the newly added row
|
|
725
|
-
*/
|
|
726
|
-
const addRow = (rowData?: Partial<TableRow>, position: 'start' | 'end' | number = 'end'): number => {
|
|
727
|
-
// Create a new row with default empty values for each column
|
|
728
|
-
const newRow: TableRow = {}
|
|
729
|
-
for (const column of columns.value) {
|
|
730
|
-
newRow[column.name] = ''
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Merge in any provided row data
|
|
734
|
-
if (rowData) {
|
|
735
|
-
Object.assign(newRow, rowData)
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
let insertIndex: number
|
|
739
|
-
if (position === 'start') {
|
|
740
|
-
insertIndex = 0
|
|
741
|
-
rows.value.unshift(newRow)
|
|
742
|
-
} else if (position === 'end') {
|
|
743
|
-
insertIndex = rows.value.length
|
|
744
|
-
rows.value.push(newRow)
|
|
745
|
-
} else {
|
|
746
|
-
insertIndex = Math.max(0, Math.min(position, rows.value.length))
|
|
747
|
-
rows.value.splice(insertIndex, 0, newRow)
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
return insertIndex
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
/**
|
|
754
|
-
* Delete a row from the table.
|
|
755
|
-
* @param rowIndex - The index of the row to delete
|
|
756
|
-
* @returns The deleted row, or null if the index was invalid
|
|
757
|
-
*/
|
|
758
|
-
const deleteRow = (rowIndex: number): TableRow | null => {
|
|
759
|
-
if (rowIndex < 0 || rowIndex >= rows.value.length) {
|
|
760
|
-
return null
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const [deletedRow] = rows.value.splice(rowIndex, 1)
|
|
764
|
-
|
|
765
|
-
// Clean up row modifications tracking for the deleted row
|
|
766
|
-
delete rowModifications.value[rowIndex]
|
|
767
|
-
delete rowExpandStates.value[rowIndex]
|
|
768
|
-
|
|
769
|
-
// Shift modification/expand state indices for rows after the deleted one
|
|
770
|
-
const newModifications: Record<number, boolean> = {}
|
|
771
|
-
const newExpandStates: Record<number, { childrenOpen?: boolean; expanded?: boolean }> = {}
|
|
772
|
-
|
|
773
|
-
for (const [key, value] of Object.entries(rowModifications.value)) {
|
|
774
|
-
const idx = parseInt(key)
|
|
775
|
-
if (idx > rowIndex) {
|
|
776
|
-
newModifications[idx - 1] = value
|
|
777
|
-
} else {
|
|
778
|
-
newModifications[idx] = value
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
for (const [key, value] of Object.entries(rowExpandStates.value)) {
|
|
783
|
-
const idx = parseInt(key)
|
|
784
|
-
if (idx > rowIndex) {
|
|
785
|
-
newExpandStates[idx - 1] = value
|
|
786
|
-
} else {
|
|
787
|
-
newExpandStates[idx] = value
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
rowModifications.value = newModifications
|
|
792
|
-
rowExpandStates.value = newExpandStates
|
|
793
|
-
|
|
794
|
-
return deletedRow
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Duplicate a row in the table.
|
|
799
|
-
* @param rowIndex - The index of the row to duplicate
|
|
800
|
-
* @returns The index of the new duplicated row, or -1 if the index was invalid
|
|
801
|
-
*/
|
|
802
|
-
const duplicateRow = (rowIndex: number): number => {
|
|
803
|
-
if (rowIndex < 0 || rowIndex >= rows.value.length) {
|
|
804
|
-
return -1
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Deep clone the row data
|
|
808
|
-
const originalRow = rows.value[rowIndex]
|
|
809
|
-
const duplicatedRow: TableRow = JSON.parse(JSON.stringify(originalRow))
|
|
810
|
-
|
|
811
|
-
// Insert the duplicated row after the original
|
|
812
|
-
const newIndex = rowIndex + 1
|
|
813
|
-
rows.value.splice(newIndex, 0, duplicatedRow)
|
|
814
|
-
|
|
815
|
-
return newIndex
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Insert a new row above the specified row.
|
|
820
|
-
* @param rowIndex - The index of the row to insert above
|
|
821
|
-
* @param rowData - Optional partial row data to initialize the new row with
|
|
822
|
-
* @returns The index of the newly inserted row
|
|
823
|
-
*/
|
|
824
|
-
const insertRowAbove = (rowIndex: number, rowData?: Partial<TableRow>): number => {
|
|
825
|
-
const insertIndex = Math.max(0, rowIndex)
|
|
826
|
-
return addRow(rowData, insertIndex)
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Insert a new row below the specified row.
|
|
831
|
-
* @param rowIndex - The index of the row to insert below
|
|
832
|
-
* @param rowData - Optional partial row data to initialize the new row with
|
|
833
|
-
* @returns The index of the newly inserted row
|
|
834
|
-
*/
|
|
835
|
-
const insertRowBelow = (rowIndex: number, rowData?: Partial<TableRow>): number => {
|
|
836
|
-
const insertIndex = Math.min(rowIndex + 1, rows.value.length)
|
|
837
|
-
return addRow(rowData, insertIndex)
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Move a row from one position to another.
|
|
842
|
-
* @param fromIndex - The current index of the row to move
|
|
843
|
-
* @param toIndex - The target index to move the row to
|
|
844
|
-
* @returns true if the move was successful, false otherwise
|
|
845
|
-
*/
|
|
846
|
-
const moveRow = (fromIndex: number, toIndex: number): boolean => {
|
|
847
|
-
// Validate indices
|
|
848
|
-
if (
|
|
849
|
-
fromIndex < 0 ||
|
|
850
|
-
fromIndex >= rows.value.length ||
|
|
851
|
-
toIndex < 0 ||
|
|
852
|
-
toIndex >= rows.value.length ||
|
|
853
|
-
fromIndex === toIndex
|
|
854
|
-
) {
|
|
855
|
-
return false
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Remove the row from its current position
|
|
859
|
-
const [movedRow] = rows.value.splice(fromIndex, 1)
|
|
860
|
-
|
|
861
|
-
// Insert at the new position
|
|
862
|
-
rows.value.splice(toIndex, 0, movedRow)
|
|
863
|
-
|
|
864
|
-
// Update row modification and expand state indices
|
|
865
|
-
const newModifications: Record<number, boolean> = {}
|
|
866
|
-
const newExpandStates: Record<number, { childrenOpen?: boolean; expanded?: boolean }> = {}
|
|
867
|
-
|
|
868
|
-
for (const [key, value] of Object.entries(rowModifications.value)) {
|
|
869
|
-
const idx = parseInt(key)
|
|
870
|
-
let newIdx = idx
|
|
871
|
-
|
|
872
|
-
if (idx === fromIndex) {
|
|
873
|
-
// The moved row
|
|
874
|
-
newIdx = toIndex
|
|
875
|
-
} else if (fromIndex < toIndex) {
|
|
876
|
-
// Moving down: rows between fromIndex and toIndex shift up
|
|
877
|
-
if (idx > fromIndex && idx <= toIndex) {
|
|
878
|
-
newIdx = idx - 1
|
|
879
|
-
}
|
|
880
|
-
} else {
|
|
881
|
-
// Moving up: rows between toIndex and fromIndex shift down
|
|
882
|
-
if (idx >= toIndex && idx < fromIndex) {
|
|
883
|
-
newIdx = idx + 1
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
newModifications[newIdx] = value
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
for (const [key, value] of Object.entries(rowExpandStates.value)) {
|
|
891
|
-
const idx = parseInt(key)
|
|
892
|
-
let newIdx = idx
|
|
893
|
-
|
|
894
|
-
if (idx === fromIndex) {
|
|
895
|
-
newIdx = toIndex
|
|
896
|
-
} else if (fromIndex < toIndex) {
|
|
897
|
-
if (idx > fromIndex && idx <= toIndex) {
|
|
898
|
-
newIdx = idx - 1
|
|
899
|
-
}
|
|
900
|
-
} else {
|
|
901
|
-
if (idx >= toIndex && idx < fromIndex) {
|
|
902
|
-
newIdx = idx + 1
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
newExpandStates[newIdx] = value
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
rowModifications.value = newModifications
|
|
910
|
-
rowExpandStates.value = newExpandStates
|
|
911
|
-
|
|
912
|
-
return true
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
return {
|
|
916
|
-
// state
|
|
917
|
-
columns,
|
|
918
|
-
config,
|
|
919
|
-
connectionHandles,
|
|
920
|
-
connectionPaths,
|
|
921
|
-
display,
|
|
922
|
-
filterState,
|
|
923
|
-
ganttBars,
|
|
924
|
-
modal,
|
|
925
|
-
rows,
|
|
926
|
-
sortState,
|
|
927
|
-
table,
|
|
928
|
-
updates,
|
|
929
|
-
|
|
930
|
-
// getters
|
|
931
|
-
filteredRows,
|
|
932
|
-
hasPinnedColumns,
|
|
933
|
-
isGanttView,
|
|
934
|
-
isTreeView,
|
|
935
|
-
isDependencyGraphEnabled,
|
|
936
|
-
numberedRowWidth,
|
|
937
|
-
zeroColumn,
|
|
938
|
-
|
|
939
|
-
// resolver
|
|
940
|
-
linkResolver,
|
|
941
|
-
|
|
942
|
-
// actions
|
|
943
|
-
addRow,
|
|
944
|
-
clearFilter,
|
|
945
|
-
closeModal,
|
|
946
|
-
createConnection,
|
|
947
|
-
deleteConnection,
|
|
948
|
-
deleteRow,
|
|
949
|
-
duplicateRow,
|
|
950
|
-
getCellData,
|
|
951
|
-
getCellDisplayValue,
|
|
952
|
-
getConnectionsForBar,
|
|
953
|
-
getFormattedValue,
|
|
954
|
-
getHandlesForBar,
|
|
955
|
-
getHeaderCellStyle,
|
|
956
|
-
getRowExpandSymbol,
|
|
957
|
-
insertRowAbove,
|
|
958
|
-
insertRowBelow,
|
|
959
|
-
isRowGantt,
|
|
960
|
-
isRowVisible,
|
|
961
|
-
moveRow,
|
|
962
|
-
registerConnectionHandle,
|
|
963
|
-
registerGanttBar,
|
|
964
|
-
resizeColumn,
|
|
965
|
-
setCellData,
|
|
966
|
-
setCellText,
|
|
967
|
-
setFilter,
|
|
968
|
-
sortByColumn,
|
|
969
|
-
toggleRowExpand,
|
|
970
|
-
unregisterConnectionHandle,
|
|
971
|
-
unregisterGanttBar,
|
|
972
|
-
updateGanttBar,
|
|
973
|
-
updateRows,
|
|
974
|
-
}
|
|
975
|
-
})
|
|
976
|
-
|
|
977
|
-
return createStore()
|
|
978
|
-
}
|