@graphenedata/cli 0.0.12 → 0.0.14

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.
Files changed (46) hide show
  1. package/dist/cli/cli.js +8591 -1214
  2. package/dist/docs/base.md +98 -0
  3. package/dist/docs/cli.md +22 -0
  4. package/dist/docs/graphene.md +10 -10
  5. package/dist/ui/component-utilities/echarts.js +2 -3
  6. package/dist/ui/component-utilities/formatting.js +3 -11
  7. package/dist/ui/component-utilities/getSeriesConfig.js +2 -1
  8. package/dist/ui/components/Area.svelte +188 -151
  9. package/dist/ui/components/AreaChart.svelte +43 -79
  10. package/dist/ui/components/Bar.svelte +273 -255
  11. package/dist/ui/components/BarChart.svelte +58 -112
  12. package/dist/ui/components/BigValue.svelte +13 -7
  13. package/dist/ui/components/Chart.svelte +280 -317
  14. package/dist/ui/components/Column.svelte +102 -113
  15. package/dist/ui/components/DateRange.svelte +37 -27
  16. package/dist/ui/components/Dropdown.svelte +77 -57
  17. package/dist/ui/components/DropdownOption.svelte +10 -7
  18. package/dist/ui/components/ECharts.svelte +23 -16
  19. package/dist/ui/components/ErrorChart.svelte +85 -21
  20. package/dist/ui/components/GrapheneQuery.svelte +7 -3
  21. package/dist/ui/components/InlineDelta.svelte +53 -34
  22. package/dist/ui/components/Line.svelte +192 -178
  23. package/dist/ui/components/LineChart.svelte +53 -96
  24. package/dist/ui/components/PieChart.svelte +26 -15
  25. package/dist/ui/components/QueryLoad.svelte +15 -10
  26. package/dist/ui/components/SortIcon.svelte +5 -1
  27. package/dist/ui/components/Table.svelte +15 -9
  28. package/dist/ui/components/TableCell.svelte +30 -17
  29. package/dist/ui/components/TableGroupRow.svelte +26 -19
  30. package/dist/ui/components/TableGroupToggle.svelte +9 -6
  31. package/dist/ui/components/TableHeader.svelte +37 -27
  32. package/dist/ui/components/TableRow.svelte +30 -20
  33. package/dist/ui/components/TableSubtotalRow.svelte +16 -9
  34. package/dist/ui/components/TableTotalRow.svelte +18 -11
  35. package/dist/ui/components/TextInput.svelte +23 -20
  36. package/dist/ui/components/_Table.svelte +303 -260
  37. package/dist/ui/internal/LocalApp.svelte +40 -0
  38. package/dist/ui/internal/NavSidebar.svelte +27 -30
  39. package/dist/ui/internal/PageError.svelte +23 -0
  40. package/dist/ui/internal/checkSocket.ts +48 -0
  41. package/dist/ui/internal/queryEngine.ts +9 -2
  42. package/dist/ui/internal/telemetry.ts +1 -0
  43. package/dist/ui/web.js +5 -55
  44. package/package.json +9 -10
  45. package/cli.ts +0 -156
  46. package/dist/ui/internal/NavSidebarHMR.svelte +0 -8
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
+ import {setContext, untrack, type Snippet} from 'svelte'
2
3
  import {writable} from 'svelte/store'
3
- import {setContext} from 'svelte'
4
4
  import {propKey, strictBuild} from '../component-utilities/chartContext.js'
5
5
  import getColumnSummary from '../component-utilities/getColumnSummary.js'
6
6
  import {convertColumnToDate} from '../component-utilities/dateParsing.js'
@@ -15,237 +15,189 @@
15
15
  import {getFinalColumnOrder} from '../component-utilities/tableUtils'
16
16
  import {getThemeStores} from '../component-utilities/themeStores'
17
17
  import {toBoolean} from '../component-utilities/convert'
18
- import {logError} from '../internal/telemetry.js'
18
+ import {logError} from '../internal/telemetry.js'
19
+
20
+ interface Props {
21
+ data?: any[], rows?: number | string, title?: string, subtitle?: string, rowNumbers?: boolean | string
22
+ sort?: string, sortable?: boolean | string, groupBy?: string, groupsOpen?: boolean | string
23
+ groupType?: 'accordion' | 'section', accordionRowColor?: string, groupNamePosition?: 'top' | 'middle' | 'bottom'
24
+ subtotals?: boolean | string, subtotalRowColor?: string, subtotalFontColor?: string
25
+ rowShading?: boolean | string, rowLines?: boolean | string, wrapTitles?: boolean | string
26
+ headerColor?: string, headerFontColor?: string, formatColumnTitles?: boolean | string
27
+ backgroundColor?: string, compact?: boolean | string, link?: string, showLinkCol?: boolean | string
28
+ totalRow?: boolean | string, totalRowColor?: string, totalFontColor?: string, emptyMessage?: string
29
+ isFullPage?: boolean | string, children?: Snippet
30
+ }
19
31
 
