@shotleybuilder/svelte-gridlite-kit 0.1.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/README.md +260 -0
- package/dist/GridLite.svelte +1361 -0
- package/dist/GridLite.svelte.d.ts +42 -0
- package/dist/components/CellContextMenu.svelte +209 -0
- package/dist/components/CellContextMenu.svelte.d.ts +28 -0
- package/dist/components/ColumnMenu.svelte +234 -0
- package/dist/components/ColumnMenu.svelte.d.ts +29 -0
- package/dist/components/ColumnPicker.svelte +403 -0
- package/dist/components/ColumnPicker.svelte.d.ts +29 -0
- package/dist/components/FilterBar.svelte +390 -0
- package/dist/components/FilterBar.svelte.d.ts +38 -0
- package/dist/components/FilterCondition.svelte +643 -0
- package/dist/components/FilterCondition.svelte.d.ts +35 -0
- package/dist/components/GroupBar.svelte +463 -0
- package/dist/components/GroupBar.svelte.d.ts +33 -0
- package/dist/components/RowDetailModal.svelte +213 -0
- package/dist/components/RowDetailModal.svelte.d.ts +25 -0
- package/dist/components/SortBar.svelte +232 -0
- package/dist/components/SortBar.svelte.d.ts +30 -0
- package/dist/components/SortCondition.svelte +129 -0
- package/dist/components/SortCondition.svelte.d.ts +30 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +29 -0
- package/dist/query/builder.d.ts +160 -0
- package/dist/query/builder.js +432 -0
- package/dist/query/live.d.ts +50 -0
- package/dist/query/live.js +118 -0
- package/dist/query/schema.d.ts +30 -0
- package/dist/query/schema.js +75 -0
- package/dist/state/migrations.d.ts +29 -0
- package/dist/state/migrations.js +113 -0
- package/dist/state/views.d.ts +54 -0
- package/dist/state/views.js +130 -0
- package/dist/styles/gridlite.css +966 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.js +2 -0
- package/dist/utils/filters.d.ts +14 -0
- package/dist/utils/filters.js +49 -0
- package/dist/utils/formatters.d.ts +16 -0
- package/dist/utils/formatters.js +39 -0
- package/dist/utils/fuzzy.d.ts +47 -0
- package/dist/utils/fuzzy.js +142 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1361 @@
|
|
|
1
|
+
<script>import { onMount, onDestroy } from "svelte";
|
|
2
|
+
import { introspectTable, getColumnNames } from "./query/schema.js";
|
|
3
|
+
import {
|
|
4
|
+
buildQuery,
|
|
5
|
+
buildCountQuery,
|
|
6
|
+
buildGroupSummaryQuery,
|
|
7
|
+
buildGroupCountQuery,
|
|
8
|
+
buildGroupDetailQuery
|
|
9
|
+
} from "./query/builder.js";
|
|
10
|
+
import {
|
|
11
|
+
createLiveQueryStore
|
|
12
|
+
} from "./query/live.js";
|
|
13
|
+
import { runMigrations } from "./state/migrations.js";
|
|
14
|
+
import FilterBar from "./components/FilterBar.svelte";
|
|
15
|
+
import SortBar from "./components/SortBar.svelte";
|
|
16
|
+
import GroupBar from "./components/GroupBar.svelte";
|
|
17
|
+
import CellContextMenu from "./components/CellContextMenu.svelte";
|
|
18
|
+
import ColumnMenu from "./components/ColumnMenu.svelte";
|
|
19
|
+
import ColumnPicker from "./components/ColumnPicker.svelte";
|
|
20
|
+
import RowDetailModal from "./components/RowDetailModal.svelte";
|
|
21
|
+
export let db;
|
|
22
|
+
export let table = void 0;
|
|
23
|
+
export let query = void 0;
|
|
24
|
+
export let config = void 0;
|
|
25
|
+
export let features = {};
|
|
26
|
+
export let classNames = {};
|
|
27
|
+
export let rowHeight = "medium";
|
|
28
|
+
export let columnSpacing = "normal";
|
|
29
|
+
export let toolbarLayout = "airtable";
|
|
30
|
+
export let onRowClick = void 0;
|
|
31
|
+
export let onStateChange = void 0;
|
|
32
|
+
let columns = [];
|
|
33
|
+
let allowedColumns = [];
|
|
34
|
+
let initialized = false;
|
|
35
|
+
let error = null;
|
|
36
|
+
let filters = config?.defaultFilters ?? [];
|
|
37
|
+
let filterLogic = config?.filterLogic ?? "and";
|
|
38
|
+
let sorting = config?.defaultSorting ?? [];
|
|
39
|
+
let grouping = config?.defaultGrouping ?? [];
|
|
40
|
+
let globalFilter = "";
|
|
41
|
+
let page = 0;
|
|
42
|
+
let pageSize = config?.pagination?.pageSize ?? 25;
|
|
43
|
+
let totalRows = 0;
|
|
44
|
+
let searchDebounceTimer;
|
|
45
|
+
let filterExpanded = false;
|
|
46
|
+
let sortExpanded = false;
|
|
47
|
+
let groupExpanded = false;
|
|
48
|
+
let contextMenu = null;
|
|
49
|
+
let columnMenuOpen = null;
|
|
50
|
+
let showRowHeightMenu = false;
|
|
51
|
+
let showColumnSpacingMenu = false;
|
|
52
|
+
let showColumnPicker = false;
|
|
53
|
+
const rowHeightOptions = ["short", "medium", "tall", "extra_tall"];
|
|
54
|
+
const columnSpacingOptions = ["narrow", "normal", "wide"];
|
|
55
|
+
let columnVisibility = {};
|
|
56
|
+
let columnOrder = config?.defaultColumnOrder ?? [];
|
|
57
|
+
let draggedColumnId = null;
|
|
58
|
+
let dragOverColumnId = null;
|
|
59
|
+
let columnSizing = config?.defaultColumnSizing ?? {};
|
|
60
|
+
let resizingColumn = null;
|
|
61
|
+
let resizeStartX = 0;
|
|
62
|
+
let resizeStartWidth = 0;
|
|
63
|
+
const COL_MIN_WIDTH = 62;
|
|
64
|
+
const COL_MAX_WIDTH = 1e3;
|
|
65
|
+
const COL_DEFAULT_WIDTH = 180;
|
|
66
|
+
let groupData = [];
|
|
67
|
+
let expandedGroups = /* @__PURE__ */ new Set();
|
|
68
|
+
let totalGroups = 0;
|
|
69
|
+
let groupLoading = /* @__PURE__ */ new Set();
|
|
70
|
+
let rowDetailOpen = false;
|
|
71
|
+
let rowDetailIndex = -1;
|
|
72
|
+
let store = null;
|
|
73
|
+
let storeState = {
|
|
74
|
+
rows: [],
|
|
75
|
+
fields: [],
|
|
76
|
+
loading: true,
|
|
77
|
+
error: null
|
|
78
|
+
};
|
|
79
|
+
$: totalPages = pageSize > 0 ? Math.ceil(totalRows / pageSize) : 0;
|
|
80
|
+
$: containerClass = [
|
|
81
|
+
"gridlite-container",
|
|
82
|
+
`gridlite-row-${rowHeight}`,
|
|
83
|
+
`gridlite-spacing-${columnSpacing}`,
|
|
84
|
+
`gridlite-layout-${toolbarLayout}`,
|
|
85
|
+
classNames.container ?? ""
|
|
86
|
+
].filter(Boolean).join(" ");
|
|
87
|
+
$: visibleColumns = columns.filter((col) => {
|
|
88
|
+
if (col.name in columnVisibility) {
|
|
89
|
+
return columnVisibility[col.name];
|
|
90
|
+
}
|
|
91
|
+
if (config?.defaultVisibleColumns) {
|
|
92
|
+
return config.defaultVisibleColumns.includes(col.name);
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
$: orderedColumns = (() => {
|
|
97
|
+
const order = columnOrder.length > 0 ? columnOrder : config?.defaultColumnOrder ?? [];
|
|
98
|
+
if (order.length > 0) {
|
|
99
|
+
return [...visibleColumns].sort((a, b) => {
|
|
100
|
+
const ai = order.indexOf(a.name);
|
|
101
|
+
const bi = order.indexOf(b.name);
|
|
102
|
+
if (ai === -1 && bi === -1) return 0;
|
|
103
|
+
if (ai === -1) return 1;
|
|
104
|
+
if (bi === -1) return -1;
|
|
105
|
+
return ai - bi;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return visibleColumns;
|
|
109
|
+
})();
|
|
110
|
+
$: validGrouping = grouping.filter((g) => g.column !== "");
|
|
111
|
+
$: isGrouped = validGrouping.length > 0;
|
|
112
|
+
$: nonGroupedColumns = isGrouped ? orderedColumns.filter((col) => !validGrouping.some((g) => g.column === col.name)) : orderedColumns;
|
|
113
|
+
function groupKey(group) {
|
|
114
|
+
return Object.entries(group.values).map(([col, val]) => `${col}=${val === null || val === void 0 ? "__null__" : String(val)}`).join("::");
|
|
115
|
+
}
|
|
116
|
+
async function init() {
|
|
117
|
+
try {
|
|
118
|
+
await runMigrations(db);
|
|
119
|
+
if (table) {
|
|
120
|
+
columns = await introspectTable(db, table);
|
|
121
|
+
allowedColumns = columns.map((c) => c.name);
|
|
122
|
+
if (columns.length === 0) {
|
|
123
|
+
error = `Table "${table}" not found or has no columns`;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
initialized = true;
|
|
128
|
+
await rebuildQuery();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
error = err instanceof Error ? err.message : String(err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function rebuildQuery() {
|
|
134
|
+
if (!initialized) return;
|
|
135
|
+
if (store) {
|
|
136
|
+
await store.destroy();
|
|
137
|
+
store = null;
|
|
138
|
+
}
|
|
139
|
+
let sql;
|
|
140
|
+
let params = [];
|
|
141
|
+
if (query) {
|
|
142
|
+
sql = query;
|
|
143
|
+
} else if (table) {
|
|
144
|
+
if (isGrouped) {
|
|
145
|
+
await rebuildGroupedQuery();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const usePagination = features.pagination !== false;
|
|
149
|
+
const built = buildQuery({
|
|
150
|
+
table,
|
|
151
|
+
filters,
|
|
152
|
+
filterLogic,
|
|
153
|
+
sorting,
|
|
154
|
+
page: usePagination ? page : void 0,
|
|
155
|
+
pageSize: usePagination ? pageSize : void 0,
|
|
156
|
+
allowedColumns,
|
|
157
|
+
globalSearch: globalFilter || void 0
|
|
158
|
+
});
|
|
159
|
+
sql = built.sql;
|
|
160
|
+
params = built.params;
|
|
161
|
+
if (usePagination) {
|
|
162
|
+
await updateTotalCount();
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
error = "Either `table` or `query` prop is required";
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
groupData = [];
|
|
169
|
+
expandedGroups = /* @__PURE__ */ new Set();
|
|
170
|
+
totalGroups = 0;
|
|
171
|
+
store = createLiveQueryStore(db, sql, params);
|
|
172
|
+
store.subscribe((state) => {
|
|
173
|
+
storeState = state;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function cleanAgg(g) {
|
|
177
|
+
return {
|
|
178
|
+
...g,
|
|
179
|
+
aggregations: g.aggregations?.filter((a) => a.column !== "") ?? void 0
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function rebuildGroupedQuery() {
|
|
183
|
+
if (!table) return;
|
|
184
|
+
try {
|
|
185
|
+
const usePagination = features.pagination !== false;
|
|
186
|
+
const topGroupConfig = cleanAgg(validGrouping[0]);
|
|
187
|
+
const summaryQuery = buildGroupSummaryQuery({
|
|
188
|
+
table,
|
|
189
|
+
grouping: [topGroupConfig],
|
|
190
|
+
filters,
|
|
191
|
+
filterLogic,
|
|
192
|
+
allowedColumns,
|
|
193
|
+
globalSearch: globalFilter || void 0,
|
|
194
|
+
sorting,
|
|
195
|
+
page: usePagination ? page : void 0,
|
|
196
|
+
pageSize: usePagination ? pageSize : void 0
|
|
197
|
+
});
|
|
198
|
+
const summaryResult = await db.query(
|
|
199
|
+
summaryQuery.sql,
|
|
200
|
+
summaryQuery.params
|
|
201
|
+
);
|
|
202
|
+
if (usePagination) {
|
|
203
|
+
const countQuery = buildGroupCountQuery({
|
|
204
|
+
table,
|
|
205
|
+
grouping: [topGroupConfig],
|
|
206
|
+
filters,
|
|
207
|
+
filterLogic,
|
|
208
|
+
allowedColumns,
|
|
209
|
+
globalSearch: globalFilter || void 0
|
|
210
|
+
});
|
|
211
|
+
const countResult = await db.query(
|
|
212
|
+
countQuery.sql,
|
|
213
|
+
countQuery.params
|
|
214
|
+
);
|
|
215
|
+
totalGroups = parseInt(countResult.rows[0]?.total ?? "0", 10);
|
|
216
|
+
totalRows = totalGroups;
|
|
217
|
+
}
|
|
218
|
+
const topCol = validGrouping[0].column;
|
|
219
|
+
const newGroupData = summaryResult.rows.map((row) => {
|
|
220
|
+
const values = { [topCol]: row[topCol] };
|
|
221
|
+
const newGroup = {
|
|
222
|
+
values,
|
|
223
|
+
summary: { ...row },
|
|
224
|
+
count: Number(row._count ?? 0),
|
|
225
|
+
depth: 0,
|
|
226
|
+
subGroups: null,
|
|
227
|
+
children: null
|
|
228
|
+
};
|
|
229
|
+
const key = groupKey(newGroup);
|
|
230
|
+
const wasExpanded = expandedGroups.has(key);
|
|
231
|
+
const existing = groupData.find((g) => groupKey(g) === key);
|
|
232
|
+
if (wasExpanded && existing) {
|
|
233
|
+
newGroup.subGroups = existing.subGroups;
|
|
234
|
+
newGroup.children = existing.children;
|
|
235
|
+
}
|
|
236
|
+
return newGroup;
|
|
237
|
+
});
|
|
238
|
+
groupData = newGroupData;
|
|
239
|
+
for (const group of groupData) {
|
|
240
|
+
const key = groupKey(group);
|
|
241
|
+
if (expandedGroups.has(key) && group.subGroups === null && group.children === null) {
|
|
242
|
+
await fetchGroupChildren(group);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
error = err instanceof Error ? err.message : String(err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function fetchGroupChildren(group) {
|
|
250
|
+
if (!table) return;
|
|
251
|
+
const key = groupKey(group);
|
|
252
|
+
groupLoading = /* @__PURE__ */ new Set([...groupLoading, key]);
|
|
253
|
+
try {
|
|
254
|
+
const nextDepth = group.depth + 1;
|
|
255
|
+
const parentValues = Object.entries(group.values).map(([column, value]) => ({
|
|
256
|
+
column,
|
|
257
|
+
value
|
|
258
|
+
}));
|
|
259
|
+
if (nextDepth < validGrouping.length) {
|
|
260
|
+
const subGroupConfig = cleanAgg(validGrouping[nextDepth]);
|
|
261
|
+
const summaryQuery = buildGroupSummaryQuery({
|
|
262
|
+
table,
|
|
263
|
+
grouping: [subGroupConfig],
|
|
264
|
+
filters: [
|
|
265
|
+
...filters,
|
|
266
|
+
// Add parent group constraints as equals filters
|
|
267
|
+
...parentValues.map((pv) => ({
|
|
268
|
+
id: `_group_${pv.column}`,
|
|
269
|
+
field: pv.column,
|
|
270
|
+
operator: pv.value === null ? "is_empty" : "equals",
|
|
271
|
+
value: pv.value
|
|
272
|
+
}))
|
|
273
|
+
],
|
|
274
|
+
filterLogic,
|
|
275
|
+
allowedColumns,
|
|
276
|
+
globalSearch: globalFilter || void 0,
|
|
277
|
+
sorting
|
|
278
|
+
});
|
|
279
|
+
const result = await db.query(
|
|
280
|
+
summaryQuery.sql,
|
|
281
|
+
summaryQuery.params
|
|
282
|
+
);
|
|
283
|
+
const subCol = validGrouping[nextDepth].column;
|
|
284
|
+
const subGroups = result.rows.map((row) => {
|
|
285
|
+
const subValues = {
|
|
286
|
+
...group.values,
|
|
287
|
+
[subCol]: row[subCol]
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
values: subValues,
|
|
291
|
+
summary: { ...row },
|
|
292
|
+
count: Number(row._count ?? 0),
|
|
293
|
+
depth: nextDepth,
|
|
294
|
+
subGroups: null,
|
|
295
|
+
children: null
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
updateGroupInTree(key, { subGroups });
|
|
299
|
+
} else {
|
|
300
|
+
const detailQuery = buildGroupDetailQuery({
|
|
301
|
+
table,
|
|
302
|
+
groupValues: parentValues,
|
|
303
|
+
filters,
|
|
304
|
+
filterLogic,
|
|
305
|
+
sorting,
|
|
306
|
+
allowedColumns,
|
|
307
|
+
globalSearch: globalFilter || void 0
|
|
308
|
+
});
|
|
309
|
+
const result = await db.query(
|
|
310
|
+
detailQuery.sql,
|
|
311
|
+
detailQuery.params
|
|
312
|
+
);
|
|
313
|
+
updateGroupInTree(key, { children: result.rows });
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("Failed to fetch group children:", err);
|
|
317
|
+
} finally {
|
|
318
|
+
const next = new Set(groupLoading);
|
|
319
|
+
next.delete(key);
|
|
320
|
+
groupLoading = next;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function updateGroupInTree(targetKey, updates) {
|
|
324
|
+
groupData = groupData.map((g) => updateGroupNode(g, targetKey, updates));
|
|
325
|
+
}
|
|
326
|
+
function updateGroupNode(node, targetKey, updates) {
|
|
327
|
+
if (groupKey(node) === targetKey) {
|
|
328
|
+
return { ...node, ...updates };
|
|
329
|
+
}
|
|
330
|
+
if (node.subGroups) {
|
|
331
|
+
return {
|
|
332
|
+
...node,
|
|
333
|
+
subGroups: node.subGroups.map((sg) => updateGroupNode(sg, targetKey, updates))
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return node;
|
|
337
|
+
}
|
|
338
|
+
async function updateTotalCount() {
|
|
339
|
+
if (!table) return;
|
|
340
|
+
try {
|
|
341
|
+
const countQuery = buildCountQuery({
|
|
342
|
+
table,
|
|
343
|
+
filters,
|
|
344
|
+
filterLogic,
|
|
345
|
+
allowedColumns,
|
|
346
|
+
globalSearch: globalFilter || void 0
|
|
347
|
+
});
|
|
348
|
+
const result = await db.query(countQuery.sql, countQuery.params);
|
|
349
|
+
totalRows = parseInt(result.rows[0]?.total ?? "0", 10);
|
|
350
|
+
} catch {
|
|
351
|
+
totalRows = 0;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
export function setFilters(newFilters, logic) {
|
|
355
|
+
filters = newFilters;
|
|
356
|
+
if (logic) filterLogic = logic;
|
|
357
|
+
page = 0;
|
|
358
|
+
rebuildQuery();
|
|
359
|
+
notifyStateChange();
|
|
360
|
+
}
|
|
361
|
+
export function setSorting(newSorting) {
|
|
362
|
+
sorting = newSorting;
|
|
363
|
+
rebuildQuery();
|
|
364
|
+
notifyStateChange();
|
|
365
|
+
}
|
|
366
|
+
export function setGrouping(newGrouping) {
|
|
367
|
+
const prevValid = grouping.filter((g) => g.column !== "");
|
|
368
|
+
grouping = newGrouping;
|
|
369
|
+
const nowValid = newGrouping.filter((g) => g.column !== "");
|
|
370
|
+
const changed = prevValid.length !== nowValid.length || prevValid.some((g, i) => g.column !== nowValid[i]?.column);
|
|
371
|
+
if (changed) {
|
|
372
|
+
expandedGroups = /* @__PURE__ */ new Set();
|
|
373
|
+
groupData = [];
|
|
374
|
+
page = 0;
|
|
375
|
+
}
|
|
376
|
+
rebuildQuery();
|
|
377
|
+
notifyStateChange();
|
|
378
|
+
}
|
|
379
|
+
export function setPage(newPage) {
|
|
380
|
+
page = Math.max(0, Math.min(newPage, totalPages - 1));
|
|
381
|
+
rebuildQuery();
|
|
382
|
+
notifyStateChange();
|
|
383
|
+
}
|
|
384
|
+
export function setPageSize(newPageSize) {
|
|
385
|
+
pageSize = newPageSize;
|
|
386
|
+
page = 0;
|
|
387
|
+
rebuildQuery();
|
|
388
|
+
notifyStateChange();
|
|
389
|
+
}
|
|
390
|
+
export function setGlobalFilter(search) {
|
|
391
|
+
globalFilter = search;
|
|
392
|
+
page = 0;
|
|
393
|
+
rebuildQuery();
|
|
394
|
+
notifyStateChange();
|
|
395
|
+
}
|
|
396
|
+
function notifyStateChange() {
|
|
397
|
+
if (onStateChange) {
|
|
398
|
+
onStateChange({
|
|
399
|
+
columnVisibility: Object.fromEntries(visibleColumns.map((c) => [c.name, true])),
|
|
400
|
+
columnOrder: columnOrder.length > 0 ? columnOrder : orderedColumns.map((c) => c.name),
|
|
401
|
+
columnSizing,
|
|
402
|
+
filters,
|
|
403
|
+
filterLogic,
|
|
404
|
+
sorting,
|
|
405
|
+
grouping,
|
|
406
|
+
globalFilter,
|
|
407
|
+
pagination: { page, pageSize, totalRows, totalPages }
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function toggleGroupExpand(group) {
|
|
412
|
+
const key = groupKey(group);
|
|
413
|
+
const next = new Set(expandedGroups);
|
|
414
|
+
if (next.has(key)) {
|
|
415
|
+
next.delete(key);
|
|
416
|
+
expandedGroups = next;
|
|
417
|
+
updateGroupInTree(key, { subGroups: null, children: null });
|
|
418
|
+
} else {
|
|
419
|
+
next.add(key);
|
|
420
|
+
expandedGroups = next;
|
|
421
|
+
await fetchGroupChildren(group);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function getGroupLabel(group) {
|
|
425
|
+
const groupConfig = validGrouping[group.depth];
|
|
426
|
+
if (!groupConfig) return "";
|
|
427
|
+
const val = group.values[groupConfig.column];
|
|
428
|
+
return val === null || val === void 0 ? "(Empty)" : String(val);
|
|
429
|
+
}
|
|
430
|
+
function getGroupAggregations(group) {
|
|
431
|
+
const aggs = [];
|
|
432
|
+
const groupConfig = validGrouping[group.depth];
|
|
433
|
+
if (groupConfig?.aggregations) {
|
|
434
|
+
for (const agg of groupConfig.aggregations) {
|
|
435
|
+
if (agg.column === "") continue;
|
|
436
|
+
const alias = agg.alias ?? `${agg.function}_${agg.column}`;
|
|
437
|
+
const rawVal = group.summary[alias];
|
|
438
|
+
if (rawVal !== null && rawVal !== void 0) {
|
|
439
|
+
const label = `${agg.function.charAt(0).toUpperCase() + agg.function.slice(1)} ${agg.column}`;
|
|
440
|
+
const value = typeof rawVal === "number" ? rawVal.toLocaleString() : String(rawVal);
|
|
441
|
+
aggs.push({ label, value });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return aggs;
|
|
446
|
+
}
|
|
447
|
+
function handleFiltersChange(newFilters) {
|
|
448
|
+
setFilters(newFilters, filterLogic);
|
|
449
|
+
}
|
|
450
|
+
function handleLogicChange(newLogic) {
|
|
451
|
+
filterLogic = newLogic;
|
|
452
|
+
page = 0;
|
|
453
|
+
rebuildQuery();
|
|
454
|
+
notifyStateChange();
|
|
455
|
+
}
|
|
456
|
+
function handleSortingChange(newSorting) {
|
|
457
|
+
setSorting(newSorting);
|
|
458
|
+
}
|
|
459
|
+
function handleGroupingChange(newGrouping) {
|
|
460
|
+
setGrouping(newGrouping);
|
|
461
|
+
}
|
|
462
|
+
function handleGlobalSearchInput(event) {
|
|
463
|
+
const value = event.target.value;
|
|
464
|
+
globalFilter = value;
|
|
465
|
+
clearTimeout(searchDebounceTimer);
|
|
466
|
+
searchDebounceTimer = setTimeout(() => {
|
|
467
|
+
page = 0;
|
|
468
|
+
rebuildQuery();
|
|
469
|
+
notifyStateChange();
|
|
470
|
+
}, 300);
|
|
471
|
+
}
|
|
472
|
+
function clearGlobalSearch() {
|
|
473
|
+
globalFilter = "";
|
|
474
|
+
clearTimeout(searchDebounceTimer);
|
|
475
|
+
page = 0;
|
|
476
|
+
rebuildQuery();
|
|
477
|
+
notifyStateChange();
|
|
478
|
+
}
|
|
479
|
+
function handleCellContextMenu(event, row, col) {
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
const colConfig = config?.columns?.find((c) => c.name === col.name);
|
|
482
|
+
contextMenu = {
|
|
483
|
+
x: event.clientX,
|
|
484
|
+
y: event.clientY,
|
|
485
|
+
value: row[col.name],
|
|
486
|
+
columnName: col.name,
|
|
487
|
+
columnLabel: colConfig?.label ?? col.name,
|
|
488
|
+
isNumeric: col.dataType === "number"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function handleContextFilterEquals(columnName, value) {
|
|
492
|
+
const id = `ctx-${Date.now()}`;
|
|
493
|
+
const newFilter = { id, field: columnName, operator: "equals", value };
|
|
494
|
+
setFilters([...filters, newFilter], filterLogic);
|
|
495
|
+
filterExpanded = true;
|
|
496
|
+
}
|
|
497
|
+
function handleContextFilterNotEquals(columnName, value) {
|
|
498
|
+
const id = `ctx-${Date.now()}`;
|
|
499
|
+
const newFilter = { id, field: columnName, operator: "not_equals", value };
|
|
500
|
+
setFilters([...filters, newFilter], filterLogic);
|
|
501
|
+
filterExpanded = true;
|
|
502
|
+
}
|
|
503
|
+
function handleContextFilterGreaterThan(columnName, value) {
|
|
504
|
+
const id = `ctx-${Date.now()}`;
|
|
505
|
+
const newFilter = { id, field: columnName, operator: "greater_than", value };
|
|
506
|
+
setFilters([...filters, newFilter], filterLogic);
|
|
507
|
+
filterExpanded = true;
|
|
508
|
+
}
|
|
509
|
+
function handleContextFilterLessThan(columnName, value) {
|
|
510
|
+
const id = `ctx-${Date.now()}`;
|
|
511
|
+
const newFilter = { id, field: columnName, operator: "less_than", value };
|
|
512
|
+
setFilters([...filters, newFilter], filterLogic);
|
|
513
|
+
filterExpanded = true;
|
|
514
|
+
}
|
|
515
|
+
function handleColumnMenuSort(columnName, direction) {
|
|
516
|
+
const existing = sorting.findIndex((s) => s.column === columnName);
|
|
517
|
+
const newSorting = [...sorting];
|
|
518
|
+
if (existing >= 0) {
|
|
519
|
+
newSorting[existing] = { column: columnName, direction };
|
|
520
|
+
} else {
|
|
521
|
+
newSorting.push({ column: columnName, direction });
|
|
522
|
+
}
|
|
523
|
+
setSorting(newSorting);
|
|
524
|
+
}
|
|
525
|
+
function handleColumnMenuFilter(columnName) {
|
|
526
|
+
const id = `colmenu-${Date.now()}`;
|
|
527
|
+
const newFilter = { id, field: columnName, operator: "contains", value: "" };
|
|
528
|
+
setFilters([...filters, newFilter], filterLogic);
|
|
529
|
+
filterExpanded = true;
|
|
530
|
+
}
|
|
531
|
+
function handleColumnMenuGroup(columnName) {
|
|
532
|
+
if (!grouping.some((g) => g.column === columnName)) {
|
|
533
|
+
setGrouping([...grouping, { column: columnName }]);
|
|
534
|
+
groupExpanded = true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function handleColumnMenuHide(columnName) {
|
|
538
|
+
toggleColumnVisibility(columnName);
|
|
539
|
+
}
|
|
540
|
+
function toggleColumnVisibility(columnName) {
|
|
541
|
+
const current = isColumnVisible(columnName);
|
|
542
|
+
columnVisibility = { ...columnVisibility, [columnName]: !current };
|
|
543
|
+
notifyStateChange();
|
|
544
|
+
}
|
|
545
|
+
function setColumnVisibility(columnName, visible) {
|
|
546
|
+
columnVisibility = { ...columnVisibility, [columnName]: visible };
|
|
547
|
+
notifyStateChange();
|
|
548
|
+
}
|
|
549
|
+
function toggleAllColumns(show) {
|
|
550
|
+
const newVisibility = {};
|
|
551
|
+
for (const col of columns) {
|
|
552
|
+
newVisibility[col.name] = show;
|
|
553
|
+
}
|
|
554
|
+
columnVisibility = newVisibility;
|
|
555
|
+
notifyStateChange();
|
|
556
|
+
}
|
|
557
|
+
function handleColumnOrderChange(newOrder) {
|
|
558
|
+
columnOrder = newOrder;
|
|
559
|
+
notifyStateChange();
|
|
560
|
+
}
|
|
561
|
+
function getColumnLabel(col) {
|
|
562
|
+
const cfg = config?.columns?.find((c) => c.name === col.name);
|
|
563
|
+
return cfg?.label ?? col.name;
|
|
564
|
+
}
|
|
565
|
+
function isColumnVisible(columnName) {
|
|
566
|
+
if (columnName in columnVisibility) {
|
|
567
|
+
return columnVisibility[columnName];
|
|
568
|
+
}
|
|
569
|
+
if (config?.defaultVisibleColumns) {
|
|
570
|
+
return config.defaultVisibleColumns.includes(columnName);
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
function closeViewMenus(event) {
|
|
575
|
+
const target = event.target;
|
|
576
|
+
if (!target.closest(".gridlite-view-control")) {
|
|
577
|
+
showRowHeightMenu = false;
|
|
578
|
+
showColumnSpacingMenu = false;
|
|
579
|
+
showColumnPicker = false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function getColumnWidth(columnName) {
|
|
583
|
+
if (columnName in columnSizing) return columnSizing[columnName];
|
|
584
|
+
const colConfig = config?.columns?.find((c) => c.name === columnName);
|
|
585
|
+
return colConfig?.width ?? COL_DEFAULT_WIDTH;
|
|
586
|
+
}
|
|
587
|
+
function handleResizeStart(event, columnName) {
|
|
588
|
+
event.preventDefault();
|
|
589
|
+
event.stopPropagation();
|
|
590
|
+
resizingColumn = columnName;
|
|
591
|
+
resizeStartX = "touches" in event ? event.touches[0].clientX : event.clientX;
|
|
592
|
+
resizeStartWidth = getColumnWidth(columnName);
|
|
593
|
+
window.addEventListener("mousemove", handleResizeMove);
|
|
594
|
+
window.addEventListener("mouseup", handleResizeEnd);
|
|
595
|
+
window.addEventListener("touchmove", handleResizeMove);
|
|
596
|
+
window.addEventListener("touchend", handleResizeEnd);
|
|
597
|
+
}
|
|
598
|
+
function handleResizeMove(event) {
|
|
599
|
+
if (!resizingColumn) return;
|
|
600
|
+
const clientX = "touches" in event ? event.touches[0].clientX : event.clientX;
|
|
601
|
+
const delta = clientX - resizeStartX;
|
|
602
|
+
const newWidth = Math.max(COL_MIN_WIDTH, Math.min(COL_MAX_WIDTH, resizeStartWidth + delta));
|
|
603
|
+
columnSizing = { ...columnSizing, [resizingColumn]: newWidth };
|
|
604
|
+
}
|
|
605
|
+
function handleResizeEnd() {
|
|
606
|
+
resizingColumn = null;
|
|
607
|
+
window.removeEventListener("mousemove", handleResizeMove);
|
|
608
|
+
window.removeEventListener("mouseup", handleResizeEnd);
|
|
609
|
+
window.removeEventListener("touchmove", handleResizeMove);
|
|
610
|
+
window.removeEventListener("touchend", handleResizeEnd);
|
|
611
|
+
notifyStateChange();
|
|
612
|
+
}
|
|
613
|
+
function initColumnOrder() {
|
|
614
|
+
if (columnOrder.length === 0 && columns.length > 0) {
|
|
615
|
+
columnOrder = columns.map((c) => c.name);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function handleDragStart(event, columnName) {
|
|
619
|
+
if (features.columnReordering === false) return;
|
|
620
|
+
draggedColumnId = columnName;
|
|
621
|
+
if (event.dataTransfer) {
|
|
622
|
+
event.dataTransfer.effectAllowed = "move";
|
|
623
|
+
event.dataTransfer.setData("text/plain", columnName);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function handleDragOver(event, columnName) {
|
|
627
|
+
if (features.columnReordering === false || !draggedColumnId) return;
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
if (event.dataTransfer) {
|
|
630
|
+
event.dataTransfer.dropEffect = "move";
|
|
631
|
+
}
|
|
632
|
+
dragOverColumnId = columnName;
|
|
633
|
+
}
|
|
634
|
+
function handleDrop(event, targetColumnId) {
|
|
635
|
+
if (features.columnReordering === false) return;
|
|
636
|
+
event.preventDefault();
|
|
637
|
+
if (!draggedColumnId || draggedColumnId === targetColumnId) {
|
|
638
|
+
draggedColumnId = null;
|
|
639
|
+
dragOverColumnId = null;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
initColumnOrder();
|
|
643
|
+
const oldIndex = columnOrder.indexOf(draggedColumnId);
|
|
644
|
+
const newIndex = columnOrder.indexOf(targetColumnId);
|
|
645
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
646
|
+
const newColumnOrder = [...columnOrder];
|
|
647
|
+
const [moved] = newColumnOrder.splice(oldIndex, 1);
|
|
648
|
+
newColumnOrder.splice(newIndex, 0, moved);
|
|
649
|
+
columnOrder = newColumnOrder;
|
|
650
|
+
notifyStateChange();
|
|
651
|
+
}
|
|
652
|
+
draggedColumnId = null;
|
|
653
|
+
dragOverColumnId = null;
|
|
654
|
+
}
|
|
655
|
+
function handleDragEnd() {
|
|
656
|
+
draggedColumnId = null;
|
|
657
|
+
dragOverColumnId = null;
|
|
658
|
+
}
|
|
659
|
+
function openRowDetail(index) {
|
|
660
|
+
rowDetailIndex = index;
|
|
661
|
+
rowDetailOpen = true;
|
|
662
|
+
}
|
|
663
|
+
function closeRowDetail() {
|
|
664
|
+
rowDetailOpen = false;
|
|
665
|
+
rowDetailIndex = -1;
|
|
666
|
+
}
|
|
667
|
+
function prevRowDetail() {
|
|
668
|
+
if (rowDetailIndex > 0) {
|
|
669
|
+
rowDetailIndex--;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function nextRowDetail() {
|
|
673
|
+
if (rowDetailIndex < storeState.rows.length - 1) {
|
|
674
|
+
rowDetailIndex++;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
$: rowDetailRow = rowDetailIndex >= 0 ? storeState.rows[rowDetailIndex] ?? null : null;
|
|
678
|
+
function flattenGroupTree(groups) {
|
|
679
|
+
const items = [];
|
|
680
|
+
for (const group of groups) {
|
|
681
|
+
items.push({ type: "group", group });
|
|
682
|
+
const key = groupKey(group);
|
|
683
|
+
if (expandedGroups.has(key)) {
|
|
684
|
+
if (group.subGroups) {
|
|
685
|
+
items.push(...flattenGroupTree(group.subGroups));
|
|
686
|
+
}
|
|
687
|
+
if (group.children) {
|
|
688
|
+
for (const row of group.children) {
|
|
689
|
+
items.push({ type: "child", row, depth: group.depth + 1 });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return items;
|
|
695
|
+
}
|
|
696
|
+
$: flatGroupItems = isGrouped ? flattenGroupTree(groupData) : [];
|
|
697
|
+
onMount(() => {
|
|
698
|
+
init();
|
|
699
|
+
});
|
|
700
|
+
onDestroy(() => {
|
|
701
|
+
clearTimeout(searchDebounceTimer);
|
|
702
|
+
if (resizingColumn) {
|
|
703
|
+
handleResizeEnd();
|
|
704
|
+
}
|
|
705
|
+
if (store) {
|
|
706
|
+
store.destroy();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
</script>
|
|
710
|
+
|
|
711
|
+
<svelte:window on:click={closeViewMenus} />
|
|
712
|
+
|
|
713
|
+
<div class={containerClass}>
|
|
714
|
+
{#if error}
|
|
715
|
+
<div class="gridlite-empty">{error}</div>
|
|
716
|
+
{:else if !initialized || storeState.loading}
|
|
717
|
+
<div class="gridlite-loading">Loading...</div>
|
|
718
|
+
{:else if storeState.error}
|
|
719
|
+
<div class="gridlite-empty">Error: {storeState.error.message}</div>
|
|
720
|
+
{:else}
|
|
721
|
+
{#if table && toolbarLayout !== 'aggrid'}
|
|
722
|
+
<div class="gridlite-toolbar">
|
|
723
|
+
<!-- Column Visibility (data control) -->
|
|
724
|
+
{#if features.columnVisibility}
|
|
725
|
+
<div class="gridlite-toolbar-columns gridlite-view-control">
|
|
726
|
+
<button
|
|
727
|
+
class="gridlite-view-control-btn"
|
|
728
|
+
class:active={showColumnPicker}
|
|
729
|
+
on:click|stopPropagation={() => {
|
|
730
|
+
showColumnPicker = !showColumnPicker;
|
|
731
|
+
showRowHeightMenu = false;
|
|
732
|
+
showColumnSpacingMenu = false;
|
|
733
|
+
}}
|
|
734
|
+
type="button"
|
|
735
|
+
title="Columns"
|
|
736
|
+
>
|
|
737
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
738
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
|
739
|
+
</svg>
|
|
740
|
+
<span class="gridlite-btn-label">Columns</span>
|
|
741
|
+
</button>
|
|
742
|
+
<ColumnPicker
|
|
743
|
+
{columns}
|
|
744
|
+
columnConfigs={config?.columns ?? []}
|
|
745
|
+
{columnVisibility}
|
|
746
|
+
{columnOrder}
|
|
747
|
+
isOpen={showColumnPicker}
|
|
748
|
+
defaultVisibleColumns={config?.defaultVisibleColumns}
|
|
749
|
+
onVisibilityChange={setColumnVisibility}
|
|
750
|
+
onToggleAll={toggleAllColumns}
|
|
751
|
+
onOrderChange={handleColumnOrderChange}
|
|
752
|
+
/>
|
|
753
|
+
</div>
|
|
754
|
+
{/if}
|
|
755
|
+
|
|
756
|
+
<!-- Filter -->
|
|
757
|
+
{#if features.filtering}
|
|
758
|
+
<div class="gridlite-toolbar-filter">
|
|
759
|
+
<FilterBar
|
|
760
|
+
{db}
|
|
761
|
+
{table}
|
|
762
|
+
{columns}
|
|
763
|
+
columnConfigs={config?.columns ?? []}
|
|
764
|
+
{allowedColumns}
|
|
765
|
+
conditions={filters}
|
|
766
|
+
onConditionsChange={handleFiltersChange}
|
|
767
|
+
logic={filterLogic}
|
|
768
|
+
onLogicChange={handleLogicChange}
|
|
769
|
+
isExpanded={filterExpanded}
|
|
770
|
+
onExpandedChange={(expanded) => (filterExpanded = expanded)}
|
|
771
|
+
/>
|
|
772
|
+
</div>
|
|
773
|
+
{/if}
|
|
774
|
+
|
|
775
|
+
<!-- Group -->
|
|
776
|
+
{#if features.grouping}
|
|
777
|
+
<div class="gridlite-toolbar-group">
|
|
778
|
+
<GroupBar
|
|
779
|
+
{columns}
|
|
780
|
+
columnConfigs={config?.columns ?? []}
|
|
781
|
+
{grouping}
|
|
782
|
+
onGroupingChange={handleGroupingChange}
|
|
783
|
+
isExpanded={groupExpanded}
|
|
784
|
+
onExpandedChange={(expanded) => (groupExpanded = expanded)}
|
|
785
|
+
/>
|
|
786
|
+
</div>
|
|
787
|
+
{/if}
|
|
788
|
+
|
|
789
|
+
<!-- Sort -->
|
|
790
|
+
{#if features.sorting}
|
|
791
|
+
<div class="gridlite-toolbar-sort">
|
|
792
|
+
<SortBar
|
|
793
|
+
{columns}
|
|
794
|
+
columnConfigs={config?.columns ?? []}
|
|
795
|
+
{sorting}
|
|
796
|
+
onSortingChange={handleSortingChange}
|
|
797
|
+
isExpanded={sortExpanded}
|
|
798
|
+
onExpandedChange={(expanded) => (sortExpanded = expanded)}
|
|
799
|
+
/>
|
|
800
|
+
</div>
|
|
801
|
+
{/if}
|
|
802
|
+
|
|
803
|
+
<!-- View Controls (Row Height + Column Spacing) -->
|
|
804
|
+
<div class="gridlite-toolbar-view gridlite-view-controls">
|
|
805
|
+
<div class="gridlite-view-control">
|
|
806
|
+
<button
|
|
807
|
+
class="gridlite-view-control-btn"
|
|
808
|
+
class:active={showRowHeightMenu}
|
|
809
|
+
on:click|stopPropagation={() => {
|
|
810
|
+
showRowHeightMenu = !showRowHeightMenu;
|
|
811
|
+
showColumnSpacingMenu = false;
|
|
812
|
+
showColumnPicker = false;
|
|
813
|
+
}}
|
|
814
|
+
type="button"
|
|
815
|
+
title="Row height"
|
|
816
|
+
>
|
|
817
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
818
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
819
|
+
</svg>
|
|
820
|
+
</button>
|
|
821
|
+
{#if showRowHeightMenu}
|
|
822
|
+
<div class="gridlite-view-dropdown">
|
|
823
|
+
<div class="gridlite-view-dropdown-title">Row Height</div>
|
|
824
|
+
{#each rowHeightOptions as rh}
|
|
825
|
+
<button
|
|
826
|
+
class="gridlite-view-dropdown-item"
|
|
827
|
+
class:selected={rowHeight === rh}
|
|
828
|
+
on:click={() => {
|
|
829
|
+
rowHeight = rh;
|
|
830
|
+
showRowHeightMenu = false;
|
|
831
|
+
}}
|
|
832
|
+
type="button"
|
|
833
|
+
>
|
|
834
|
+
{rh === 'extra_tall' ? 'Extra Tall' : rh.charAt(0).toUpperCase() + rh.slice(1)}
|
|
835
|
+
</button>
|
|
836
|
+
{/each}
|
|
837
|
+
</div>
|
|
838
|
+
{/if}
|
|
839
|
+
</div>
|
|
840
|
+
<div class="gridlite-view-control">
|
|
841
|
+
<button
|
|
842
|
+
class="gridlite-view-control-btn"
|
|
843
|
+
class:active={showColumnSpacingMenu}
|
|
844
|
+
on:click|stopPropagation={() => {
|
|
845
|
+
showColumnSpacingMenu = !showColumnSpacingMenu;
|
|
846
|
+
showRowHeightMenu = false;
|
|
847
|
+
showColumnPicker = false;
|
|
848
|
+
}}
|
|
849
|
+
type="button"
|
|
850
|
+
title="Column spacing"
|
|
851
|
+
>
|
|
852
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
853
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4v16M15 4v16M4 9h16M4 15h16" />
|
|
854
|
+
</svg>
|
|
855
|
+
</button>
|
|
856
|
+
{#if showColumnSpacingMenu}
|
|
857
|
+
<div class="gridlite-view-dropdown">
|
|
858
|
+
<div class="gridlite-view-dropdown-title">Column Spacing</div>
|
|
859
|
+
{#each columnSpacingOptions as sp}
|
|
860
|
+
<button
|
|
861
|
+
class="gridlite-view-dropdown-item"
|
|
862
|
+
class:selected={columnSpacing === sp}
|
|
863
|
+
on:click={() => {
|
|
864
|
+
columnSpacing = sp;
|
|
865
|
+
showColumnSpacingMenu = false;
|
|
866
|
+
}}
|
|
867
|
+
type="button"
|
|
868
|
+
>
|
|
869
|
+
{sp.charAt(0).toUpperCase() + sp.slice(1)}
|
|
870
|
+
</button>
|
|
871
|
+
{/each}
|
|
872
|
+
</div>
|
|
873
|
+
{/if}
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<!-- Search -->
|
|
878
|
+
{#if features.globalSearch}
|
|
879
|
+
<div class="gridlite-toolbar-search">
|
|
880
|
+
<div class="gridlite-search">
|
|
881
|
+
<svg class="gridlite-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
882
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
883
|
+
</svg>
|
|
884
|
+
<input
|
|
885
|
+
class="gridlite-search-input"
|
|
886
|
+
type="text"
|
|
887
|
+
placeholder="Search all columns..."
|
|
888
|
+
value={globalFilter}
|
|
889
|
+
on:input={handleGlobalSearchInput}
|
|
890
|
+
/>
|
|
891
|
+
{#if globalFilter}
|
|
892
|
+
<button class="gridlite-search-clear" on:click={clearGlobalSearch} type="button" title="Clear search">
|
|
893
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
894
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
895
|
+
</svg>
|
|
896
|
+
</button>
|
|
897
|
+
{/if}
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
900
|
+
{/if}
|
|
901
|
+
</div>
|
|
902
|
+
{/if}
|
|
903
|
+
|
|
904
|
+
{#if table && toolbarLayout === 'aggrid'}
|
|
905
|
+
<!-- AG Grid layout: sidebar on right, minimal toolbar on top -->
|
|
906
|
+
<!-- TODO(#1): aggrid layout is experimental — not production-ready. Needs debugging. -->
|
|
907
|
+
<div class="gridlite-toolbar gridlite-toolbar-aggrid-top">
|
|
908
|
+
{#if features.globalSearch}
|
|
909
|
+
<div class="gridlite-toolbar-search">
|
|
910
|
+
<div class="gridlite-search">
|
|
911
|
+
<svg class="gridlite-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
912
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
913
|
+
</svg>
|
|
914
|
+
<input
|
|
915
|
+
class="gridlite-search-input"
|
|
916
|
+
type="text"
|
|
917
|
+
placeholder="Search all columns..."
|
|
918
|
+
value={globalFilter}
|
|
919
|
+
on:input={handleGlobalSearchInput}
|
|
920
|
+
/>
|
|
921
|
+
{#if globalFilter}
|
|
922
|
+
<button class="gridlite-search-clear" on:click={clearGlobalSearch} type="button" title="Clear search">
|
|
923
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
924
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
925
|
+
</svg>
|
|
926
|
+
</button>
|
|
927
|
+
{/if}
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
{/if}
|
|
931
|
+
{#if features.sorting}
|
|
932
|
+
<div class="gridlite-toolbar-sort">
|
|
933
|
+
<SortBar
|
|
934
|
+
{columns}
|
|
935
|
+
columnConfigs={config?.columns ?? []}
|
|
936
|
+
{sorting}
|
|
937
|
+
onSortingChange={handleSortingChange}
|
|
938
|
+
isExpanded={sortExpanded}
|
|
939
|
+
onExpandedChange={(expanded) => (sortExpanded = expanded)}
|
|
940
|
+
/>
|
|
941
|
+
</div>
|
|
942
|
+
{/if}
|
|
943
|
+
{#if features.grouping}
|
|
944
|
+
<div class="gridlite-toolbar-group">
|
|
945
|
+
<GroupBar
|
|
946
|
+
{columns}
|
|
947
|
+
columnConfigs={config?.columns ?? []}
|
|
948
|
+
{grouping}
|
|
949
|
+
onGroupingChange={handleGroupingChange}
|
|
950
|
+
isExpanded={groupExpanded}
|
|
951
|
+
onExpandedChange={(expanded) => (groupExpanded = expanded)}
|
|
952
|
+
/>
|
|
953
|
+
</div>
|
|
954
|
+
{/if}
|
|
955
|
+
<div class="gridlite-toolbar-view gridlite-view-controls">
|
|
956
|
+
<div class="gridlite-view-control">
|
|
957
|
+
<button
|
|
958
|
+
class="gridlite-view-control-btn"
|
|
959
|
+
class:active={showRowHeightMenu}
|
|
960
|
+
on:click|stopPropagation={() => {
|
|
961
|
+
showRowHeightMenu = !showRowHeightMenu;
|
|
962
|
+
showColumnSpacingMenu = false;
|
|
963
|
+
}}
|
|
964
|
+
type="button"
|
|
965
|
+
title="Row height"
|
|
966
|
+
>
|
|
967
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
968
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
969
|
+
</svg>
|
|
970
|
+
</button>
|
|
971
|
+
{#if showRowHeightMenu}
|
|
972
|
+
<div class="gridlite-view-dropdown">
|
|
973
|
+
<div class="gridlite-view-dropdown-title">Row Height</div>
|
|
974
|
+
{#each rowHeightOptions as rh}
|
|
975
|
+
<button
|
|
976
|
+
class="gridlite-view-dropdown-item"
|
|
977
|
+
class:selected={rowHeight === rh}
|
|
978
|
+
on:click={() => { rowHeight = rh; showRowHeightMenu = false; }}
|
|
979
|
+
type="button"
|
|
980
|
+
>{rh === 'extra_tall' ? 'Extra Tall' : rh.charAt(0).toUpperCase() + rh.slice(1)}</button>
|
|
981
|
+
{/each}
|
|
982
|
+
</div>
|
|
983
|
+
{/if}
|
|
984
|
+
</div>
|
|
985
|
+
<div class="gridlite-view-control">
|
|
986
|
+
<button
|
|
987
|
+
class="gridlite-view-control-btn"
|
|
988
|
+
class:active={showColumnSpacingMenu}
|
|
989
|
+
on:click|stopPropagation={() => {
|
|
990
|
+
showColumnSpacingMenu = !showColumnSpacingMenu;
|
|
991
|
+
showRowHeightMenu = false;
|
|
992
|
+
}}
|
|
993
|
+
type="button"
|
|
994
|
+
title="Column spacing"
|
|
995
|
+
>
|
|
996
|
+
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
997
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4v16M15 4v16M4 9h16M4 15h16" />
|
|
998
|
+
</svg>
|
|
999
|
+
</button>
|
|
1000
|
+
{#if showColumnSpacingMenu}
|
|
1001
|
+
<div class="gridlite-view-dropdown">
|
|
1002
|
+
<div class="gridlite-view-dropdown-title">Column Spacing</div>
|
|
1003
|
+
{#each columnSpacingOptions as sp}
|
|
1004
|
+
<button
|
|
1005
|
+
class="gridlite-view-dropdown-item"
|
|
1006
|
+
class:selected={columnSpacing === sp}
|
|
1007
|
+
on:click={() => { columnSpacing = sp; showColumnSpacingMenu = false; }}
|
|
1008
|
+
type="button"
|
|
1009
|
+
>{sp.charAt(0).toUpperCase() + sp.slice(1)}</button>
|
|
1010
|
+
{/each}
|
|
1011
|
+
</div>
|
|
1012
|
+
{/if}
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
{/if}
|
|
1017
|
+
|
|
1018
|
+
<div class="gridlite-body" class:gridlite-aggrid-body={toolbarLayout === 'aggrid'}>
|
|
1019
|
+
<div class="gridlite-table-wrap">
|
|
1020
|
+
<table
|
|
1021
|
+
class={`gridlite-table ${classNames.table ?? ''}`}
|
|
1022
|
+
style={features.columnResizing ? 'table-layout: fixed;' : ''}
|
|
1023
|
+
>
|
|
1024
|
+
<thead class={`gridlite-thead ${classNames.thead ?? ''}`}>
|
|
1025
|
+
<tr class={classNames.tr ?? ''}>
|
|
1026
|
+
{#each (isGrouped ? nonGroupedColumns : orderedColumns) as col}
|
|
1027
|
+
<th
|
|
1028
|
+
class={`gridlite-th gridlite-th-interactive ${classNames.th ?? ''}`}
|
|
1029
|
+
class:dragging={draggedColumnId === col.name}
|
|
1030
|
+
class:drag-over={dragOverColumnId === col.name && draggedColumnId !== col.name}
|
|
1031
|
+
style={features.columnResizing ? `width: ${getColumnWidth(col.name)}px;` : ''}
|
|
1032
|
+
on:dragover={(e) => handleDragOver(e, col.name)}
|
|
1033
|
+
on:drop={(e) => handleDrop(e, col.name)}
|
|
1034
|
+
>
|
|
1035
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
1036
|
+
<div
|
|
1037
|
+
class="gridlite-th-content"
|
|
1038
|
+
draggable={features.columnReordering ?? false}
|
|
1039
|
+
on:dragstart={(e) => handleDragStart(e, col.name)}
|
|
1040
|
+
on:dragend={handleDragEnd}
|
|
1041
|
+
style={features.columnReordering ? 'cursor: grab;' : ''}
|
|
1042
|
+
>
|
|
1043
|
+
<span class="gridlite-th-label">
|
|
1044
|
+
{#if config?.columns}
|
|
1045
|
+
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1046
|
+
{colConfig?.label ?? col.name}
|
|
1047
|
+
{:else}
|
|
1048
|
+
{col.name}
|
|
1049
|
+
{/if}
|
|
1050
|
+
</span>
|
|
1051
|
+
{#if table}
|
|
1052
|
+
<button
|
|
1053
|
+
class="gridlite-th-menu-btn"
|
|
1054
|
+
on:click|stopPropagation={() =>
|
|
1055
|
+
(columnMenuOpen = columnMenuOpen === col.name ? null : col.name)}
|
|
1056
|
+
title="Column options"
|
|
1057
|
+
type="button"
|
|
1058
|
+
>
|
|
1059
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1060
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
1061
|
+
</svg>
|
|
1062
|
+
</button>
|
|
1063
|
+
<ColumnMenu
|
|
1064
|
+
columnName={col.name}
|
|
1065
|
+
isOpen={columnMenuOpen === col.name}
|
|
1066
|
+
{sorting}
|
|
1067
|
+
canSort={features.sorting ?? false}
|
|
1068
|
+
canFilter={features.filtering ?? false}
|
|
1069
|
+
canGroup={features.grouping ?? false}
|
|
1070
|
+
onSort={handleColumnMenuSort}
|
|
1071
|
+
onFilter={handleColumnMenuFilter}
|
|
1072
|
+
onGroup={handleColumnMenuGroup}
|
|
1073
|
+
onHide={handleColumnMenuHide}
|
|
1074
|
+
onClose={() => (columnMenuOpen = null)}
|
|
1075
|
+
/>
|
|
1076
|
+
{/if}
|
|
1077
|
+
</div>
|
|
1078
|
+
{#if features.columnResizing}
|
|
1079
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
1080
|
+
<div
|
|
1081
|
+
class="gridlite-resize-handle"
|
|
1082
|
+
class:resizing={resizingColumn === col.name}
|
|
1083
|
+
on:mousedown={(e) => handleResizeStart(e, col.name)}
|
|
1084
|
+
on:touchstart={(e) => handleResizeStart(e, col.name)}
|
|
1085
|
+
/>
|
|
1086
|
+
{/if}
|
|
1087
|
+
</th>
|
|
1088
|
+
{/each}
|
|
1089
|
+
</tr>
|
|
1090
|
+
</thead>
|
|
1091
|
+
<tbody class={`gridlite-tbody ${classNames.tbody ?? ''}`}>
|
|
1092
|
+
{#if isGrouped}
|
|
1093
|
+
<!-- Grouped view: flattened tree of group headers and child rows -->
|
|
1094
|
+
{#if flatGroupItems.length === 0}
|
|
1095
|
+
<tr>
|
|
1096
|
+
<td colspan={nonGroupedColumns.length} class="gridlite-empty">
|
|
1097
|
+
No data
|
|
1098
|
+
</td>
|
|
1099
|
+
</tr>
|
|
1100
|
+
{:else}
|
|
1101
|
+
{#each flatGroupItems as item}
|
|
1102
|
+
{#if item.type === 'group'}
|
|
1103
|
+
{@const group = item.group}
|
|
1104
|
+
{@const key = groupKey(group)}
|
|
1105
|
+
{@const expanded = expandedGroups.has(key)}
|
|
1106
|
+
{@const loading = groupLoading.has(key)}
|
|
1107
|
+
{@const aggs = getGroupAggregations(group)}
|
|
1108
|
+
<!-- Group header row -->
|
|
1109
|
+
<tr
|
|
1110
|
+
class="gridlite-group-row"
|
|
1111
|
+
on:click={() => toggleGroupExpand(group)}
|
|
1112
|
+
role="button"
|
|
1113
|
+
tabindex={0}
|
|
1114
|
+
on:keydown={(e) => {
|
|
1115
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
toggleGroupExpand(group);
|
|
1118
|
+
}
|
|
1119
|
+
}}
|
|
1120
|
+
>
|
|
1121
|
+
<td colspan={nonGroupedColumns.length} class="gridlite-group-td">
|
|
1122
|
+
<div class="gridlite-group-header gridlite-group-level-{Math.min(group.depth, 2)}">
|
|
1123
|
+
<svg
|
|
1124
|
+
class="gridlite-group-chevron"
|
|
1125
|
+
class:expanded
|
|
1126
|
+
width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
1127
|
+
>
|
|
1128
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
1129
|
+
</svg>
|
|
1130
|
+
<span class="gridlite-group-label">{getGroupLabel(group)}</span>
|
|
1131
|
+
<span class="gridlite-group-count">{group.count}</span>
|
|
1132
|
+
{#each aggs as agg}
|
|
1133
|
+
<span class="gridlite-group-agg" title={agg.label}>{agg.label}: {agg.value}</span>
|
|
1134
|
+
{/each}
|
|
1135
|
+
{#if loading}
|
|
1136
|
+
<span class="gridlite-group-loading">Loading...</span>
|
|
1137
|
+
{/if}
|
|
1138
|
+
</div>
|
|
1139
|
+
</td>
|
|
1140
|
+
</tr>
|
|
1141
|
+
{:else}
|
|
1142
|
+
{@const row = item.row}
|
|
1143
|
+
<!-- Child data row -->
|
|
1144
|
+
<tr
|
|
1145
|
+
class={`gridlite-tr gridlite-group-child ${classNames.tr ?? ''}`}
|
|
1146
|
+
on:click={() => {
|
|
1147
|
+
onRowClick?.(row);
|
|
1148
|
+
}}
|
|
1149
|
+
role={onRowClick ? 'button' : undefined}
|
|
1150
|
+
tabindex={onRowClick ? 0 : undefined}
|
|
1151
|
+
on:keydown={(e) => {
|
|
1152
|
+
if ((e.key === 'Enter' || e.key === ' ')) {
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
onRowClick?.(row);
|
|
1155
|
+
}
|
|
1156
|
+
}}
|
|
1157
|
+
>
|
|
1158
|
+
{#each nonGroupedColumns as col}
|
|
1159
|
+
<td
|
|
1160
|
+
class={`gridlite-td ${classNames.td ?? ''}`}
|
|
1161
|
+
on:contextmenu={(e) => handleCellContextMenu(e, row, col)}
|
|
1162
|
+
>
|
|
1163
|
+
{#if config?.columns}
|
|
1164
|
+
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1165
|
+
{#if colConfig?.format}
|
|
1166
|
+
{colConfig.format(row[col.name])}
|
|
1167
|
+
{:else}
|
|
1168
|
+
{row[col.name] ?? ''}
|
|
1169
|
+
{/if}
|
|
1170
|
+
{:else}
|
|
1171
|
+
{row[col.name] ?? ''}
|
|
1172
|
+
{/if}
|
|
1173
|
+
</td>
|
|
1174
|
+
{/each}
|
|
1175
|
+
</tr>
|
|
1176
|
+
{/if}
|
|
1177
|
+
{/each}
|
|
1178
|
+
{/if}
|
|
1179
|
+
{:else}
|
|
1180
|
+
<!-- Flat (non-grouped) view -->
|
|
1181
|
+
{#if storeState.rows.length === 0}
|
|
1182
|
+
<tr>
|
|
1183
|
+
<td colspan={orderedColumns.length} class="gridlite-empty">
|
|
1184
|
+
No data
|
|
1185
|
+
</td>
|
|
1186
|
+
</tr>
|
|
1187
|
+
{:else}
|
|
1188
|
+
{#each storeState.rows as row, rowIndex}
|
|
1189
|
+
<tr
|
|
1190
|
+
class={`gridlite-tr ${classNames.tr ?? ''}`}
|
|
1191
|
+
on:click={() => {
|
|
1192
|
+
if (features.rowDetail) {
|
|
1193
|
+
openRowDetail(rowIndex);
|
|
1194
|
+
}
|
|
1195
|
+
onRowClick?.(row);
|
|
1196
|
+
}}
|
|
1197
|
+
role={onRowClick || features.rowDetail ? 'button' : undefined}
|
|
1198
|
+
tabindex={onRowClick || features.rowDetail ? 0 : undefined}
|
|
1199
|
+
on:keydown={(e) => {
|
|
1200
|
+
if ((e.key === 'Enter' || e.key === ' ')) {
|
|
1201
|
+
e.preventDefault();
|
|
1202
|
+
if (features.rowDetail) openRowDetail(rowIndex);
|
|
1203
|
+
onRowClick?.(row);
|
|
1204
|
+
}
|
|
1205
|
+
}}
|
|
1206
|
+
>
|
|
1207
|
+
{#each orderedColumns as col}
|
|
1208
|
+
<td
|
|
1209
|
+
class={`gridlite-td ${classNames.td ?? ''}`}
|
|
1210
|
+
on:contextmenu={(e) => handleCellContextMenu(e, row, col)}
|
|
1211
|
+
>
|
|
1212
|
+
{#if config?.columns}
|
|
1213
|
+
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1214
|
+
{#if colConfig?.format}
|
|
1215
|
+
{colConfig.format(row[col.name])}
|
|
1216
|
+
{:else}
|
|
1217
|
+
{row[col.name] ?? ''}
|
|
1218
|
+
{/if}
|
|
1219
|
+
{:else}
|
|
1220
|
+
{row[col.name] ?? ''}
|
|
1221
|
+
{/if}
|
|
1222
|
+
</td>
|
|
1223
|
+
{/each}
|
|
1224
|
+
</tr>
|
|
1225
|
+
{/each}
|
|
1226
|
+
{/if}
|
|
1227
|
+
{/if}
|
|
1228
|
+
</tbody>
|
|
1229
|
+
</table>
|
|
1230
|
+
</div>
|
|
1231
|
+
|
|
1232
|
+
{#if features.pagination !== false && totalRows > 0}
|
|
1233
|
+
<div class={`gridlite-pagination ${classNames.pagination ?? ''}`}>
|
|
1234
|
+
<span>
|
|
1235
|
+
Page {page + 1} of {totalPages} ({totalRows} {isGrouped ? 'groups' : 'rows'})
|
|
1236
|
+
</span>
|
|
1237
|
+
<div class="gridlite-pagination-controls">
|
|
1238
|
+
<select
|
|
1239
|
+
class="gridlite-page-size-select"
|
|
1240
|
+
value={pageSize}
|
|
1241
|
+
on:change={(e) => setPageSize(Number(e.currentTarget.value))}
|
|
1242
|
+
>
|
|
1243
|
+
{#each config?.pagination?.pageSizeOptions ?? [10, 25, 50, 100] as size}
|
|
1244
|
+
<option value={size}>{size} / page</option>
|
|
1245
|
+
{/each}
|
|
1246
|
+
</select>
|
|
1247
|
+
<button disabled={page === 0} on:click={() => setPage(0)}>
|
|
1248
|
+
First
|
|
1249
|
+
</button>
|
|
1250
|
+
<button disabled={page === 0} on:click={() => setPage(page - 1)}>
|
|
1251
|
+
Prev
|
|
1252
|
+
</button>
|
|
1253
|
+
<button disabled={page >= totalPages - 1} on:click={() => setPage(page + 1)}>
|
|
1254
|
+
Next
|
|
1255
|
+
</button>
|
|
1256
|
+
<button disabled={page >= totalPages - 1} on:click={() => setPage(totalPages - 1)}>
|
|
1257
|
+
Last
|
|
1258
|
+
</button>
|
|
1259
|
+
</div>
|
|
1260
|
+
</div>
|
|
1261
|
+
{/if}
|
|
1262
|
+
|
|
1263
|
+
{#if toolbarLayout === 'aggrid'}
|
|
1264
|
+
<!-- AG Grid sidebar: columns + filters on right -->
|
|
1265
|
+
<!-- TODO(#1): aggrid sidebar is experimental — not production-ready. Needs debugging. -->
|
|
1266
|
+
<aside class="gridlite-aggrid-sidebar">
|
|
1267
|
+
{#if features.columnVisibility}
|
|
1268
|
+
<div class="gridlite-aggrid-sidebar-section">
|
|
1269
|
+
<div class="gridlite-aggrid-sidebar-header">Columns</div>
|
|
1270
|
+
<ColumnPicker
|
|
1271
|
+
{columns}
|
|
1272
|
+
columnConfigs={config?.columns ?? []}
|
|
1273
|
+
{columnVisibility}
|
|
1274
|
+
{columnOrder}
|
|
1275
|
+
isOpen={true}
|
|
1276
|
+
defaultVisibleColumns={config?.defaultVisibleColumns}
|
|
1277
|
+
onVisibilityChange={setColumnVisibility}
|
|
1278
|
+
onToggleAll={toggleAllColumns}
|
|
1279
|
+
onOrderChange={handleColumnOrderChange}
|
|
1280
|
+
/>
|
|
1281
|
+
</div>
|
|
1282
|
+
{/if}
|
|
1283
|
+
{#if features.filtering}
|
|
1284
|
+
<div class="gridlite-aggrid-sidebar-section">
|
|
1285
|
+
<div class="gridlite-aggrid-sidebar-header">Filters</div>
|
|
1286
|
+
<FilterBar
|
|
1287
|
+
{db}
|
|
1288
|
+
table={table ?? ''}
|
|
1289
|
+
{columns}
|
|
1290
|
+
columnConfigs={config?.columns ?? []}
|
|
1291
|
+
{allowedColumns}
|
|
1292
|
+
conditions={filters}
|
|
1293
|
+
onConditionsChange={handleFiltersChange}
|
|
1294
|
+
logic={filterLogic}
|
|
1295
|
+
onLogicChange={handleLogicChange}
|
|
1296
|
+
isExpanded={true}
|
|
1297
|
+
onExpandedChange={() => {}}
|
|
1298
|
+
/>
|
|
1299
|
+
</div>
|
|
1300
|
+
{/if}
|
|
1301
|
+
</aside>
|
|
1302
|
+
{/if}
|
|
1303
|
+
</div>
|
|
1304
|
+
|
|
1305
|
+
{#if contextMenu}
|
|
1306
|
+
<CellContextMenu
|
|
1307
|
+
x={contextMenu.x}
|
|
1308
|
+
y={contextMenu.y}
|
|
1309
|
+
value={contextMenu.value}
|
|
1310
|
+
columnName={contextMenu.columnName}
|
|
1311
|
+
columnLabel={contextMenu.columnLabel}
|
|
1312
|
+
isNumeric={contextMenu.isNumeric}
|
|
1313
|
+
onFilterEquals={handleContextFilterEquals}
|
|
1314
|
+
onFilterNotEquals={handleContextFilterNotEquals}
|
|
1315
|
+
onFilterGreaterThan={handleContextFilterGreaterThan}
|
|
1316
|
+
onFilterLessThan={handleContextFilterLessThan}
|
|
1317
|
+
onClose={() => (contextMenu = null)}
|
|
1318
|
+
/>
|
|
1319
|
+
{/if}
|
|
1320
|
+
|
|
1321
|
+
{#if features.rowDetail}
|
|
1322
|
+
<RowDetailModal
|
|
1323
|
+
isOpen={rowDetailOpen}
|
|
1324
|
+
hasPrev={rowDetailIndex > 0}
|
|
1325
|
+
hasNext={rowDetailIndex < storeState.rows.length - 1}
|
|
1326
|
+
onClose={closeRowDetail}
|
|
1327
|
+
onPrev={prevRowDetail}
|
|
1328
|
+
onNext={nextRowDetail}
|
|
1329
|
+
>
|
|
1330
|
+
{#if rowDetailRow}
|
|
1331
|
+
<dl class="gridlite-row-detail">
|
|
1332
|
+
{#each orderedColumns as col}
|
|
1333
|
+
<div class="gridlite-row-detail-field">
|
|
1334
|
+
<dt>
|
|
1335
|
+
{#if config?.columns}
|
|
1336
|
+
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1337
|
+
{colConfig?.label ?? col.name}
|
|
1338
|
+
{:else}
|
|
1339
|
+
{col.name}
|
|
1340
|
+
{/if}
|
|
1341
|
+
</dt>
|
|
1342
|
+
<dd>
|
|
1343
|
+
{#if config?.columns}
|
|
1344
|
+
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1345
|
+
{#if colConfig?.format}
|
|
1346
|
+
{colConfig.format(rowDetailRow[col.name])}
|
|
1347
|
+
{:else}
|
|
1348
|
+
{rowDetailRow[col.name] ?? '—'}
|
|
1349
|
+
{/if}
|
|
1350
|
+
{:else}
|
|
1351
|
+
{rowDetailRow[col.name] ?? '—'}
|
|
1352
|
+
{/if}
|
|
1353
|
+
</dd>
|
|
1354
|
+
</div>
|
|
1355
|
+
{/each}
|
|
1356
|
+
</dl>
|
|
1357
|
+
{/if}
|
|
1358
|
+
</RowDetailModal>
|
|
1359
|
+
{/if}
|
|
1360
|
+
{/if}
|
|
1361
|
+
</div>
|