@shotleybuilder/svelte-gridlite-kit 0.2.1 → 0.3.1
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 +2 -1
- package/dist/GridLite.svelte +121 -29
- package/dist/GridLite.svelte.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/query/schema.d.ts +7 -2
- package/dist/query/schema.js +61 -26
- package/dist/state/migrations.js +7 -0
- package/dist/state/views.d.ts +4 -2
- package/dist/state/views.js +14 -10
- package/dist/styles/gridlite.css +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -156,12 +156,13 @@ The component accepts a **PGLite instance + table name** (or a raw SQL query), n
|
|
|
156
156
|
| Numeric range hints | Scan all rows for min/max | `SELECT MIN(), MAX()` |
|
|
157
157
|
| Column type detection | Sample rows and guess | Schema introspection |
|
|
158
158
|
| Config persistence | localStorage JSON | PGLite tables with IndexedDB backing |
|
|
159
|
+
| Column labels | Static config only | User-editable, persisted in PGLite |
|
|
159
160
|
| Global search | String matching across columns | `ILIKE` or full-text search |
|
|
160
161
|
|
|
161
162
|
### Key Design Decisions
|
|
162
163
|
|
|
163
164
|
- **No TanStack Table dependency.** The SQL engine IS the table engine.
|
|
164
|
-
- **PGLite is the state store.** Table configs, view presets, column visibility, sort/filter state — all stored in PGLite tables, persisted automatically via IndexedDB.
|
|
165
|
+
- **PGLite is the state store.** Table configs, view presets, column visibility, custom labels, sort/filter state — all stored in PGLite tables, persisted automatically via IndexedDB.
|
|
165
166
|
- **FilterBar emits SQL.** Postgres operators (regex, `ILIKE`, date math, JSON paths, FTS) are available natively.
|
|
166
167
|
- **Live queries drive reactivity.** PGLite `live.query()` replaces Svelte writable stores for data. UI auto-updates when underlying data changes.
|
|
167
168
|
- **Column types come from schema introspection**, not data sampling.
|
package/dist/GridLite.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>import { onMount, onDestroy } from "svelte";
|
|
2
|
-
import { introspectTable, getColumnNames } from "./query/schema.js";
|
|
2
|
+
import { introspectTable, getColumnNames, mapOidToDataType } from "./query/schema.js";
|
|
3
3
|
import {
|
|
4
4
|
buildQuery,
|
|
5
5
|
buildCountQuery,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createLiveQueryStore
|
|
12
12
|
} from "./query/live.js";
|
|
13
13
|
import { runMigrations } from "./state/migrations.js";
|
|
14
|
+
import { loadColumnState, saveColumnState } from "./state/views.js";
|
|
14
15
|
import FilterBar from "./components/FilterBar.svelte";
|
|
15
16
|
import SortBar from "./components/SortBar.svelte";
|
|
16
17
|
import GroupBar from "./components/GroupBar.svelte";
|
|
@@ -63,6 +64,9 @@ let resizeStartWidth = 0;
|
|
|
63
64
|
const COL_MIN_WIDTH = 62;
|
|
64
65
|
const COL_MAX_WIDTH = 1e3;
|
|
65
66
|
const COL_DEFAULT_WIDTH = 180;
|
|
67
|
+
let customLabels = {};
|
|
68
|
+
let editingColumnLabel = null;
|
|
69
|
+
let editingLabelValue = "";
|
|
66
70
|
let groupData = [];
|
|
67
71
|
let expandedGroups = /* @__PURE__ */ new Set();
|
|
68
72
|
let totalGroups = 0;
|
|
@@ -110,6 +114,20 @@ $: orderedColumns = (() => {
|
|
|
110
114
|
$: validGrouping = grouping.filter((g) => g.column !== "");
|
|
111
115
|
$: isGrouped = validGrouping.length > 0;
|
|
112
116
|
$: nonGroupedColumns = isGrouped ? orderedColumns.filter((col) => !validGrouping.some((g) => g.column === col.name)) : orderedColumns;
|
|
117
|
+
$: mergedColumnConfigs = (() => {
|
|
118
|
+
const base = config?.columns ?? [];
|
|
119
|
+
if (Object.keys(customLabels).length === 0) return base;
|
|
120
|
+
const configMap = new Map(base.map((c) => [c.name, c]));
|
|
121
|
+
for (const [name, label] of Object.entries(customLabels)) {
|
|
122
|
+
const existing = configMap.get(name);
|
|
123
|
+
if (existing) {
|
|
124
|
+
configMap.set(name, { ...existing, label });
|
|
125
|
+
} else {
|
|
126
|
+
configMap.set(name, { name, label });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...configMap.values()];
|
|
130
|
+
})();
|
|
113
131
|
function groupKey(group) {
|
|
114
132
|
return Object.entries(group.values).map(([col, val]) => `${col}=${val === null || val === void 0 ? "__null__" : String(val)}`).join("::");
|
|
115
133
|
}
|
|
@@ -124,6 +142,14 @@ async function init() {
|
|
|
124
142
|
return;
|
|
125
143
|
}
|
|
126
144
|
}
|
|
145
|
+
if (config?.id) {
|
|
146
|
+
const savedState = await loadColumnState(db, config.id);
|
|
147
|
+
const labels = {};
|
|
148
|
+
for (const col of savedState) {
|
|
149
|
+
if (col.label) labels[col.name] = col.label;
|
|
150
|
+
}
|
|
151
|
+
if (Object.keys(labels).length > 0) customLabels = labels;
|
|
152
|
+
}
|
|
127
153
|
initialized = true;
|
|
128
154
|
await rebuildQuery();
|
|
129
155
|
} catch (err) {
|
|
@@ -171,6 +197,16 @@ async function rebuildQuery() {
|
|
|
171
197
|
store = createLiveQueryStore(db, sql, params);
|
|
172
198
|
store.subscribe((state) => {
|
|
173
199
|
storeState = state;
|
|
200
|
+
if (query && columns.length === 0 && state.fields.length > 0) {
|
|
201
|
+
columns = state.fields.map((f) => ({
|
|
202
|
+
name: f.name,
|
|
203
|
+
dataType: mapOidToDataType(f.dataTypeID),
|
|
204
|
+
postgresType: "unknown",
|
|
205
|
+
nullable: true,
|
|
206
|
+
hasDefault: false
|
|
207
|
+
}));
|
|
208
|
+
allowedColumns = columns.map((c) => c.name);
|
|
209
|
+
}
|
|
174
210
|
});
|
|
175
211
|
}
|
|
176
212
|
function cleanAgg(g) {
|
|
@@ -478,13 +514,12 @@ function clearGlobalSearch() {
|
|
|
478
514
|
}
|
|
479
515
|
function handleCellContextMenu(event, row, col) {
|
|
480
516
|
event.preventDefault();
|
|
481
|
-
const colConfig = config?.columns?.find((c) => c.name === col.name);
|
|
482
517
|
contextMenu = {
|
|
483
518
|
x: event.clientX,
|
|
484
519
|
y: event.clientY,
|
|
485
520
|
value: row[col.name],
|
|
486
521
|
columnName: col.name,
|
|
487
|
-
columnLabel:
|
|
522
|
+
columnLabel: getColumnLabel(col),
|
|
488
523
|
isNumeric: col.dataType === "number"
|
|
489
524
|
};
|
|
490
525
|
}
|
|
@@ -559,9 +594,60 @@ function handleColumnOrderChange(newOrder) {
|
|
|
559
594
|
notifyStateChange();
|
|
560
595
|
}
|
|
561
596
|
function getColumnLabel(col) {
|
|
597
|
+
if (col.name in customLabels) return customLabels[col.name];
|
|
562
598
|
const cfg = config?.columns?.find((c) => c.name === col.name);
|
|
563
599
|
return cfg?.label ?? col.name;
|
|
564
600
|
}
|
|
601
|
+
function startEditingLabel(columnName) {
|
|
602
|
+
const col = columns.find((c) => c.name === columnName);
|
|
603
|
+
if (!col) return;
|
|
604
|
+
editingColumnLabel = columnName;
|
|
605
|
+
editingLabelValue = getColumnLabel(col);
|
|
606
|
+
}
|
|
607
|
+
function commitLabelEdit() {
|
|
608
|
+
if (!editingColumnLabel) return;
|
|
609
|
+
const columnName = editingColumnLabel;
|
|
610
|
+
const newLabel = editingLabelValue.trim();
|
|
611
|
+
const cfg = config?.columns?.find((c) => c.name === columnName);
|
|
612
|
+
const defaultLabel = cfg?.label ?? columnName;
|
|
613
|
+
if (newLabel && newLabel !== defaultLabel) {
|
|
614
|
+
customLabels = { ...customLabels, [columnName]: newLabel };
|
|
615
|
+
} else {
|
|
616
|
+
const { [columnName]: _, ...rest } = customLabels;
|
|
617
|
+
customLabels = rest;
|
|
618
|
+
}
|
|
619
|
+
editingColumnLabel = null;
|
|
620
|
+
editingLabelValue = "";
|
|
621
|
+
persistColumnLabels();
|
|
622
|
+
notifyStateChange();
|
|
623
|
+
}
|
|
624
|
+
function cancelLabelEdit() {
|
|
625
|
+
editingColumnLabel = null;
|
|
626
|
+
editingLabelValue = "";
|
|
627
|
+
}
|
|
628
|
+
function handleLabelKeydown(event) {
|
|
629
|
+
if (event.key === "Enter") {
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
commitLabelEdit();
|
|
632
|
+
} else if (event.key === "Escape") {
|
|
633
|
+
cancelLabelEdit();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function persistColumnLabels() {
|
|
637
|
+
if (!config?.id) return;
|
|
638
|
+
try {
|
|
639
|
+
const colState = columns.map((col, i) => ({
|
|
640
|
+
name: col.name,
|
|
641
|
+
visible: isColumnVisible(col.name),
|
|
642
|
+
width: columnSizing[col.name] ?? void 0,
|
|
643
|
+
position: columnOrder.indexOf(col.name) >= 0 ? columnOrder.indexOf(col.name) : i,
|
|
644
|
+
label: customLabels[col.name] ?? null
|
|
645
|
+
}));
|
|
646
|
+
await saveColumnState(db, config.id, colState);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error("Failed to persist column labels:", err);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
565
651
|
function isColumnVisible(columnName) {
|
|
566
652
|
if (columnName in columnVisibility) {
|
|
567
653
|
return columnVisibility[columnName];
|
|
@@ -744,7 +830,7 @@ onDestroy(() => {
|
|
|
744
830
|
</button>
|
|
745
831
|
<ColumnPicker
|
|
746
832
|
{columns}
|
|
747
|
-
columnConfigs={
|
|
833
|
+
columnConfigs={mergedColumnConfigs}
|
|
748
834
|
{columnVisibility}
|
|
749
835
|
{columnOrder}
|
|
750
836
|
isOpen={showColumnPicker}
|
|
@@ -763,7 +849,7 @@ onDestroy(() => {
|
|
|
763
849
|
{db}
|
|
764
850
|
{table}
|
|
765
851
|
{columns}
|
|
766
|
-
columnConfigs={
|
|
852
|
+
columnConfigs={mergedColumnConfigs}
|
|
767
853
|
{allowedColumns}
|
|
768
854
|
conditions={filters}
|
|
769
855
|
onConditionsChange={handleFiltersChange}
|
|
@@ -780,7 +866,7 @@ onDestroy(() => {
|
|
|
780
866
|
<div class="gridlite-toolbar-group">
|
|
781
867
|
<GroupBar
|
|
782
868
|
{columns}
|
|
783
|
-
columnConfigs={
|
|
869
|
+
columnConfigs={mergedColumnConfigs}
|
|
784
870
|
{grouping}
|
|
785
871
|
onGroupingChange={handleGroupingChange}
|
|
786
872
|
isExpanded={groupExpanded}
|
|
@@ -794,7 +880,7 @@ onDestroy(() => {
|
|
|
794
880
|
<div class="gridlite-toolbar-sort">
|
|
795
881
|
<SortBar
|
|
796
882
|
{columns}
|
|
797
|
-
columnConfigs={
|
|
883
|
+
columnConfigs={mergedColumnConfigs}
|
|
798
884
|
{sorting}
|
|
799
885
|
onSortingChange={handleSortingChange}
|
|
800
886
|
isExpanded={sortExpanded}
|
|
@@ -941,7 +1027,7 @@ onDestroy(() => {
|
|
|
941
1027
|
<div class="gridlite-toolbar-sort">
|
|
942
1028
|
<SortBar
|
|
943
1029
|
{columns}
|
|
944
|
-
columnConfigs={
|
|
1030
|
+
columnConfigs={mergedColumnConfigs}
|
|
945
1031
|
{sorting}
|
|
946
1032
|
onSortingChange={handleSortingChange}
|
|
947
1033
|
isExpanded={sortExpanded}
|
|
@@ -953,7 +1039,7 @@ onDestroy(() => {
|
|
|
953
1039
|
<div class="gridlite-toolbar-group">
|
|
954
1040
|
<GroupBar
|
|
955
1041
|
{columns}
|
|
956
|
-
columnConfigs={
|
|
1042
|
+
columnConfigs={mergedColumnConfigs}
|
|
957
1043
|
{grouping}
|
|
958
1044
|
onGroupingChange={handleGroupingChange}
|
|
959
1045
|
isExpanded={groupExpanded}
|
|
@@ -1052,14 +1138,27 @@ onDestroy(() => {
|
|
|
1052
1138
|
on:dragend={handleDragEnd}
|
|
1053
1139
|
style={features.columnReordering ? 'cursor: grab;' : ''}
|
|
1054
1140
|
>
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
{
|
|
1061
|
-
|
|
1062
|
-
|
|
1141
|
+
{#if editingColumnLabel === col.name}
|
|
1142
|
+
<!-- svelte-ignore a11y-autofocus -->
|
|
1143
|
+
<input
|
|
1144
|
+
class="gridlite-th-label-input"
|
|
1145
|
+
type="text"
|
|
1146
|
+
bind:value={editingLabelValue}
|
|
1147
|
+
on:blur={commitLabelEdit}
|
|
1148
|
+
on:keydown={handleLabelKeydown}
|
|
1149
|
+
on:click|stopPropagation
|
|
1150
|
+
autofocus
|
|
1151
|
+
/>
|
|
1152
|
+
{:else}
|
|
1153
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
1154
|
+
<span
|
|
1155
|
+
class="gridlite-th-label"
|
|
1156
|
+
on:dblclick|stopPropagation={() => startEditingLabel(col.name)}
|
|
1157
|
+
title="Double-click to rename"
|
|
1158
|
+
>
|
|
1159
|
+
{getColumnLabel(col)}
|
|
1160
|
+
</span>
|
|
1161
|
+
{/if}
|
|
1063
1162
|
{#if table}
|
|
1064
1163
|
<button
|
|
1065
1164
|
class="gridlite-th-menu-btn"
|
|
@@ -1285,7 +1384,7 @@ onDestroy(() => {
|
|
|
1285
1384
|
<div class="gridlite-aggrid-sidebar-header">Columns</div>
|
|
1286
1385
|
<ColumnPicker
|
|
1287
1386
|
{columns}
|
|
1288
|
-
columnConfigs={
|
|
1387
|
+
columnConfigs={mergedColumnConfigs}
|
|
1289
1388
|
{columnVisibility}
|
|
1290
1389
|
{columnOrder}
|
|
1291
1390
|
isOpen={true}
|
|
@@ -1303,7 +1402,7 @@ onDestroy(() => {
|
|
|
1303
1402
|
{db}
|
|
1304
1403
|
table={table ?? ''}
|
|
1305
1404
|
{columns}
|
|
1306
|
-
columnConfigs={
|
|
1405
|
+
columnConfigs={mergedColumnConfigs}
|
|
1307
1406
|
{allowedColumns}
|
|
1308
1407
|
conditions={filters}
|
|
1309
1408
|
onConditionsChange={handleFiltersChange}
|
|
@@ -1350,17 +1449,10 @@ onDestroy(() => {
|
|
|
1350
1449
|
<dl class="gridlite-row-detail">
|
|
1351
1450
|
{#each orderedColumns as col}
|
|
1352
1451
|
<div class="gridlite-row-detail-field">
|
|
1353
|
-
<dt>
|
|
1354
|
-
{#if config?.columns}
|
|
1355
|
-
{@const colConfig = config.columns.find((c) => c.name === col.name)}
|
|
1356
|
-
{colConfig?.label ?? col.name}
|
|
1357
|
-
{:else}
|
|
1358
|
-
{col.name}
|
|
1359
|
-
{/if}
|
|
1360
|
-
</dt>
|
|
1452
|
+
<dt>{getColumnLabel(col)}</dt>
|
|
1361
1453
|
<dd>
|
|
1362
|
-
{#if
|
|
1363
|
-
{@const colConfig =
|
|
1454
|
+
{#if mergedColumnConfigs.length > 0}
|
|
1455
|
+
{@const colConfig = mergedColumnConfigs.find((c) => c.name === col.name)}
|
|
1364
1456
|
{#if colConfig?.format}
|
|
1365
1457
|
{colConfig.format(rowDetailRow[col.name])}
|
|
1366
1458
|
{:else}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
|
|
|
11
11
|
export type { GridLiteProps, GridConfig, GridFeatures, GridState, ColumnConfig, ColumnDataType, ColumnMetadata, FilterCondition, FilterOperator, FilterLogic, SortConfig, GroupConfig, AggregationConfig, AggregateFunction, ViewPreset, ClassNameMap, RowHeight, ColumnSpacing, ParameterizedQuery, ToolbarLayout, } from "./types.js";
|
|
12
12
|
export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
13
13
|
export type { QueryOptions, GroupSummaryOptions, GroupDetailOptions, } from "./query/builder.js";
|
|
14
|
-
export { mapPostgresType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
14
|
+
export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
15
15
|
export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
|
|
16
16
|
export type { LiveQueryState, LiveQueryStore, PGliteWithLive, } from "./query/live.js";
|
|
17
17
|
export { runMigrations, getLatestVersion, isMigrated, } from "./state/migrations.js";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
|
|
|
14
14
|
// Query builder
|
|
15
15
|
export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
16
16
|
// Schema introspection
|
|
17
|
-
export { mapPostgresType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
17
|
+
export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
18
18
|
// Live query store
|
|
19
19
|
export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
|
|
20
20
|
// State persistence — migrations
|
package/dist/query/schema.d.ts
CHANGED
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* and nullability for a given table. Maps Postgres data types to
|
|
6
6
|
* GridLite's ColumnDataType for filter operator selection and UI rendering.
|
|
7
7
|
*/
|
|
8
|
-
import type { PGlite } from
|
|
9
|
-
import type { ColumnDataType, ColumnMetadata } from
|
|
8
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
9
|
+
import type { ColumnDataType, ColumnMetadata } from "../types.js";
|
|
10
10
|
/**
|
|
11
11
|
* Map a Postgres data_type string (from information_schema.columns)
|
|
12
12
|
* to a GridLite ColumnDataType.
|
|
13
13
|
*/
|
|
14
14
|
export declare function mapPostgresType(postgresType: string): ColumnDataType;
|
|
15
|
+
/**
|
|
16
|
+
* Map a Postgres type OID to a GridLite ColumnDataType.
|
|
17
|
+
* Returns 'text' for unrecognized OIDs.
|
|
18
|
+
*/
|
|
19
|
+
export declare function mapOidToDataType(oid: number): ColumnDataType;
|
|
15
20
|
/**
|
|
16
21
|
* Introspect a table's schema using information_schema.columns.
|
|
17
22
|
*
|
package/dist/query/schema.js
CHANGED
|
@@ -13,34 +13,69 @@
|
|
|
13
13
|
export function mapPostgresType(postgresType) {
|
|
14
14
|
const t = postgresType.toLowerCase();
|
|
15
15
|
// Numeric types
|
|
16
|
-
if (t ===
|
|
17
|
-
t ===
|
|
18
|
-
t ===
|
|
19
|
-
t ===
|
|
20
|
-
t ===
|
|
21
|
-
t ===
|
|
22
|
-
t ===
|
|
23
|
-
t ===
|
|
24
|
-
t ===
|
|
25
|
-
t ===
|
|
26
|
-
t ===
|
|
27
|
-
return
|
|
16
|
+
if (t === "integer" ||
|
|
17
|
+
t === "bigint" ||
|
|
18
|
+
t === "smallint" ||
|
|
19
|
+
t === "numeric" ||
|
|
20
|
+
t === "decimal" ||
|
|
21
|
+
t === "real" ||
|
|
22
|
+
t === "double precision" ||
|
|
23
|
+
t === "serial" ||
|
|
24
|
+
t === "bigserial" ||
|
|
25
|
+
t === "smallserial" ||
|
|
26
|
+
t === "money") {
|
|
27
|
+
return "number";
|
|
28
28
|
}
|
|
29
29
|
// Date/time types
|
|
30
|
-
if (t ===
|
|
31
|
-
t ===
|
|
32
|
-
t ===
|
|
33
|
-
t ===
|
|
34
|
-
t ===
|
|
35
|
-
t ===
|
|
36
|
-
return
|
|
30
|
+
if (t === "date" ||
|
|
31
|
+
t === "timestamp without time zone" ||
|
|
32
|
+
t === "timestamp with time zone" ||
|
|
33
|
+
t === "time without time zone" ||
|
|
34
|
+
t === "time with time zone" ||
|
|
35
|
+
t === "interval") {
|
|
36
|
+
return "date";
|
|
37
37
|
}
|
|
38
38
|
// Boolean
|
|
39
|
-
if (t ===
|
|
40
|
-
return
|
|
39
|
+
if (t === "boolean") {
|
|
40
|
+
return "boolean";
|
|
41
41
|
}
|
|
42
42
|
// Everything else is text (varchar, char, text, json, jsonb, uuid, etc.)
|
|
43
|
-
return
|
|
43
|
+
return "text";
|
|
44
|
+
}
|
|
45
|
+
// ─── OID → ColumnDataType Mapping ───────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Map a Postgres type OID (from query result fields) to a GridLite ColumnDataType.
|
|
48
|
+
* Used when introspecting columns from raw query results rather than information_schema.
|
|
49
|
+
*
|
|
50
|
+
* Common OIDs from the Postgres catalog (pg_type):
|
|
51
|
+
* https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
|
|
52
|
+
*/
|
|
53
|
+
const OID_MAP = {
|
|
54
|
+
// Boolean
|
|
55
|
+
16: "boolean",
|
|
56
|
+
// Numeric
|
|
57
|
+
20: "number", // int8 / bigint
|
|
58
|
+
21: "number", // int2 / smallint
|
|
59
|
+
23: "number", // int4 / integer
|
|
60
|
+
26: "number", // oid
|
|
61
|
+
700: "number", // float4 / real
|
|
62
|
+
701: "number", // float8 / double precision
|
|
63
|
+
790: "number", // money
|
|
64
|
+
1700: "number", // numeric / decimal
|
|
65
|
+
// Date/time
|
|
66
|
+
1082: "date", // date
|
|
67
|
+
1083: "date", // time
|
|
68
|
+
1114: "date", // timestamp without time zone
|
|
69
|
+
1184: "date", // timestamp with time zone
|
|
70
|
+
1186: "date", // interval
|
|
71
|
+
1266: "date", // time with time zone
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Map a Postgres type OID to a GridLite ColumnDataType.
|
|
75
|
+
* Returns 'text' for unrecognized OIDs.
|
|
76
|
+
*/
|
|
77
|
+
export function mapOidToDataType(oid) {
|
|
78
|
+
return OID_MAP[oid] ?? "text";
|
|
44
79
|
}
|
|
45
80
|
/**
|
|
46
81
|
* Introspect a table's schema using information_schema.columns.
|
|
@@ -52,7 +87,7 @@ export function mapPostgresType(postgresType) {
|
|
|
52
87
|
* @param tableName - The table to introspect
|
|
53
88
|
* @param schema - The schema to search (defaults to 'public')
|
|
54
89
|
*/
|
|
55
|
-
export async function introspectTable(db, tableName, schema =
|
|
90
|
+
export async function introspectTable(db, tableName, schema = "public") {
|
|
56
91
|
const result = await db.query(`SELECT column_name, data_type, is_nullable, column_default
|
|
57
92
|
FROM information_schema.columns
|
|
58
93
|
WHERE table_name = $1 AND table_schema = $2
|
|
@@ -61,15 +96,15 @@ export async function introspectTable(db, tableName, schema = 'public') {
|
|
|
61
96
|
name: row.column_name,
|
|
62
97
|
dataType: mapPostgresType(row.data_type),
|
|
63
98
|
postgresType: row.data_type,
|
|
64
|
-
nullable: row.is_nullable ===
|
|
65
|
-
hasDefault: row.column_default !== null
|
|
99
|
+
nullable: row.is_nullable === "YES",
|
|
100
|
+
hasDefault: row.column_default !== null,
|
|
66
101
|
}));
|
|
67
102
|
}
|
|
68
103
|
/**
|
|
69
104
|
* Get the list of column names for a table.
|
|
70
105
|
* Useful for the query builder's allowedColumns parameter.
|
|
71
106
|
*/
|
|
72
|
-
export async function getColumnNames(db, tableName, schema =
|
|
107
|
+
export async function getColumnNames(db, tableName, schema = "public") {
|
|
73
108
|
const columns = await introspectTable(db, tableName, schema);
|
|
74
109
|
return columns.map((c) => c.name);
|
|
75
110
|
}
|
package/dist/state/migrations.js
CHANGED
|
@@ -51,6 +51,13 @@ const MIGRATIONS = [
|
|
|
51
51
|
ON _gridlite_column_state (grid_id);
|
|
52
52
|
`,
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
version: 2,
|
|
56
|
+
description: "Add label column to column_state for user-editable column names",
|
|
57
|
+
sql: `
|
|
58
|
+
ALTER TABLE _gridlite_column_state ADD COLUMN IF NOT EXISTS label TEXT;
|
|
59
|
+
`,
|
|
60
|
+
},
|
|
54
61
|
];
|
|
55
62
|
// ─── Migration Runner ───────────────────────────────────────────────────────
|
|
56
63
|
/**
|
package/dist/state/views.d.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Requires `runMigrations()` to have been called first.
|
|
9
9
|
*/
|
|
10
|
-
import type { PGlite } from
|
|
11
|
-
import type { ViewPreset } from
|
|
10
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
11
|
+
import type { ViewPreset } from "../types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Save a view configuration. Creates or updates (upserts).
|
|
14
14
|
*/
|
|
@@ -42,6 +42,7 @@ export declare function saveColumnState(db: PGlite, gridId: string, columns: {
|
|
|
42
42
|
visible?: boolean;
|
|
43
43
|
width?: number;
|
|
44
44
|
position?: number;
|
|
45
|
+
label?: string | null;
|
|
45
46
|
}[], viewId?: string): Promise<void>;
|
|
46
47
|
/**
|
|
47
48
|
* Load column state for a grid (optionally scoped to a view).
|
|
@@ -51,4 +52,5 @@ export declare function loadColumnState(db: PGlite, gridId: string, viewId?: str
|
|
|
51
52
|
visible: boolean;
|
|
52
53
|
width: number | null;
|
|
53
54
|
position: number | null;
|
|
55
|
+
label: string | null;
|
|
54
56
|
}[]>;
|
package/dist/state/views.js
CHANGED
|
@@ -30,11 +30,11 @@ export async function saveView(db, gridId, view) {
|
|
|
30
30
|
view.name,
|
|
31
31
|
view.description ?? null,
|
|
32
32
|
JSON.stringify(view.filters ?? []),
|
|
33
|
-
view.filterLogic ??
|
|
33
|
+
view.filterLogic ?? "and",
|
|
34
34
|
JSON.stringify(view.sorting ?? []),
|
|
35
35
|
JSON.stringify(view.grouping ?? []),
|
|
36
36
|
JSON.stringify(view.columnVisibility ?? {}),
|
|
37
|
-
JSON.stringify(view.columnOrder ?? [])
|
|
37
|
+
JSON.stringify(view.columnOrder ?? []),
|
|
38
38
|
]);
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
@@ -76,7 +76,9 @@ export async function setDefaultView(db, gridId, viewId) {
|
|
|
76
76
|
*/
|
|
77
77
|
export async function deleteView(db, viewId) {
|
|
78
78
|
// Also clean up associated column state
|
|
79
|
-
await db.query(`DELETE FROM _gridlite_column_state WHERE view_id = $1`, [
|
|
79
|
+
await db.query(`DELETE FROM _gridlite_column_state WHERE view_id = $1`, [
|
|
80
|
+
viewId,
|
|
81
|
+
]);
|
|
80
82
|
await db.query(`DELETE FROM _gridlite_views WHERE id = $1`, [viewId]);
|
|
81
83
|
}
|
|
82
84
|
// ─── Column State CRUD ──────────────────────────────────────────────────────
|
|
@@ -84,26 +86,27 @@ export async function deleteView(db, viewId) {
|
|
|
84
86
|
* Save column state for a grid (optionally scoped to a view).
|
|
85
87
|
* Replaces all existing column state for the given grid+view.
|
|
86
88
|
*/
|
|
87
|
-
export async function saveColumnState(db, gridId, columns, viewId =
|
|
89
|
+
export async function saveColumnState(db, gridId, columns, viewId = "__default__") {
|
|
88
90
|
// Clear existing state for this grid+view
|
|
89
91
|
await db.query(`DELETE FROM _gridlite_column_state WHERE grid_id = $1 AND view_id = $2`, [gridId, viewId]);
|
|
90
92
|
// Insert new state
|
|
91
93
|
for (const col of columns) {
|
|
92
|
-
await db.query(`INSERT INTO _gridlite_column_state (grid_id, view_id, column_name, visible, width, position)
|
|
93
|
-
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
94
|
+
await db.query(`INSERT INTO _gridlite_column_state (grid_id, view_id, column_name, visible, width, position, label)
|
|
95
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
|
|
94
96
|
gridId,
|
|
95
97
|
viewId,
|
|
96
98
|
col.name,
|
|
97
99
|
col.visible ?? true,
|
|
98
100
|
col.width ?? null,
|
|
99
|
-
col.position ?? null
|
|
101
|
+
col.position ?? null,
|
|
102
|
+
col.label ?? null,
|
|
100
103
|
]);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
/**
|
|
104
107
|
* Load column state for a grid (optionally scoped to a view).
|
|
105
108
|
*/
|
|
106
|
-
export async function loadColumnState(db, gridId, viewId =
|
|
109
|
+
export async function loadColumnState(db, gridId, viewId = "__default__") {
|
|
107
110
|
const result = await db.query(`SELECT * FROM _gridlite_column_state
|
|
108
111
|
WHERE grid_id = $1 AND view_id = $2
|
|
109
112
|
ORDER BY position NULLS LAST, column_name`, [gridId, viewId]);
|
|
@@ -111,7 +114,8 @@ export async function loadColumnState(db, gridId, viewId = '__default__') {
|
|
|
111
114
|
name: row.column_name,
|
|
112
115
|
visible: row.visible,
|
|
113
116
|
width: row.width,
|
|
114
|
-
position: row.position
|
|
117
|
+
position: row.position,
|
|
118
|
+
label: row.label,
|
|
115
119
|
}));
|
|
116
120
|
}
|
|
117
121
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
@@ -125,6 +129,6 @@ function rowToViewPreset(row) {
|
|
|
125
129
|
sorting: row.sorting,
|
|
126
130
|
grouping: row.grouping,
|
|
127
131
|
columnVisibility: row.column_visibility,
|
|
128
|
-
columnOrder: row.column_order
|
|
132
|
+
columnOrder: row.column_order,
|
|
129
133
|
};
|
|
130
134
|
}
|
package/dist/styles/gridlite.css
CHANGED
|
@@ -752,6 +752,21 @@
|
|
|
752
752
|
|
|
753
753
|
.gridlite-th-label {
|
|
754
754
|
flex: 1;
|
|
755
|
+
cursor: default;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.gridlite-th-label-input {
|
|
759
|
+
flex: 1;
|
|
760
|
+
font: inherit;
|
|
761
|
+
font-weight: inherit;
|
|
762
|
+
font-size: inherit;
|
|
763
|
+
padding: 0 2px;
|
|
764
|
+
margin: -1px 0;
|
|
765
|
+
border: 1px solid #3b82f6;
|
|
766
|
+
border-radius: 3px;
|
|
767
|
+
outline: none;
|
|
768
|
+
background: #fff;
|
|
769
|
+
min-width: 40px;
|
|
755
770
|
}
|
|
756
771
|
|
|
757
772
|
.gridlite-th-menu-btn {
|