20
32
  const {resolveColor} = getThemeStores()
21
33
 
22
- export let data: any[] = []
23
- export let rows: number | string = 10
24
- export let title: string | undefined = undefined
25
- export let subtitle: string | undefined = undefined
26
- export let rowNumbers: boolean | string | undefined = false
27
- export let sort: string | undefined = undefined
28
- export let sortable: boolean | string | undefined = true
29
- export let groupBy: string | undefined = undefined
30
- export let groupsOpen: boolean | string | undefined = true
31
- export let groupType: 'accordion' | 'section' = 'accordion'
32
- export let accordionRowColor: string | undefined = undefined
33
- export let groupNamePosition: 'top' | 'middle' | 'bottom' = 'middle'
34
- export let subtotals: boolean | string | undefined = false
35
- export let subtotalRowColor: string | undefined = undefined
36
- export let subtotalFontColor: string | undefined = undefined
37
- export let rowShading: boolean | string | undefined = false
38
- export let rowLines: boolean | string | undefined = true
39
- export let wrapTitles: boolean | string | undefined = false
40
- export let headerColor: string | undefined = undefined
41
- export let headerFontColor: string | undefined = undefined
42
- export let formatColumnTitles: boolean | string | undefined = true
43
- export let backgroundColor: string | undefined = undefined
44
- export let compact: boolean | string | undefined = undefined
45
- export let link: string | undefined = undefined
46
- export let showLinkCol: boolean | string | undefined = false
47
- export let totalRow: boolean | string | undefined = false
48
- export let totalRowColor: string | undefined = undefined
49
- export let totalFontColor: string | undefined = undefined
50
- export let emptyMessage: string | undefined = undefined
51
- export let isFullPage: boolean | string | undefined = undefined
52
-
53
- rows = Number.parseInt(String(rows), 10)
54
- if (!Number.isFinite(rows) || rows <= 0) rows = 10
55
-
56
- rowNumbers = toBoolean(rowNumbers) ?? false
57
- groupsOpen = toBoolean(groupsOpen) ?? true
58
- subtotals = toBoolean(subtotals) ?? false
59
- rowShading = toBoolean(rowShading) ?? false
60
- rowLines = toBoolean(rowLines) ?? true
61
- wrapTitles = toBoolean(wrapTitles) ?? false
62
- formatColumnTitles = toBoolean(formatColumnTitles) ?? true
63
- compact = toBoolean(compact)
64
- showLinkCol = toBoolean(showLinkCol) ?? false
65
- totalRow = toBoolean(totalRow) ?? false
66
- sortable = toBoolean(sortable) ?? true
67
- isFullPage = toBoolean(isFullPage) ?? false
68
-
69
- if (groupType === 'section') rowNumbers = false
70
-
71
- const props = writable<{data: any[]; columns: any[]; priorityColumns:(string | undefined)[]}>({data, columns: [], priorityColumns: [groupBy]})
72
- setContext(propKey, props)
73
-
74
- $: props.update((state) => ({...state, data, priorityColumns: [groupBy]}))
75
-
76
- $: accordionRowColorStore = resolveColor(accordionRowColor)
77
- $: subtotalRowColorStore = resolveColor(subtotalRowColor)
78
- $: subtotalFontColorStore = resolveColor(subtotalFontColor)
79
- $: totalRowColorStore = resolveColor(totalRowColor)
80
- $: totalFontColorStore = resolveColor(totalFontColor)
81
- $: headerColorStore = resolveColor(headerColor)
82
- $: headerFontColorStore = resolveColor(headerFontColor)
83
- $: backgroundColorStore = resolveColor(backgroundColor)
84
-
85
- let error: string | undefined = undefined
86
- let columnSummary: any[] = []
87
- let priorityColumns: (string | undefined)[] = [groupBy]
88
- let finalColumnOrder: string[] = []
89
- let orderedColumns: any[] = []
90
- let dataForDisplay: any[] = []
91
- let displayedRows: any[] = []
92
-
93
- $: priorityColumns = [groupBy]
94
- $: props.update((state) => ({...state, priorityColumns}))
95
- $: finalColumnOrder = getFinalColumnOrder(($props.columns ?? []).map((column: any) => column.id), priorityColumns)
96
- $: orderedColumns = [...($props.columns ?? [])].sort(
34
+ let {
35
+ data = [], rows = 10, title = undefined, subtitle = undefined, rowNumbers = false, sort = undefined,
36
+ sortable = true, groupBy = undefined, groupsOpen = true, groupType = 'accordion',
37
+ accordionRowColor = undefined, groupNamePosition = 'middle', subtotals = false,
38
+ subtotalRowColor = undefined, subtotalFontColor = undefined, rowShading = false, rowLines = true,
39
+ wrapTitles = false, headerColor = undefined, headerFontColor = undefined, formatColumnTitles = true,
40
+ backgroundColor = undefined, compact = undefined, link = undefined, showLinkCol = false,
41
+ totalRow = false, totalRowColor = undefined, totalFontColor = undefined, emptyMessage = undefined,
42
+ isFullPage = undefined, children,
43
+ }: Props = $props()
44
+
45
+ let rowsNum = $derived.by(() => {
46
+ let parsed = Number.parseInt(String(rows), 10)
47
+ return (!Number.isFinite(parsed) || parsed <= 0) ? 10 : parsed
48
+ })
49
+
50
+ let rowNumbersBool = $derived(toBoolean(rowNumbers) ?? false)
51
+ let groupsOpenBool = $derived(toBoolean(groupsOpen) ?? true)
52
+ let subtotalsBool = $derived(toBoolean(subtotals) ?? false)
53
+ let rowShadingBool = $derived(toBoolean(rowShading) ?? false)
54
+ let rowLinesBool = $derived(toBoolean(rowLines) ?? true)
55
+ let wrapTitlesBool = $derived(toBoolean(wrapTitles) ?? false)
56
+ let formatColumnTitlesBool = $derived(toBoolean(formatColumnTitles) ?? true)
57
+ let compactBool = $derived(toBoolean(compact))
58
+ let showLinkColBool = $derived(toBoolean(showLinkCol) ?? false)
59
+ let totalRowBool = $derived(toBoolean(totalRow) ?? false)
60
+ let sortableBool = $derived(toBoolean(sortable) ?? true)
61
+ let isFullPageBool = $derived(toBoolean(isFullPage) ?? false)
62
+
63
+ let effectiveRowNumbers = $derived(groupType === 'section' ? false : rowNumbersBool)
64
+
65
+ // Initialize store without data - it will be populated by the effect below
66
+ const tablePropsStore = writable<{data: any[]; columns: any[]; priorityColumns:(string | undefined)[]}>({data: [], columns: [], priorityColumns: []})
67
+ setContext(propKey, tablePropsStore)
68
+
69
+ // Update store when data or groupBy changes
70
+ $effect(() => {
71
+ // Track data and groupBy
72
+ let currentData = data
73
+ let currentGroupBy = groupBy
74
+ untrack(() => {
75
+ tablePropsStore.update((state) => ({...state, data: currentData, priorityColumns: [currentGroupBy]}))
76
+ })
77
+ })
78
+
79
+ let accordionRowColorStore = $derived(resolveColor(accordionRowColor))
80
+ let subtotalRowColorStore = $derived(resolveColor(subtotalRowColor))
81
+ let subtotalFontColorStore = $derived(resolveColor(subtotalFontColor))
82
+ let totalRowColorStore = $derived(resolveColor(totalRowColor))
83
+ let totalFontColorStore = $derived(resolveColor(totalFontColor))
84
+ let headerColorStore = $derived(resolveColor(headerColor))
85
+ let headerFontColorStore = $derived(resolveColor(headerFontColor))
86
+ let backgroundColorStore = $derived(resolveColor(backgroundColor))
87
+
88
+ let priorityColumns = $derived<(string | undefined)[]>([groupBy])
89
+
90
+ $effect(() => {
91
+ void priorityColumns
92
+ untrack(() => {
93
+ tablePropsStore.update((state) => ({...state, priorityColumns}))
94
+ })
95
+ })
96
+
97
+ // Use $derived to reactively read from the store
98
+ let tablePropsColumns = $derived($tablePropsStore.columns ?? [])
99
+
100
+ let finalColumnOrder = $derived(getFinalColumnOrder(tablePropsColumns.map((column: any) => column.id), priorityColumns))
101
+ let orderedColumns = $derived([...tablePropsColumns].sort(
97
102
  (a, b) => finalColumnOrder.indexOf(a.id) - finalColumnOrder.indexOf(b.id),
98
- )
103
+ ))
99
104
 
100
- let sortObj: {col: string | null; ascending: boolean | null} = {col: null, ascending: null}
101
- let sortBy: string | undefined
102
- let sortAsc: boolean | undefined
105
+ let sortObj: {col: string | null; ascending: boolean | null} = $state({col: null, ascending: null})
103
106
 
104
- $: if (sort) {
107
+ // Parse initial sort on mount
108
+ let initialSort = $derived.by(() => {
109
+ if (!sort) return {sortBy: undefined, sortAsc: true}
105
110
  let [column, direction] = sort.split(/\s+/)
106
- sortBy = column
107
- if (direction) {
108
- sortAsc = direction.toLowerCase() !== 'desc'
109
- } else {
110
- sortAsc = true
111
- }
112
- sortObj = sortBy ? {col: sortBy, ascending: sortAsc ?? true} : {col: null, ascending: null}
113
- }
114
-
115
- $: props.update((state) => ({...state, data}))
116
-
117
- $: try {
118
- error = undefined
119
- checkInputs(Array.isArray(data) ? data : [])
120
- columnSummary = getColumnSummary(data ?? [], 'array')
121
-
122
- if (sortBy) {
123
- let columnNames = columnSummary.map((col) => col.id)
124
- if (!columnNames.includes(sortBy)) {
125
- throw new Error(`${sortBy} is not a column in the dataset. sort should contain one column name and optionally a direction (asc or desc).`)
126
- }
111
+ return {
112
+ sortBy: column,
113
+ sortAsc: direction ? direction.toLowerCase() !== 'desc' : true,
127
114
  }
115
+ })
128
116
 
129
- let dateCols = columnSummary
130
- .filter((col) => col.type === 'date' && !(data?.[0]?.[col.id] instanceof Date))
131
- .map((col) => col.id)
132
-
133
- for (let columnId of dateCols) {
134
- data = convertColumnToDate(data, columnId)
117
+ // Initialize sortObj when sort prop changes
118
+ $effect(() => {
119
+ if (sort && initialSort.sortBy) {
120
+ sortObj = {col: initialSort.sortBy, ascending: initialSort.sortAsc}
135
121
  }
136
-
137
- if (link && !showLinkCol) {
138
- let linkIndex = columnSummary.findIndex((col) => col.id === link)
139
- if (linkIndex !== -1) {
140
- columnSummary = [...columnSummary.slice(0, linkIndex), ...columnSummary.slice(linkIndex + 1)]
141
- }
142
- }
143
- } catch (thrown) {
144
- let message = thrown instanceof Error ? thrown.message : 'Unable to prepare dataset'
145
- logError(thrown, {id: 'DataTable'})
146
- error = message
147
- if (strictBuild) throw thrown
148
- }
149
-
150
- let paginated = false
151
- let currentPage = 1
152
- let pageCount = 1
153
- let displayedPageLength = 0
154
-
155
- const goToPage = (page: number) => {
156
- if (!paginated) return
157
- let next = Math.min(Math.max(page, 1), pageCount)
158
- if (Number.isFinite(next)) currentPage = next
159
- }
160
-
161
- let groupedData: Record<string, any[]> = {}
162
- let groupToggleStates: Record<string, boolean> = {}
122
+ })
163
123
 
164
124
  const coerceId = (value: any) => {
165
125
  if (value === undefined || value === null || value === '') return undefined
166
126
  return String(value)
167
127
  }
168
128
 
169
- let dataTestId: string | undefined = undefined
129
+ // Process data - combine all data processing into one $derived.by block to avoid loops
130
+ let processedState = $derived.by(() => {
131
+ let resultError: string | undefined = undefined
132
+ let resultColumnSummary: any[] = []
133
+ let resultProcessedData: any[] = []
134
+ let resultDataTestId: string | undefined = undefined
135
+ let resultNormalizedData: any[] = []
136
+
137
+ try {
138
+ // First normalize data - extract rows array from object if needed
139
+ let inputData: any[]
140
+ if (!Array.isArray(data)) {
141
+ let raw = data as any
142
+ resultDataTestId = coerceId(raw?.id)
143
+ let candidate = raw?.rows
144
+ inputData = Array.isArray(candidate) ? candidate : []
145
+ } else {
146
+ resultDataTestId = coerceId((data as any)?.id)
147
+ inputData = data
148
+ }
170
149
 
171
- $: {
172
- if (!Array.isArray(data)) {
173
- let raw = data as any
174
- dataTestId = coerceId(raw?.id)
175
- let candidate = raw?.rows
176
- data = Array.isArray(candidate) ? candidate : []
177
- } else {
178
- dataTestId = coerceId((data as any)?.id)
179
- }
180
- }
150
+ checkInputs(inputData)
151
+ resultColumnSummary = getColumnSummary(inputData, 'array')
181
152
 
182
- $: paginated = !groupBy && rows > 0 && (dataForDisplay?.length ?? 0) > rows
183
- $: pageCount = paginated ? Math.ceil((dataForDisplay?.length ?? 0) / rows) : 1
184
- $: currentPage = Math.min(Math.max(currentPage, 1), pageCount)
185
- $: displayedPageLength = paginated
186
- ? Math.min(rows, (dataForDisplay?.length ?? 0) - rows * (currentPage - 1))
187
- : dataForDisplay?.length ?? 0
153
+ if (initialSort.sortBy) {
154
+ let columnNames = resultColumnSummary.map((col) => col.id)
155
+ if (!columnNames.includes(initialSort.sortBy)) {
156
+ throw new Error(`${initialSort.sortBy} is not a column in the dataset. sort should contain one column name and optionally a direction (asc or desc).`)
157
+ }
158
+ }
188
159
 
189
- $: if (groupBy && data) {
190
- groupedData = data.reduce<Record<string, any[]>>((acc, row) => {
191
- let groupName = row[groupBy]
192
- let key = groupName ?? '(blank)'
193
- if (!acc[key]) acc[key] = []
194
- acc[key].push(row)
195
- return acc
196
- }, {})
160
+ let dateCols = resultColumnSummary
161
+ .filter((col) => col.type === 'date' && !(inputData?.[0]?.[col.id] instanceof Date))
162
+ .map((col) => col.id)
197
163
 
198
- for (let groupName of Object.keys(groupedData)) {
199
- if (!(groupName in groupToggleStates)) groupToggleStates[groupName] = groupsOpen ?? true
164
+ let tempData = inputData
165
+ for (let columnId of dateCols) {
166
+ tempData = convertColumnToDate(tempData, columnId)
167
+ }
168
+ resultProcessedData = tempData
169
+ resultNormalizedData = tempData
170
+
171
+ if (link && !showLinkColBool) {
172
+ let linkIndex = resultColumnSummary.findIndex((col) => col.id === link)
173
+ if (linkIndex !== -1) {
174
+ resultColumnSummary = [...resultColumnSummary.slice(0, linkIndex), ...resultColumnSummary.slice(linkIndex + 1)]
175
+ }
176
+ }
177
+ } catch (thrown) {
178
+ let message = thrown instanceof Error ? thrown.message : 'Unable to prepare dataset'
179
+ logError(thrown, {id: 'DataTable'})
180
+ resultError = message
181
+ if (strictBuild) throw thrown
200
182
  }
201
- } else {
202
- groupedData = {}
203
- }
204
183
 
205
- const handleToggle = (event: CustomEvent<{groupName: string}>) => {
206
- let {groupName} = event.detail
207
- groupToggleStates = {...groupToggleStates, [groupName]: !groupToggleStates[groupName]}
208
- }
209
-
210
- $: {
211
- if (groupBy) {
212
- displayedRows = data ?? []
213
- } else if (paginated) {
214
- let start = rows * (currentPage - 1)
215
- let end = start + rows
216
- displayedRows = dataForDisplay?.slice(start, end) ?? []
217
- } else {
218
- displayedRows = dataForDisplay ?? []
184
+ return {
185
+ error: resultError,
186
+ columnSummary: resultColumnSummary,
187
+ processedData: resultProcessedData,
188
+ dataTestId: resultDataTestId,
189
+ normalizedData: resultNormalizedData,
219
190
  }
220
- }
191
+ })
221
192
 
222
- const applySort = (state: {col: string | null; ascending: boolean | null}) => {
223
- if (!state.col) return
224
- let ascending = state.ascending ?? true
225
- if (groupBy) {
226
- groupedData = Object.fromEntries(
227
- Object.entries(groupedData).map(([groupName, rows]) => [
228
- groupName,
229
- [...rows].sort((a, b) => compareValues(a[state.col as string], b[state.col as string], ascending)),
230
- ]),
231
- )
232
- } else {
233
- let source = Array.isArray(data) ? data : []
234
- dataForDisplay = [...source].sort((a, b) => compareValues(a[state.col as string], b[state.col as string], ascending))
235
- }
236
- }
193
+ // Extract processed state
194
+ let columnSummary = $derived(processedState.columnSummary)
195
+ let normalizedData = $derived(processedState.normalizedData)
196
+ let dataTestId = $derived(processedState.dataTestId)
237
197
 
238
- const sortClick = (column: string) => () => {
239
- if (!sortable) return
240
- if (!column) return
241
- if (sortObj.col === column) {
242
- sortObj = {col: column, ascending: !sortObj.ascending}
243
- } else {
244
- sortObj = {col: column, ascending: true}
245
- }
246
- applySort(sortObj)
247
- }
198
+ let error = $derived(processedState.error)
248
199
 
200
+ // Sorting helpers
249
201
  const normalizeForSort = (value: unknown) => {
250
202
  if (value instanceof Date) return value.getTime()
251
203
  if (typeof value === 'number') return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY
@@ -273,51 +225,142 @@
273
225
  return 0
274
226
  }
275
227
 
276
- $: {
277
- let source = Array.isArray(data) ? data : []
228
+ // Compute dataForDisplay as derived to avoid effect loops
229
+ let dataForDisplay = $derived.by(() => {
230
+ let source = Array.isArray(normalizedData) ? normalizedData : []
278
231
  if (groupBy) {
279
- dataForDisplay = source
232
+ return source
280
233
  } else if (sortObj.col) {
281
234
  let ascending = sortObj.ascending ?? true
282
- dataForDisplay = [...source].sort((a, b) => compareValues(a[sortObj.col as string], b[sortObj.col as string], ascending))
235
+ return [...source].sort((a, b) => compareValues(a[sortObj.col as string], b[sortObj.col as string], ascending))
283
236
  } else {
284
- dataForDisplay = source
237
+ return source
238
+ }
239
+ })
240
+
241
+ // Pagination state and derived values
242
+ let currentPage = $state(1)
243
+ let paginated = $derived(!groupBy && rowsNum > 0 && (dataForDisplay?.length ?? 0) > rowsNum)
244
+ let pageCount = $derived(paginated ? Math.ceil((dataForDisplay?.length ?? 0) / rowsNum) : 1)
245
+
246
+ // Clamp currentPage when pageCount changes - but only update if needed
247
+ $effect(() => {
248
+ let clamped = Math.min(Math.max(currentPage, 1), pageCount)
249
+ if (clamped !== currentPage) {
250
+ untrack(() => {
251
+ currentPage = clamped
252
+ })
253
+ }
254
+ })
255
+
256
+ let displayedPageLength = $derived(paginated
257
+ ? Math.min(rowsNum, (dataForDisplay?.length ?? 0) - rowsNum * (currentPage - 1))
258
+ : dataForDisplay?.length ?? 0)
259
+
260
+ const goToPage = (page: number) => {
261
+ if (!paginated) return
262
+ let next = Math.min(Math.max(page, 1), pageCount)
263
+ if (Number.isFinite(next)) currentPage = next
264
+ }
265
+
266
+ let groupToggleStates: Record<string, boolean> = $state({})
267
+
268
+ // Compute grouped data as derived
269
+ let groupedData = $derived.by(() => {
270
+ if (!groupBy || !normalizedData) return {}
271
+ return normalizedData.reduce<Record<string, any[]>>((acc, row) => {
272
+ let groupName = row[groupBy]
273
+ let key = groupName ?? '(blank)'
274
+ if (!acc[key]) acc[key] = []
275
+ acc[key].push(row)
276
+ return acc
277
+ }, {})
278
+ })
279
+
280
+ // Initialize toggle states for new groups
281
+ $effect(() => {
282
+ if (groupBy && groupedData) {
283
+ for (let name of Object.keys(groupedData)) {
284
+ if (!(name in groupToggleStates)) {
285
+ untrack(() => {
286
+ groupToggleStates = {...groupToggleStates, [name]: groupsOpenBool}
287
+ })
288
+ }
289
+ }
285
290
  }
291
+ })
292
+
293
+ const handleToggle = (event: CustomEvent<{groupName: string}>) => {
294
+ let {groupName} = event.detail
295
+ groupToggleStates = {...groupToggleStates, [groupName]: !groupToggleStates[groupName]}
286
296
  }
287
297
 
288
- $: if (data && sortObj.col) applySort(sortObj)
298
+ // Compute displayedRows as derived
299
+ let displayedRows = $derived.by(() => {
300
+ if (groupBy) {
301
+ return normalizedData ?? []
302
+ } else if (paginated) {
303
+ let start = rowsNum * (currentPage - 1)
304
+ let end = start + rowsNum
305
+ return dataForDisplay?.slice(start, end) ?? []
306
+ } else {
307
+ return dataForDisplay ?? []
308
+ }
309
+ })
310
+
311
+ // Sort grouped data when sortObj changes
312
+ let sortedGroupedData = $derived.by(() => {
313
+ if (!groupBy || !sortObj.col) return groupedData
314
+ let ascending = sortObj.ascending ?? true
315
+ return Object.fromEntries(
316
+ Object.entries(groupedData).map(([name, rows]) => [
317
+ name,
318
+ [...rows].sort((a, b) => compareValues(a[sortObj.col as string], b[sortObj.col as string], ascending)),
319
+ ]),
320
+ )
321
+ })
289
322
 
290
- $: sortedGroupNames = groupBy
291
- ? Object.keys(groupedData).sort((a, b) => a.localeCompare(b))
292
- : []
323
+ const sortClick = (column: string) => () => {
324
+ if (!sortableBool) return
325
+ if (!column) return
326
+ if (sortObj.col === column) {
327
+ sortObj = {col: column, ascending: !sortObj.ascending}
328
+ } else {
329
+ sortObj = {col: column, ascending: true}
330
+ }
331
+ }
293
332
 
294
- let groupOffsets: Record<string, number> = {}
295
- $: if (groupBy) {
333
+ let sortedGroupNames = $derived(groupBy
334
+ ? Object.keys(sortedGroupedData).sort((a, b) => a.localeCompare(b))
335
+ : [])
336
+
337
+ let groupOffsets = $derived.by(() => {
338
+ if (!groupBy) return {}
296
339
  let running = 0
297
- groupOffsets = {}
340
+ let offsets: Record<string, number> = {}
298
341
  for (let name of sortedGroupNames) {
299
- groupOffsets[name] = running
300
- running += groupedData[name]?.length ?? 0
342
+ offsets[name] = running
343
+ running += sortedGroupedData[name]?.length ?? 0
301
344
  }
302
- } else {
303
- groupOffsets = {}
304
- }
345
+ return offsets
346
+ })
305
347
 
306
- let totalRows = 0
307
- $: totalRows = dataForDisplay?.length ?? 0
308
- $: tableData = dataForDisplay ?? []
348
+ let totalRows = $derived(dataForDisplay?.length ?? 0)
349
+ let tableData = $derived(dataForDisplay ?? [])
309
350
  </script>
310
351
 
311
352
  {#if !error}
312
- <slot>
353
+ {#if children}
354
+ {@render children()}
355
+ {:else}
313
356
  {#each columnSummary as column (column.id)}
314
357
  <Column id={column.id} />
315
358
  {/each}
316
- </slot>
359
+ {/if}
317
360
 
318
361
  <div
319
362
  class={`table-container ${paginated ? 'table-container--has-pagination' : ''}`}
320
- data-testid={isFullPage ? undefined : `DataTable-${dataTestId ?? 'no-id'}`}
363
+ data-testid={isFullPageBool ? undefined : `DataTable-${dataTestId ?? 'no-id'}`}
321
364
  >
322
365
  {#if title || subtitle}
323
366
  <div class="table-title">
@@ -329,17 +372,17 @@
329
372
  <div class="scrollbox pretty-scrollbar" style:background-color={$backgroundColorStore}>
330
373
  <table>
331
374
  <TableHeader
332
- {rowNumbers}
375
+ rowNumbers={effectiveRowNumbers}
333
376
  headerColor={$headerColorStore}
334
377
  headerFontColor={$headerFontColorStore}
335
378
  {orderedColumns}
336
379
  {columnSummary}
337
- {sortable}
380
+ sortable={sortableBool}
338
381
  {sortClick}
339
- {formatColumnTitles}
382
+ formatColumnTitles={formatColumnTitlesBool}
340
383
  {sortObj}
341
- {wrapTitles}
342
- {compact}
384
+ wrapTitles={wrapTitlesBool}
385
+ compact={compactBool}
343
386
  link={link}
344
387
  />
345
388
 
@@ -348,25 +391,25 @@
348
391
  {#if groupType !== 'section'}
349
392
  <TableGroupRow
350
393
  {groupName}
351
- currentGroupData={groupedData[groupName]}
394
+ currentGroupData={sortedGroupedData[groupName]}
352
395
  toggled={groupToggleStates[groupName]}
353
396
  {columnSummary}
354
- {rowNumbers}
397
+ rowNumbers={effectiveRowNumbers}
355
398
  rowColor={$accordionRowColorStore}
356
- {subtotals}
357
- on:toggle={handleToggle}
399
+ subtotals={subtotalsBool}
400
+ ontoggle={handleToggle}
358
401
  {orderedColumns}
359
- {compact}
402
+ compact={compactBool}
360
403
  />
361
404
  {/if}
362
405
  {#if groupType === 'section' || groupToggleStates[groupName]}
363
406
  <TableRow
364
- displayedData={groupedData[groupName]}
365
- {rowShading}
407
+ displayedData={sortedGroupedData[groupName]}
408
+ rowShading={rowShadingBool}
366
409
  {link}
367
- {rowNumbers}
368
- {rowLines}
369
- {compact}
410
+ rowNumbers={effectiveRowNumbers}
411
+ rowLines={rowLinesBool}
412
+ compact={compactBool}
370
413
  {columnSummary}
371
414
  grouped={true}
372
415
  {groupType}
@@ -374,19 +417,19 @@
374
417
  groupNamePosition={groupNamePosition}
375
418
  orderedColumns={orderedColumns}
376
419
  index={groupOffsets[groupName] ?? 0}
377
- rowSpan={groupedData[groupName].length}
420
+ rowSpan={sortedGroupedData[groupName].length}
378
421
  />
379
- {#if subtotals}
422
+ {#if subtotalsBool}
380
423
  <TableSubtotalRow
381
424
  {groupName}
382
- currentGroupData={groupedData[groupName]}
425
+ currentGroupData={sortedGroupedData[groupName]}
383
426
  {columnSummary}
384
427
  rowColor={$subtotalRowColorStore}
385
428
  fontColor={$subtotalFontColorStore}
386
429
  groupBy={groupBy}
387
430
  groupType={groupType}
388
431
  {orderedColumns}
389
- {compact}
432
+ compact={compactBool}
390
433
  />
391
434
  {/if}
392
435
  {/if}
@@ -394,31 +437,31 @@
394
437
  {:else}
395
438
  <TableRow
396
439
  displayedData={displayedRows}
397
- {rowShading}
440
+ rowShading={rowShadingBool}
398
441
  {link}
399
- {rowNumbers}
400
- {rowLines}
401
- {compact}
442
+ rowNumbers={effectiveRowNumbers}
443
+ rowLines={rowLinesBool}
444
+ compact={compactBool}
402
445
  {columnSummary}
403
446
  grouped={false}
404
447
  {groupType}
405
448
  groupColumn={groupBy}
406
449
  groupNamePosition={groupNamePosition}
407
450
  orderedColumns={orderedColumns}
408
- index={rows * (currentPage - 1)}
451
+ index={rowsNum * (currentPage - 1)}
409
452
  />
410
453
  {/if}
411
454
 
412
- {#if totalRow && !groupBy}
455
+ {#if totalRowBool && !groupBy}
413
456
  <TableTotalRow
414
457
  data={tableData}
415
- {rowNumbers}
458
+ rowNumbers={effectiveRowNumbers}
416
459
  {columnSummary}
417
460
  rowColor={$totalRowColorStore}
418
461
  fontColor={$totalFontColorStore}
419
462
  groupType={groupType}
420
463
  {orderedColumns}
421
- {compact}
464
+ compact={compactBool}
422
465
  />
423
466
  {/if}
424
467
  </table>
@@ -426,13 +469,13 @@
426
469
 
427
470
  {#if paginated && pageCount > 1}
428
471
  <div class="pagination">
429
- <button class="pagination__button" disabled={currentPage === 1} on:click={() => goToPage(1)}>First</button>
430
- <button class="pagination__button" disabled={currentPage === 1} on:click={() => goToPage(currentPage - 1)}>Prev</button>
472
+ <button class="pagination__button" disabled={currentPage === 1} onclick={() => goToPage(1)}>First</button>
473
+ <button class="pagination__button" disabled={currentPage === 1} onclick={() => goToPage(currentPage - 1)}>Prev</button>
431
474
  <div class="pagination__status">
432
475
  Page {currentPage.toLocaleString()} of {pageCount.toLocaleString()}
433
476
  </div>
434
- <button class="pagination__button" disabled={currentPage === pageCount} on:click={() => goToPage(currentPage + 1)}>Next</button>
435
- <button class="pagination__button" disabled={currentPage === pageCount} on:click={() => goToPage(pageCount)}>Last</button>
477
+ <button class="pagination__button" disabled={currentPage === pageCount} onclick={() => goToPage(currentPage + 1)}>Next</button>
478
+ <button class="pagination__button" disabled={currentPage === pageCount} onclick={() => goToPage(pageCount)}>Last</button>
436
479
  <div class="pagination__meta">{displayedPageLength.toLocaleString()} of {totalRows.toLocaleString()} rows</div>
437
480
  </div>
438
481
  {/if}