@pythia-software/query-table-react 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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +715 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 query-table contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @pythia-software/query-table-react
|
|
2
|
+
|
|
3
|
+
Headless React hooks for query-table: query state, fetching, URL/storage sync,
|
|
4
|
+
selection, saved queries, aggregations, and column interactions.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @pythia-software/query-table-core @pythia-software/query-table-react react
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Example
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { useQueryTable } from "@pythia-software/query-table-react";
|
|
16
|
+
|
|
17
|
+
function Orders({ schema, rows }) {
|
|
18
|
+
const table = useQueryTable({ schema, clientRows: rows });
|
|
19
|
+
return <p>{table.total ?? 0} orders</p>;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Privacy defaults
|
|
24
|
+
|
|
25
|
+
URL synchronization and durable browser storage are disabled by default. Set
|
|
26
|
+
`syncUrl: true` only when raw filter values are safe to place in browser
|
|
27
|
+
history, referrers, logs, analytics, bookmarks, and screenshots. Pass
|
|
28
|
+
`localStorageAdapter()` explicitly only when cleartext persistence on the device
|
|
29
|
+
is appropriate; sensitive applications should use an access-controlled server
|
|
30
|
+
`StorageAdapter`.
|
|
31
|
+
|
|
32
|
+
See the [repository README](https://github.com/Pythia-Software/query-table#readme)
|
|
33
|
+
for transport and schema examples.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { RowId, SelectColumn, FieldDef, QueryState, FieldSchema, SavedQuery, StorageAdapter, AggOp, AggregationClause, AggregationResult, WhereClause, DistinctValuesResult, OrderByClause, Transport } from '@pythia-software/query-table-core';
|
|
2
|
+
|
|
3
|
+
interface SelectionApi {
|
|
4
|
+
selected: Set<RowId>;
|
|
5
|
+
isSelected: (id: RowId) => boolean;
|
|
6
|
+
/** Toggle one row. With `shiftKey`, extend from the last-toggled anchor to `id`
|
|
7
|
+
* across the *currently displayed* order (falls back to a plain toggle if the
|
|
8
|
+
* anchor row is no longer present). */
|
|
9
|
+
toggle: (id: RowId, shiftKey?: boolean) => void;
|
|
10
|
+
/** Select / clear every row currently on the page. */
|
|
11
|
+
setPage: (ids: RowId[], selected: boolean) => void;
|
|
12
|
+
/** Header checkbox tri-state for the current page. */
|
|
13
|
+
pageState: (pageIds: RowId[]) => "none" | "some" | "all";
|
|
14
|
+
clear: () => void;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
/** `displayedIds` is the page's id list in display order; the hook uses it to
|
|
18
|
+
* resolve shift-ranges and header tri-state against what the user sees. */
|
|
19
|
+
declare function useSelection(displayedIds: RowId[]): SelectionApi;
|
|
20
|
+
|
|
21
|
+
interface SelectApi<Row> {
|
|
22
|
+
/** Ordered visible columns (resolved: schema defaults when query.select is empty). */
|
|
23
|
+
visible: SelectColumn[];
|
|
24
|
+
/** The corresponding FieldDefs, same order. */
|
|
25
|
+
fields: FieldDef<Row>[];
|
|
26
|
+
/** Selectable fields not currently shown (for the "add column" picker). */
|
|
27
|
+
hidden: FieldDef<Row>[];
|
|
28
|
+
show: (field: string) => void;
|
|
29
|
+
hide: (field: string) => void;
|
|
30
|
+
/** Reorder via drag-drop: move `field` to `toIndex` in the visible order. */
|
|
31
|
+
move: (field: string, toIndex: number) => void;
|
|
32
|
+
/** Set a column's pixel width. */
|
|
33
|
+
setWidth: (field: string, width: number) => void;
|
|
34
|
+
/** Reset to the schema's default columns (clears query.select). */
|
|
35
|
+
reset: () => void;
|
|
36
|
+
}
|
|
37
|
+
type SetQuery = (next: QueryState | ((prev: QueryState) => QueryState)) => void;
|
|
38
|
+
declare function useSelect<Row>(query: QueryState, setQuery: SetQuery, schema: FieldSchema<Row>): SelectApi<Row>;
|
|
39
|
+
|
|
40
|
+
interface ColumnDragApi {
|
|
41
|
+
/** Field name currently being dragged, or null when idle. */
|
|
42
|
+
source: string | null;
|
|
43
|
+
/** Insertion index within the order with `source` removed (0..n; n = append). */
|
|
44
|
+
overIndex: number | null;
|
|
45
|
+
/** True while a column drag is in progress. */
|
|
46
|
+
active: boolean;
|
|
47
|
+
/** Begin dragging `field`, initially previewed at `index` (its own slot). */
|
|
48
|
+
start: (field: string, index: number) => void;
|
|
49
|
+
/** Update the previewed insertion index (no-op if unchanged). */
|
|
50
|
+
over: (index: number) => void;
|
|
51
|
+
/** End the drag (on drop or cancel). */
|
|
52
|
+
end: () => void;
|
|
53
|
+
/** The live order to render while dragging: `source` moved to `overIndex`.
|
|
54
|
+
* Returns `order` unchanged when idle, or when `source` isn't in `order`. */
|
|
55
|
+
preview: (order: string[]) => string[];
|
|
56
|
+
}
|
|
57
|
+
declare function useColumnDrag(): ColumnDragApi;
|
|
58
|
+
|
|
59
|
+
interface SavedQueriesApi {
|
|
60
|
+
items: SavedQuery[];
|
|
61
|
+
loading: boolean;
|
|
62
|
+
/** Saved query id used as the default view when no query is specified. */
|
|
63
|
+
defaultId: string | null;
|
|
64
|
+
/** Persist the current query under a name. Rejects on duplicate names. */
|
|
65
|
+
save: (name: string) => Promise<SavedQuery>;
|
|
66
|
+
/** Load a saved query into the live controller. */
|
|
67
|
+
load: (id: string) => void;
|
|
68
|
+
/** Mark a saved query as the default view. No-op when storage lacks support. */
|
|
69
|
+
setDefault: (id: string) => Promise<void>;
|
|
70
|
+
/** Clear the saved-query default view. */
|
|
71
|
+
clearDefault: () => Promise<void>;
|
|
72
|
+
remove: (id: string) => Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
declare function useSavedQueries(key: string, currentQuery: QueryState, applyQueryState: (q: QueryState) => void, storage: StorageAdapter,
|
|
75
|
+
/** Injected clock so saves are deterministic/testable. */
|
|
76
|
+
now: () => number): SavedQueriesApi;
|
|
77
|
+
|
|
78
|
+
interface UseQueryTableOptions<Row> {
|
|
79
|
+
schema: FieldSchema<Row>;
|
|
80
|
+
/** Data access. Omit for a purely client-side table driven by `clientRows`. */
|
|
81
|
+
transport?: Transport<Row>;
|
|
82
|
+
/** All rows, for client-side mode (applyQuery runs locally over these). */
|
|
83
|
+
clientRows?: Row[];
|
|
84
|
+
/** Saved/last-query persistence. Defaults to a non-durable in-memory adapter.
|
|
85
|
+
* Pass localStorageAdapter() explicitly only when filter values are safe to
|
|
86
|
+
* retain as cleartext JSON on the device. */
|
|
87
|
+
storage?: StorageAdapter;
|
|
88
|
+
/** Seed query, normalized and resource-bounded before use. When omitted the
|
|
89
|
+
* hook resolves: opted-in URL → storage.loadLast → schema defaults. */
|
|
90
|
+
initialQuery?: QueryState;
|
|
91
|
+
/** Mirror QueryState to `?q=` via history.replaceState. Default false because
|
|
92
|
+
* base64url is encoding, not encryption: filters will appear in browser
|
|
93
|
+
* history, referrers, logs, analytics, and screenshots when enabled. */
|
|
94
|
+
syncUrl?: boolean;
|
|
95
|
+
/** Debounce (ms) between a query change and the fetch it triggers. Default 200. */
|
|
96
|
+
debounceMs?: number;
|
|
97
|
+
/** Injected clock for deterministic saved-query timestamps. Default Date.now. */
|
|
98
|
+
now?: () => number;
|
|
99
|
+
/** Side-effect to run on every refresh — both manual `api.refresh()` and each
|
|
100
|
+
* auto-refresh tick. Fires *in addition to* the internal `setNonce` re-query,
|
|
101
|
+
* so manual and auto stay identical. Use it in client mode (no transport) to
|
|
102
|
+
* bridge a UI refresh to your own server fetch without monkey-patching the
|
|
103
|
+
* returned `api`. Always read through a ref, so supplying it never tears down
|
|
104
|
+
* and recreates the auto-refresh interval. */
|
|
105
|
+
onRefresh?: () => void;
|
|
106
|
+
}
|
|
107
|
+
interface AutoRefreshConfig {
|
|
108
|
+
frequencyMs: number;
|
|
109
|
+
turnOffAfterMs: number;
|
|
110
|
+
}
|
|
111
|
+
interface AutoRefreshStatus extends AutoRefreshConfig {
|
|
112
|
+
startedAt: number;
|
|
113
|
+
stopsAt: number;
|
|
114
|
+
pollCount: number;
|
|
115
|
+
}
|
|
116
|
+
interface AutoRefreshApi {
|
|
117
|
+
status: AutoRefreshStatus | null;
|
|
118
|
+
start: (config: AutoRefreshConfig) => void;
|
|
119
|
+
stop: () => void;
|
|
120
|
+
}
|
|
121
|
+
/** A patch for one metric. `field`/`label` accept an explicit `undefined` to
|
|
122
|
+
* CLEAR them (e.g. switching to an op whose measure no longer applies); an
|
|
123
|
+
* absent key leaves the existing value untouched. */
|
|
124
|
+
interface AggregationPatch {
|
|
125
|
+
op?: AggOp;
|
|
126
|
+
field?: string | undefined;
|
|
127
|
+
groupBy?: string[];
|
|
128
|
+
label?: string | undefined;
|
|
129
|
+
}
|
|
130
|
+
interface AggregationsApi {
|
|
131
|
+
/** The metric specs currently in the query. */
|
|
132
|
+
clauses: AggregationClause[];
|
|
133
|
+
/** Server (or client-mirror) results, one entry per clause; null when none. */
|
|
134
|
+
results: AggregationResult | null;
|
|
135
|
+
loading: boolean;
|
|
136
|
+
error: Error | null;
|
|
137
|
+
/** Append a metric. Defaults to "count all rows"; pass a partial to override.
|
|
138
|
+
* Returns the generated id. */
|
|
139
|
+
add: (partial?: Partial<Omit<AggregationClause, "id">>) => string;
|
|
140
|
+
/** Patch a metric by id. */
|
|
141
|
+
update: (id: string, patch: AggregationPatch) => void;
|
|
142
|
+
/** Reorder: move the metric `id` to `toIndex` (an index in the list with the
|
|
143
|
+
* metric removed, so a drag preview's slot maps straight to the result). */
|
|
144
|
+
move: (id: string, toIndex: number) => void;
|
|
145
|
+
/** Remove a metric by id. */
|
|
146
|
+
remove: (id: string) => void;
|
|
147
|
+
/** Remove all metrics. */
|
|
148
|
+
clear: () => void;
|
|
149
|
+
}
|
|
150
|
+
interface QueryTableApi<Row> {
|
|
151
|
+
query: QueryState;
|
|
152
|
+
/** Schema defaults used for reset controls. */
|
|
153
|
+
defaults: QueryState;
|
|
154
|
+
setQuery: (next: QueryState | ((prev: QueryState) => QueryState)) => void;
|
|
155
|
+
/** Can move back to a previously applied local query state. */
|
|
156
|
+
canUndo: boolean;
|
|
157
|
+
/** Can move forward to a previously undone local query state. */
|
|
158
|
+
canRedo: boolean;
|
|
159
|
+
/** Move backward through local, unsaved query history. */
|
|
160
|
+
undo: () => void;
|
|
161
|
+
/** Move forward through local, unsaved query history. */
|
|
162
|
+
redo: () => void;
|
|
163
|
+
rows: Row[];
|
|
164
|
+
total: number | null;
|
|
165
|
+
loading: boolean;
|
|
166
|
+
error: Error | null;
|
|
167
|
+
refresh: () => void;
|
|
168
|
+
autoRefresh: AutoRefreshApi;
|
|
169
|
+
refreshRow: (id: RowId) => Promise<void>;
|
|
170
|
+
aggregations: AggregationsApi;
|
|
171
|
+
addFilter: (clause: WhereClause) => void;
|
|
172
|
+
updateFilter: (index: number, clause: WhereClause) => void;
|
|
173
|
+
removeFilter: (index: number) => void;
|
|
174
|
+
clearFilters: () => void;
|
|
175
|
+
/** Reset the complete query state back to schema defaults. */
|
|
176
|
+
resetAll: () => void;
|
|
177
|
+
/** Keystroke-driven filter-value autocomplete (Transport.fetchDistinctValues). */
|
|
178
|
+
filterValues: (field: string, search: string) => Promise<DistinctValuesResult>;
|
|
179
|
+
toggleSort: (field: string, additive?: boolean) => void;
|
|
180
|
+
setSort: (orderBy: OrderByClause[]) => void;
|
|
181
|
+
select: SelectApi<Row>;
|
|
182
|
+
setLimit: (limit: number) => void;
|
|
183
|
+
setOffset: (offset: number) => void;
|
|
184
|
+
nextPage: () => void;
|
|
185
|
+
prevPage: () => void;
|
|
186
|
+
selection: SelectionApi;
|
|
187
|
+
saved: SavedQueriesApi;
|
|
188
|
+
/** Shared, transient column-reorder drag state (table headers + select chips). */
|
|
189
|
+
columnDrag: ColumnDragApi;
|
|
190
|
+
visibleFields: FieldDef<Row>[];
|
|
191
|
+
rowId: (row: Row) => RowId | null;
|
|
192
|
+
}
|
|
193
|
+
declare function useQueryTable<Row>(opts: UseQueryTableOptions<Row>): QueryTableApi<Row>;
|
|
194
|
+
|
|
195
|
+
interface AggregationsApiState {
|
|
196
|
+
results: AggregationResult | null;
|
|
197
|
+
loading: boolean;
|
|
198
|
+
error: Error | null;
|
|
199
|
+
}
|
|
200
|
+
declare function useAggregations<Row>(query: QueryState, schema: FieldSchema<Row>, transport: Transport<Row> | undefined, clientRows: Row[] | undefined, debounceMs: number, nonce: number): AggregationsApiState;
|
|
201
|
+
|
|
202
|
+
export { type AggregationPatch, type AggregationsApi, type AggregationsApiState, type AutoRefreshApi, type AutoRefreshConfig, type AutoRefreshStatus, type ColumnDragApi, type QueryTableApi, type SavedQueriesApi, type SelectApi, type SelectionApi, type UseQueryTableOptions, useAggregations, useColumnDrag, useQueryTable, useSavedQueries, useSelect, useSelection };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
// src/useQueryTable.ts
|
|
2
|
+
import { useCallback as useCallback5, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_QUERY,
|
|
5
|
+
applyQuery,
|
|
6
|
+
decodeQuery,
|
|
7
|
+
encodeQuery,
|
|
8
|
+
memoryStorageAdapter,
|
|
9
|
+
normalizeQueryState,
|
|
10
|
+
readFieldValue,
|
|
11
|
+
selectedFields as selectedFields2,
|
|
12
|
+
toServerQuery,
|
|
13
|
+
queriesEqual
|
|
14
|
+
} from "@pythia-software/query-table-core";
|
|
15
|
+
|
|
16
|
+
// src/useAggregations.ts
|
|
17
|
+
import { useEffect, useMemo, useState } from "react";
|
|
18
|
+
import { applyAggregations, toAggregationQuery } from "@pythia-software/query-table-core";
|
|
19
|
+
var EMPTY_RESULT = { metrics: [] };
|
|
20
|
+
function useAggregations(query, schema, transport, clientRows, debounceMs, nonce) {
|
|
21
|
+
const [results, setResults] = useState(null);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const key = useMemo(
|
|
25
|
+
() => JSON.stringify({ where: query.where, aggregations: query.aggregations ?? [] }),
|
|
26
|
+
[query.where, query.aggregations]
|
|
27
|
+
);
|
|
28
|
+
const hasAggregations = (query.aggregations?.length ?? 0) > 0;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!hasAggregations) {
|
|
31
|
+
setResults(null);
|
|
32
|
+
setError(null);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const ac = new AbortController();
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
const run = async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
let res;
|
|
43
|
+
if (transport?.fetchAggregations) {
|
|
44
|
+
res = await transport.fetchAggregations(toAggregationQuery(query, schema), ac.signal);
|
|
45
|
+
} else if (clientRows) {
|
|
46
|
+
res = applyAggregations(clientRows, query, schema);
|
|
47
|
+
} else {
|
|
48
|
+
res = EMPTY_RESULT;
|
|
49
|
+
}
|
|
50
|
+
if (!cancelled) setResults(res);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (!cancelled && !ac.signal.aborted) setError(e);
|
|
53
|
+
} finally {
|
|
54
|
+
if (!cancelled) setLoading(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const t = setTimeout(run, debounceMs);
|
|
58
|
+
return () => {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
clearTimeout(t);
|
|
61
|
+
ac.abort();
|
|
62
|
+
};
|
|
63
|
+
}, [key, hasAggregations, transport, clientRows, schema, debounceMs, nonce]);
|
|
64
|
+
return { results, loading, error };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/useSelection.ts
|
|
68
|
+
import { useCallback, useMemo as useMemo2, useRef, useState as useState2 } from "react";
|
|
69
|
+
function useSelection(displayedIds) {
|
|
70
|
+
const [selected, setSelected] = useState2(() => /* @__PURE__ */ new Set());
|
|
71
|
+
const anchor = useRef(null);
|
|
72
|
+
const toggle = useCallback(
|
|
73
|
+
(id, shiftKey = false) => {
|
|
74
|
+
setSelected((prev) => {
|
|
75
|
+
const next = new Set(prev);
|
|
76
|
+
const a = anchor.current;
|
|
77
|
+
if (shiftKey && a != null) {
|
|
78
|
+
const i = displayedIds.indexOf(a);
|
|
79
|
+
const j = displayedIds.indexOf(id);
|
|
80
|
+
if (i !== -1 && j !== -1) {
|
|
81
|
+
const [lo, hi] = i <= j ? [i, j] : [j, i];
|
|
82
|
+
const span = displayedIds.slice(lo, hi + 1);
|
|
83
|
+
const allChecked = span.every((rowId) => prev.has(rowId));
|
|
84
|
+
for (const rowId of span) {
|
|
85
|
+
if (allChecked) next.delete(rowId);
|
|
86
|
+
else next.add(rowId);
|
|
87
|
+
}
|
|
88
|
+
anchor.current = id;
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (next.has(id)) next.delete(id);
|
|
93
|
+
else next.add(id);
|
|
94
|
+
anchor.current = id;
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
[displayedIds]
|
|
99
|
+
);
|
|
100
|
+
const setPage = useCallback((ids, select) => {
|
|
101
|
+
setSelected((prev) => {
|
|
102
|
+
const next = new Set(prev);
|
|
103
|
+
for (const id of ids) {
|
|
104
|
+
if (select) next.add(id);
|
|
105
|
+
else next.delete(id);
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
110
|
+
const pageState = useCallback(
|
|
111
|
+
(pageIds) => {
|
|
112
|
+
if (pageIds.length === 0) return "none";
|
|
113
|
+
let n = 0;
|
|
114
|
+
for (const id of pageIds) if (selected.has(id)) n++;
|
|
115
|
+
return n === 0 ? "none" : n === pageIds.length ? "all" : "some";
|
|
116
|
+
},
|
|
117
|
+
[selected]
|
|
118
|
+
);
|
|
119
|
+
const clear = useCallback(() => {
|
|
120
|
+
anchor.current = null;
|
|
121
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
122
|
+
}, []);
|
|
123
|
+
return useMemo2(
|
|
124
|
+
() => ({
|
|
125
|
+
selected,
|
|
126
|
+
isSelected: (id) => selected.has(id),
|
|
127
|
+
toggle,
|
|
128
|
+
setPage,
|
|
129
|
+
pageState,
|
|
130
|
+
clear,
|
|
131
|
+
count: selected.size
|
|
132
|
+
}),
|
|
133
|
+
[selected, toggle, setPage, pageState, clear]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/useSelect.ts
|
|
138
|
+
import { useCallback as useCallback2, useMemo as useMemo3 } from "react";
|
|
139
|
+
import { selectedFields } from "@pythia-software/query-table-core";
|
|
140
|
+
function useSelect(query, setQuery, schema) {
|
|
141
|
+
const fields = useMemo3(() => selectedFields(schema, query), [schema, query]);
|
|
142
|
+
const visible = useMemo3(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
|
|
143
|
+
const hidden = useMemo3(() => {
|
|
144
|
+
const shown = new Set(fields.map((f) => f.name));
|
|
145
|
+
return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
|
|
146
|
+
}, [schema, fields]);
|
|
147
|
+
const writeSelect = useCallback2(
|
|
148
|
+
(mutate) => {
|
|
149
|
+
setQuery((prev) => {
|
|
150
|
+
const current = prev.select.length ? prev.select : visible;
|
|
151
|
+
return { ...prev, select: mutate([...current]) };
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
[setQuery, visible]
|
|
155
|
+
);
|
|
156
|
+
const show = useCallback2((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
|
|
157
|
+
const hide = useCallback2((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
|
|
158
|
+
const move = useCallback2(
|
|
159
|
+
(field, toIndex) => writeSelect((c) => {
|
|
160
|
+
const from = c.findIndex((x) => x.field === field);
|
|
161
|
+
if (from === -1) return c;
|
|
162
|
+
const [item] = c.splice(from, 1);
|
|
163
|
+
c.splice(Math.max(0, Math.min(toIndex, c.length)), 0, item);
|
|
164
|
+
return c;
|
|
165
|
+
}),
|
|
166
|
+
[writeSelect]
|
|
167
|
+
);
|
|
168
|
+
const setWidth = useCallback2(
|
|
169
|
+
(field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
|
|
170
|
+
[writeSelect]
|
|
171
|
+
);
|
|
172
|
+
const reset = useCallback2(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
|
|
173
|
+
return { visible, fields, hidden, show, hide, move, setWidth, reset };
|
|
174
|
+
}
|
|
175
|
+
function columnFor(select, name) {
|
|
176
|
+
const found = select.find((c) => c.field === name);
|
|
177
|
+
return found ?? { field: name };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/useColumnDrag.ts
|
|
181
|
+
import { useCallback as useCallback3, useMemo as useMemo4, useState as useState3 } from "react";
|
|
182
|
+
function useColumnDrag() {
|
|
183
|
+
const [state, setState] = useState3(null);
|
|
184
|
+
const start = useCallback3((field, index) => setState({ source: field, overIndex: index }), []);
|
|
185
|
+
const over = useCallback3(
|
|
186
|
+
(index) => (
|
|
187
|
+
// Keep the SAME object reference when unchanged so React can bail out of the
|
|
188
|
+
// re-render — onDragOver fires continuously while the cursor moves.
|
|
189
|
+
setState((s) => s && s.overIndex !== index ? { source: s.source, overIndex: index } : s)
|
|
190
|
+
),
|
|
191
|
+
[]
|
|
192
|
+
);
|
|
193
|
+
const end = useCallback3(() => setState(null), []);
|
|
194
|
+
const preview = useCallback3(
|
|
195
|
+
(order) => {
|
|
196
|
+
if (!state) return order;
|
|
197
|
+
const without = order.filter((n) => n !== state.source);
|
|
198
|
+
if (without.length === order.length) return order;
|
|
199
|
+
const at = Math.max(0, Math.min(state.overIndex, without.length));
|
|
200
|
+
return [...without.slice(0, at), state.source, ...without.slice(at)];
|
|
201
|
+
},
|
|
202
|
+
[state]
|
|
203
|
+
);
|
|
204
|
+
return useMemo4(
|
|
205
|
+
() => ({
|
|
206
|
+
source: state?.source ?? null,
|
|
207
|
+
overIndex: state?.overIndex ?? null,
|
|
208
|
+
active: state != null,
|
|
209
|
+
start,
|
|
210
|
+
over,
|
|
211
|
+
end,
|
|
212
|
+
preview
|
|
213
|
+
}),
|
|
214
|
+
[state, start, over, end, preview]
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/useSavedQueries.ts
|
|
219
|
+
import { useCallback as useCallback4, useEffect as useEffect2, useState as useState4 } from "react";
|
|
220
|
+
function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
221
|
+
const [items, setItems] = useState4([]);
|
|
222
|
+
const [defaultId, setDefaultId] = useState4(null);
|
|
223
|
+
const [loading, setLoading] = useState4(true);
|
|
224
|
+
const refresh = useCallback4(async () => {
|
|
225
|
+
setLoading(true);
|
|
226
|
+
try {
|
|
227
|
+
const [savedItems, defaultSaved] = await Promise.all([
|
|
228
|
+
storage.listSaved(key),
|
|
229
|
+
storage.loadDefaultSaved?.(key) ?? Promise.resolve(null)
|
|
230
|
+
]);
|
|
231
|
+
setItems(savedItems);
|
|
232
|
+
setDefaultId(defaultSaved?.id ?? null);
|
|
233
|
+
} finally {
|
|
234
|
+
setLoading(false);
|
|
235
|
+
}
|
|
236
|
+
}, [storage, key]);
|
|
237
|
+
useEffect2(() => {
|
|
238
|
+
void refresh();
|
|
239
|
+
}, [refresh]);
|
|
240
|
+
const save = useCallback4(
|
|
241
|
+
async (name) => {
|
|
242
|
+
const saved = await storage.saveNamed(key, name, currentQuery, now());
|
|
243
|
+
await refresh();
|
|
244
|
+
return saved;
|
|
245
|
+
},
|
|
246
|
+
[storage, key, currentQuery, now, refresh]
|
|
247
|
+
);
|
|
248
|
+
const load = useCallback4(
|
|
249
|
+
(id) => {
|
|
250
|
+
const found = items.find((q) => q.id === id);
|
|
251
|
+
if (found) applyQueryState(found.query);
|
|
252
|
+
},
|
|
253
|
+
[items, applyQueryState]
|
|
254
|
+
);
|
|
255
|
+
const setDefault = useCallback4(
|
|
256
|
+
async (id) => {
|
|
257
|
+
await storage.setDefaultSaved?.(key, id);
|
|
258
|
+
await refresh();
|
|
259
|
+
},
|
|
260
|
+
[storage, key, refresh]
|
|
261
|
+
);
|
|
262
|
+
const clearDefault = useCallback4(async () => {
|
|
263
|
+
await storage.setDefaultSaved?.(key, null);
|
|
264
|
+
await refresh();
|
|
265
|
+
}, [storage, key, refresh]);
|
|
266
|
+
const remove = useCallback4(
|
|
267
|
+
async (id) => {
|
|
268
|
+
if (id === defaultId) await storage.setDefaultSaved?.(key, null);
|
|
269
|
+
await storage.deleteSaved(key, id);
|
|
270
|
+
await refresh();
|
|
271
|
+
},
|
|
272
|
+
[storage, key, defaultId, refresh]
|
|
273
|
+
);
|
|
274
|
+
return { items, loading, defaultId, save, load, setDefault, clearDefault, remove };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/useQueryTable.ts
|
|
278
|
+
var AUTOCOMPLETE_LIMIT = 50;
|
|
279
|
+
var MAX_AUTO_REFRESH_POLLS = 1e3;
|
|
280
|
+
function defaultsFor(schema) {
|
|
281
|
+
return normalizeQueryState({
|
|
282
|
+
select: schema.defaultSelect ?? [],
|
|
283
|
+
where: [],
|
|
284
|
+
orderBy: schema.defaultSort ?? [],
|
|
285
|
+
limit: schema.defaultLimit ?? EMPTY_QUERY.limit,
|
|
286
|
+
offset: 0
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function cloneQueryState(q) {
|
|
290
|
+
const clone = {
|
|
291
|
+
select: q.select.map((column) => ({ ...column })),
|
|
292
|
+
where: q.where.map((clause) => ({ ...clause })),
|
|
293
|
+
orderBy: q.orderBy.map((term) => ({ ...term })),
|
|
294
|
+
limit: q.limit,
|
|
295
|
+
offset: q.offset
|
|
296
|
+
};
|
|
297
|
+
if (q.aggregations) clone.aggregations = q.aggregations.map((a) => ({ ...a, groupBy: [...a.groupBy] }));
|
|
298
|
+
return clone;
|
|
299
|
+
}
|
|
300
|
+
function mergeAggregation(a, patch) {
|
|
301
|
+
const next = {
|
|
302
|
+
id: a.id,
|
|
303
|
+
op: patch.op ?? a.op,
|
|
304
|
+
groupBy: patch.groupBy ?? a.groupBy
|
|
305
|
+
};
|
|
306
|
+
const field = "field" in patch ? patch.field : a.field;
|
|
307
|
+
if (field) next.field = field;
|
|
308
|
+
const label = "label" in patch ? patch.label : a.label;
|
|
309
|
+
if (label) next.label = label;
|
|
310
|
+
return next;
|
|
311
|
+
}
|
|
312
|
+
function isNullLike(value) {
|
|
313
|
+
return value == null || Array.isArray(value) && value.length === 0;
|
|
314
|
+
}
|
|
315
|
+
function asDistinctValue(value) {
|
|
316
|
+
if (isNullLike(value)) return null;
|
|
317
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
318
|
+
return String(value);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function fieldHasNullInRows(field, rows) {
|
|
323
|
+
for (const row of rows) {
|
|
324
|
+
if (isNullLike(readFieldValue(field, row))) return true;
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
function distinctValuesFromRows(field, rows, search) {
|
|
329
|
+
const target = search.trim().toLowerCase();
|
|
330
|
+
const values = [];
|
|
331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
332
|
+
let hasMore = false;
|
|
333
|
+
for (const row of rows) {
|
|
334
|
+
const raw = asDistinctValue(readFieldValue(field, row));
|
|
335
|
+
if (raw == null || seen.has(raw)) continue;
|
|
336
|
+
if (target && !raw.toLowerCase().includes(target)) continue;
|
|
337
|
+
if (values.length < AUTOCOMPLETE_LIMIT) {
|
|
338
|
+
seen.add(raw);
|
|
339
|
+
values.push(raw);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
hasMore = true;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
return { values: values.sort((a, b) => a.localeCompare(b)), hasMore };
|
|
346
|
+
}
|
|
347
|
+
function resolveInitial(opts) {
|
|
348
|
+
const defaults = defaultsFor(opts.schema);
|
|
349
|
+
if (opts.initialQuery) return { q: normalizeQueryState(opts.initialQuery, defaults), fromUrl: false };
|
|
350
|
+
if (opts.syncUrl === true && typeof window !== "undefined") {
|
|
351
|
+
const token = new URLSearchParams(window.location.search).get("q");
|
|
352
|
+
if (token) return { q: normalizeQueryState(decodeQuery(token), defaults), fromUrl: true };
|
|
353
|
+
}
|
|
354
|
+
return { q: defaults, fromUrl: false };
|
|
355
|
+
}
|
|
356
|
+
function useQueryTable(opts) {
|
|
357
|
+
const { schema, transport, clientRows, initialQuery, syncUrl = false, debounceMs = 200 } = opts;
|
|
358
|
+
const storage = useMemo5(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
|
|
359
|
+
const now = opts.now ?? Date.now;
|
|
360
|
+
const nowRef = useRef2(now);
|
|
361
|
+
const onRefreshRef = useRef2(opts.onRefresh);
|
|
362
|
+
const defaults = useMemo5(() => defaultsFor(schema), [schema]);
|
|
363
|
+
const byName = useMemo5(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);
|
|
364
|
+
const initRef = useRef2(resolveInitial(opts));
|
|
365
|
+
const aggIdRef = useRef2(0);
|
|
366
|
+
const [query, setQueryState] = useState5(initRef.current.q);
|
|
367
|
+
const undoStack = useRef2([cloneQueryState(initRef.current.q)]);
|
|
368
|
+
const redoStack = useRef2([]);
|
|
369
|
+
const [rows, setRows] = useState5([]);
|
|
370
|
+
const [total, setTotal] = useState5(null);
|
|
371
|
+
const [loading, setLoading] = useState5(false);
|
|
372
|
+
const [error, setError] = useState5(null);
|
|
373
|
+
const [nonce, setNonce] = useState5(0);
|
|
374
|
+
const [autoRefreshStatus, setAutoRefreshStatus] = useState5(null);
|
|
375
|
+
useEffect3(() => {
|
|
376
|
+
nowRef.current = now;
|
|
377
|
+
}, [now]);
|
|
378
|
+
useEffect3(() => {
|
|
379
|
+
onRefreshRef.current = opts.onRefresh;
|
|
380
|
+
}, [opts.onRefresh]);
|
|
381
|
+
const setQuery = useCallback5((next) => {
|
|
382
|
+
setQueryState((prev) => {
|
|
383
|
+
const candidate = typeof next === "function" ? next(prev) : next;
|
|
384
|
+
const nextQuery = normalizeQueryState(candidate, defaults);
|
|
385
|
+
if (queriesEqual(prev, nextQuery)) return prev;
|
|
386
|
+
undoStack.current.push(cloneQueryState(prev));
|
|
387
|
+
redoStack.current = [];
|
|
388
|
+
return cloneQueryState(nextQuery);
|
|
389
|
+
});
|
|
390
|
+
}, [defaults]);
|
|
391
|
+
useEffect3(() => {
|
|
392
|
+
if (initRef.current.fromUrl || initialQuery) return;
|
|
393
|
+
let cancelled = false;
|
|
394
|
+
void (async () => {
|
|
395
|
+
const defaultSaved = await storage.loadDefaultSaved?.(schema.name);
|
|
396
|
+
const restored = defaultSaved?.query ?? await storage.loadLast(schema.name);
|
|
397
|
+
if (!cancelled && restored) {
|
|
398
|
+
const normalized = normalizeQueryState(restored, defaults);
|
|
399
|
+
undoStack.current = [cloneQueryState(normalized)];
|
|
400
|
+
redoStack.current = [];
|
|
401
|
+
setQueryState(normalized);
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
return () => {
|
|
405
|
+
cancelled = true;
|
|
406
|
+
};
|
|
407
|
+
}, []);
|
|
408
|
+
const queryKey = useMemo5(() => encodeQuery(query), [query]);
|
|
409
|
+
useEffect3(() => {
|
|
410
|
+
const ac = new AbortController();
|
|
411
|
+
let cancelled = false;
|
|
412
|
+
const run = async () => {
|
|
413
|
+
setLoading(true);
|
|
414
|
+
setError(null);
|
|
415
|
+
try {
|
|
416
|
+
if (transport) {
|
|
417
|
+
const res = await transport.fetchRows(toServerQuery(query, schema), ac.signal);
|
|
418
|
+
if (!cancelled) {
|
|
419
|
+
setRows(res.rows);
|
|
420
|
+
setTotal(res.total);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
const res = applyQuery(clientRows ?? [], query, schema);
|
|
424
|
+
if (!cancelled) {
|
|
425
|
+
setRows(res.rows);
|
|
426
|
+
setTotal(res.total);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (e) {
|
|
430
|
+
if (!cancelled && !ac.signal.aborted) setError(e);
|
|
431
|
+
} finally {
|
|
432
|
+
if (!cancelled) setLoading(false);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const t = setTimeout(run, debounceMs);
|
|
436
|
+
return () => {
|
|
437
|
+
cancelled = true;
|
|
438
|
+
clearTimeout(t);
|
|
439
|
+
ac.abort();
|
|
440
|
+
};
|
|
441
|
+
}, [queryKey, transport, clientRows, schema, debounceMs, nonce]);
|
|
442
|
+
useEffect3(() => {
|
|
443
|
+
if (!syncUrl || typeof window === "undefined") return;
|
|
444
|
+
const url = new URL(window.location.href);
|
|
445
|
+
const token = encodeQuery(query);
|
|
446
|
+
if (token) url.searchParams.set("q", token);
|
|
447
|
+
else url.searchParams.delete("q");
|
|
448
|
+
window.history.replaceState(null, "", url.toString());
|
|
449
|
+
}, [queryKey, syncUrl, query]);
|
|
450
|
+
useEffect3(() => {
|
|
451
|
+
const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
|
|
452
|
+
return () => clearTimeout(t);
|
|
453
|
+
}, [queryKey, storage, schema.name, query]);
|
|
454
|
+
const idField = useMemo5(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
|
|
455
|
+
const rowId = useCallback5(
|
|
456
|
+
(row) => idField ? readFieldValue(idField, row) : null,
|
|
457
|
+
[idField]
|
|
458
|
+
);
|
|
459
|
+
const displayedIds = useMemo5(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
|
|
460
|
+
const selection = useSelection(displayedIds);
|
|
461
|
+
const select = useSelect(query, setQuery, schema);
|
|
462
|
+
const columnDrag = useColumnDrag();
|
|
463
|
+
const saved = useSavedQueries(
|
|
464
|
+
schema.name,
|
|
465
|
+
query,
|
|
466
|
+
(q) => setQuery(q),
|
|
467
|
+
storage,
|
|
468
|
+
now
|
|
469
|
+
);
|
|
470
|
+
const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce);
|
|
471
|
+
const canUndo = undoStack.current.length > 0;
|
|
472
|
+
const canRedo = redoStack.current.length > 0;
|
|
473
|
+
const undo = useCallback5(() => {
|
|
474
|
+
setQueryState((prev) => {
|
|
475
|
+
const previous = undoStack.current.pop();
|
|
476
|
+
if (!previous) return prev;
|
|
477
|
+
redoStack.current.push(cloneQueryState(prev));
|
|
478
|
+
return cloneQueryState(previous);
|
|
479
|
+
});
|
|
480
|
+
}, []);
|
|
481
|
+
const redo = useCallback5(() => {
|
|
482
|
+
setQueryState((prev) => {
|
|
483
|
+
const next = redoStack.current.pop();
|
|
484
|
+
if (!next) return prev;
|
|
485
|
+
undoStack.current.push(cloneQueryState(prev));
|
|
486
|
+
return cloneQueryState(next);
|
|
487
|
+
});
|
|
488
|
+
}, []);
|
|
489
|
+
const patch = useCallback5((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
|
|
490
|
+
const addFilter = useCallback5((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
|
|
491
|
+
const updateFilter = useCallback5(
|
|
492
|
+
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((c, i) => i === index ? clause : c) })),
|
|
493
|
+
[setQuery]
|
|
494
|
+
);
|
|
495
|
+
const removeFilter = useCallback5(
|
|
496
|
+
(index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
|
|
497
|
+
[setQuery]
|
|
498
|
+
);
|
|
499
|
+
const clearFilters = useCallback5(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
|
|
500
|
+
const resetAll = useCallback5(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);
|
|
501
|
+
const addAggregation = useCallback5(
|
|
502
|
+
(partial) => {
|
|
503
|
+
const id = `a${nowRef.current()}-${aggIdRef.current++}`;
|
|
504
|
+
const clause = { id, op: "count", groupBy: [], ...partial };
|
|
505
|
+
setQuery((q) => ({ ...q, aggregations: [...q.aggregations ?? [], clause] }));
|
|
506
|
+
return id;
|
|
507
|
+
},
|
|
508
|
+
[setQuery]
|
|
509
|
+
);
|
|
510
|
+
const updateAggregation = useCallback5(
|
|
511
|
+
(id, patch2) => setQuery((q) => ({
|
|
512
|
+
...q,
|
|
513
|
+
aggregations: (q.aggregations ?? []).map((a) => a.id === id ? mergeAggregation(a, patch2) : a)
|
|
514
|
+
})),
|
|
515
|
+
[setQuery]
|
|
516
|
+
);
|
|
517
|
+
const moveAggregation = useCallback5(
|
|
518
|
+
(id, toIndex) => setQuery((q) => {
|
|
519
|
+
const current = q.aggregations ?? [];
|
|
520
|
+
const from = current.findIndex((a) => a.id === id);
|
|
521
|
+
if (from === -1) return q;
|
|
522
|
+
const next = [...current];
|
|
523
|
+
const [item] = next.splice(from, 1);
|
|
524
|
+
next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, item);
|
|
525
|
+
return { ...q, aggregations: next };
|
|
526
|
+
}),
|
|
527
|
+
[setQuery]
|
|
528
|
+
);
|
|
529
|
+
const removeAggregation = useCallback5(
|
|
530
|
+
(id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),
|
|
531
|
+
[setQuery]
|
|
532
|
+
);
|
|
533
|
+
const clearAggregations = useCallback5(
|
|
534
|
+
() => setQuery((q) => ({ ...q, aggregations: [] })),
|
|
535
|
+
[setQuery]
|
|
536
|
+
);
|
|
537
|
+
const setSort = useCallback5((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
|
|
538
|
+
const toggleSort = useCallback5(
|
|
539
|
+
(field, additive = false) => setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),
|
|
540
|
+
[setQuery]
|
|
541
|
+
);
|
|
542
|
+
const setLimit = useCallback5((limit) => patch({ limit, offset: 0 }), [patch]);
|
|
543
|
+
const setOffset = useCallback5((offset) => patch({ offset }), [patch]);
|
|
544
|
+
const nextPage = useCallback5(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
|
|
545
|
+
const prevPage = useCallback5(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
|
|
546
|
+
const filterValues = useCallback5(
|
|
547
|
+
async (field, search) => {
|
|
548
|
+
if (!transport?.fetchDistinctValues) {
|
|
549
|
+
const f = byName.get(field);
|
|
550
|
+
if (!f || !clientRows) return { values: [], hasMore: false };
|
|
551
|
+
const fieldHasNull = fieldHasNullInRows(f, clientRows);
|
|
552
|
+
const matches = distinctValuesFromRows(f, clientRows, search);
|
|
553
|
+
return {
|
|
554
|
+
values: matches.values,
|
|
555
|
+
hasMore: matches.hasMore,
|
|
556
|
+
hasNull: fieldHasNull
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return transport.fetchDistinctValues({ field, search });
|
|
560
|
+
},
|
|
561
|
+
[transport, byName, clientRows]
|
|
562
|
+
);
|
|
563
|
+
const refresh = useCallback5(() => {
|
|
564
|
+
setNonce((n) => n + 1);
|
|
565
|
+
onRefreshRef.current?.();
|
|
566
|
+
}, []);
|
|
567
|
+
const stopAutoRefresh = useCallback5(() => setAutoRefreshStatus(null), []);
|
|
568
|
+
const startAutoRefresh = useCallback5(
|
|
569
|
+
({ frequencyMs, turnOffAfterMs }) => {
|
|
570
|
+
const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
|
|
571
|
+
if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
|
|
572
|
+
throw new Error("Auto-refresh must schedule between 1 and 1000 polls.");
|
|
573
|
+
}
|
|
574
|
+
const startedAt = nowRef.current();
|
|
575
|
+
refresh();
|
|
576
|
+
setAutoRefreshStatus({
|
|
577
|
+
frequencyMs,
|
|
578
|
+
turnOffAfterMs,
|
|
579
|
+
startedAt,
|
|
580
|
+
stopsAt: startedAt + turnOffAfterMs,
|
|
581
|
+
pollCount: 1
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
[refresh]
|
|
585
|
+
);
|
|
586
|
+
const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
|
|
587
|
+
const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
|
|
588
|
+
useEffect3(() => {
|
|
589
|
+
if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
|
|
590
|
+
const tick = () => {
|
|
591
|
+
if (nowRef.current() >= autoRefreshStopsAt) {
|
|
592
|
+
setAutoRefreshStatus((current) => current?.stopsAt === autoRefreshStopsAt ? null : current);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
refresh();
|
|
596
|
+
setAutoRefreshStatus((current) => {
|
|
597
|
+
if (!current) return current;
|
|
598
|
+
if (current.stopsAt !== autoRefreshStopsAt) return current;
|
|
599
|
+
return { ...current, pollCount: current.pollCount + 1 };
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
const interval = setInterval(tick, autoRefreshFrequencyMs);
|
|
603
|
+
const timeout = setTimeout(
|
|
604
|
+
() => setAutoRefreshStatus((current) => current?.stopsAt === autoRefreshStopsAt ? null : current),
|
|
605
|
+
Math.max(0, autoRefreshStopsAt - nowRef.current())
|
|
606
|
+
);
|
|
607
|
+
return () => {
|
|
608
|
+
clearInterval(interval);
|
|
609
|
+
clearTimeout(timeout);
|
|
610
|
+
};
|
|
611
|
+
}, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
|
|
612
|
+
const autoRefresh = useMemo5(
|
|
613
|
+
() => ({
|
|
614
|
+
status: autoRefreshStatus,
|
|
615
|
+
start: startAutoRefresh,
|
|
616
|
+
stop: stopAutoRefresh
|
|
617
|
+
}),
|
|
618
|
+
[autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
|
|
619
|
+
);
|
|
620
|
+
const refreshRow = useCallback5(
|
|
621
|
+
async (id) => {
|
|
622
|
+
if (!transport?.fetchRow) return;
|
|
623
|
+
const updated = await transport.fetchRow(id);
|
|
624
|
+
setRows((prev) => prev.map((r) => rowId(r) === id ? updated ?? r : r));
|
|
625
|
+
},
|
|
626
|
+
[transport, rowId]
|
|
627
|
+
);
|
|
628
|
+
const visibleFields = useMemo5(() => selectedFields2(schema, query), [schema, query]);
|
|
629
|
+
const aggregations = useMemo5(
|
|
630
|
+
() => ({
|
|
631
|
+
clauses: query.aggregations ?? [],
|
|
632
|
+
results: aggState.results,
|
|
633
|
+
loading: aggState.loading,
|
|
634
|
+
error: aggState.error,
|
|
635
|
+
add: addAggregation,
|
|
636
|
+
update: updateAggregation,
|
|
637
|
+
move: moveAggregation,
|
|
638
|
+
remove: removeAggregation,
|
|
639
|
+
clear: clearAggregations
|
|
640
|
+
}),
|
|
641
|
+
[
|
|
642
|
+
query.aggregations,
|
|
643
|
+
aggState.results,
|
|
644
|
+
aggState.loading,
|
|
645
|
+
aggState.error,
|
|
646
|
+
addAggregation,
|
|
647
|
+
updateAggregation,
|
|
648
|
+
moveAggregation,
|
|
649
|
+
removeAggregation,
|
|
650
|
+
clearAggregations
|
|
651
|
+
]
|
|
652
|
+
);
|
|
653
|
+
return {
|
|
654
|
+
query,
|
|
655
|
+
defaults,
|
|
656
|
+
setQuery,
|
|
657
|
+
rows,
|
|
658
|
+
total,
|
|
659
|
+
loading,
|
|
660
|
+
error,
|
|
661
|
+
canUndo,
|
|
662
|
+
canRedo,
|
|
663
|
+
undo,
|
|
664
|
+
redo,
|
|
665
|
+
refresh,
|
|
666
|
+
autoRefresh,
|
|
667
|
+
refreshRow,
|
|
668
|
+
aggregations,
|
|
669
|
+
addFilter,
|
|
670
|
+
updateFilter,
|
|
671
|
+
removeFilter,
|
|
672
|
+
clearFilters,
|
|
673
|
+
resetAll,
|
|
674
|
+
filterValues,
|
|
675
|
+
toggleSort,
|
|
676
|
+
setSort,
|
|
677
|
+
select,
|
|
678
|
+
setLimit,
|
|
679
|
+
setOffset,
|
|
680
|
+
nextPage,
|
|
681
|
+
prevPage,
|
|
682
|
+
selection,
|
|
683
|
+
saved,
|
|
684
|
+
columnDrag,
|
|
685
|
+
visibleFields,
|
|
686
|
+
rowId
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function nextOrderBy(existing, field, additive) {
|
|
690
|
+
const i = existing.findIndex((s) => s.field === field);
|
|
691
|
+
if (!additive) {
|
|
692
|
+
if (i === 0 && existing.length === 1) {
|
|
693
|
+
const flipped = existing[0].dir === "desc" ? "asc" : "desc";
|
|
694
|
+
return [{ field, dir: flipped }];
|
|
695
|
+
}
|
|
696
|
+
return [{ field, dir: "desc" }];
|
|
697
|
+
}
|
|
698
|
+
if (i === -1) return [...existing, { field, dir: "desc" }];
|
|
699
|
+
const cur = existing[i];
|
|
700
|
+
if (cur.dir === "desc") {
|
|
701
|
+
const next = [...existing];
|
|
702
|
+
next[i] = { field, dir: "asc" };
|
|
703
|
+
return next;
|
|
704
|
+
}
|
|
705
|
+
return existing.filter((_, k) => k !== i);
|
|
706
|
+
}
|
|
707
|
+
export {
|
|
708
|
+
useAggregations,
|
|
709
|
+
useColumnDrag,
|
|
710
|
+
useQueryTable,
|
|
711
|
+
useSavedQueries,
|
|
712
|
+
useSelect,
|
|
713
|
+
useSelection
|
|
714
|
+
};
|
|
715
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useQueryTable.ts","../src/useAggregations.ts","../src/useSelection.ts","../src/useSelect.ts","../src/useColumnDrag.ts","../src/useSavedQueries.ts"],"sourcesContent":["// useQueryTable — the one hook that owns everything.\n//\n// The headless brain of the table: it holds the QueryState, keeps it in sync\n// with the URL (`?q=`) and the StorageAdapter, orchestrates fetches through the\n// Transport (debounced + abortable), and exposes intent-level mutators so a UI\n// never hand-edits QueryState. @pythia-software/query-table-ui is a thin layer over this; you\n// can also build an entirely custom table on it.\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n EMPTY_QUERY,\n applyQuery,\n decodeQuery,\n encodeQuery,\n memoryStorageAdapter,\n normalizeQueryState,\n readFieldValue,\n selectedFields,\n toServerQuery,\n queriesEqual,\n} from \"@pythia-software/query-table-core\";\n\nconst AUTOCOMPLETE_LIMIT = 50;\nconst MAX_AUTO_REFRESH_POLLS = 1000;\nimport type {\n QueryState,\n WhereClause,\n OrderByClause,\n AggOp,\n AggregationClause,\n AggregationResult,\n RowId,\n FieldDef,\n FieldSchema,\n Transport,\n StorageAdapter,\n DistinctValuesResult,\n} from \"@pythia-software/query-table-core\";\nimport { useAggregations } from \"./useAggregations\";\nimport { useSelection, type SelectionApi } from \"./useSelection\";\nimport { useSelect, type SelectApi } from \"./useSelect\";\nimport { useColumnDrag, type ColumnDragApi } from \"./useColumnDrag\";\nimport { useSavedQueries, type SavedQueriesApi } from \"./useSavedQueries\";\n\nexport interface UseQueryTableOptions<Row> {\n schema: FieldSchema<Row>;\n /** Data access. Omit for a purely client-side table driven by `clientRows`. */\n transport?: Transport<Row>;\n /** All rows, for client-side mode (applyQuery runs locally over these). */\n clientRows?: Row[];\n /** Saved/last-query persistence. Defaults to a non-durable in-memory adapter.\n * Pass localStorageAdapter() explicitly only when filter values are safe to\n * retain as cleartext JSON on the device. */\n storage?: StorageAdapter;\n /** Seed query, normalized and resource-bounded before use. When omitted the\n * hook resolves: opted-in URL → storage.loadLast → schema defaults. */\n initialQuery?: QueryState;\n /** Mirror QueryState to `?q=` via history.replaceState. Default false because\n * base64url is encoding, not encryption: filters will appear in browser\n * history, referrers, logs, analytics, and screenshots when enabled. */\n syncUrl?: boolean;\n /** Debounce (ms) between a query change and the fetch it triggers. Default 200. */\n debounceMs?: number;\n /** Injected clock for deterministic saved-query timestamps. Default Date.now. */\n now?: () => number;\n /** Side-effect to run on every refresh — both manual `api.refresh()` and each\n * auto-refresh tick. Fires *in addition to* the internal `setNonce` re-query,\n * so manual and auto stay identical. Use it in client mode (no transport) to\n * bridge a UI refresh to your own server fetch without monkey-patching the\n * returned `api`. Always read through a ref, so supplying it never tears down\n * and recreates the auto-refresh interval. */\n onRefresh?: () => void;\n}\n\nexport interface AutoRefreshConfig {\n frequencyMs: number;\n turnOffAfterMs: number;\n}\n\nexport interface AutoRefreshStatus extends AutoRefreshConfig {\n startedAt: number;\n stopsAt: number;\n pollCount: number;\n}\n\nexport interface AutoRefreshApi {\n status: AutoRefreshStatus | null;\n start: (config: AutoRefreshConfig) => void;\n stop: () => void;\n}\n\n/** A patch for one metric. `field`/`label` accept an explicit `undefined` to\n * CLEAR them (e.g. switching to an op whose measure no longer applies); an\n * absent key leaves the existing value untouched. */\nexport interface AggregationPatch {\n op?: AggOp;\n field?: string | undefined;\n groupBy?: string[];\n label?: string | undefined;\n}\n\nexport interface AggregationsApi {\n /** The metric specs currently in the query. */\n clauses: AggregationClause[];\n /** Server (or client-mirror) results, one entry per clause; null when none. */\n results: AggregationResult | null;\n loading: boolean;\n error: Error | null;\n /** Append a metric. Defaults to \"count all rows\"; pass a partial to override.\n * Returns the generated id. */\n add: (partial?: Partial<Omit<AggregationClause, \"id\">>) => string;\n /** Patch a metric by id. */\n update: (id: string, patch: AggregationPatch) => void;\n /** Reorder: move the metric `id` to `toIndex` (an index in the list with the\n * metric removed, so a drag preview's slot maps straight to the result). */\n move: (id: string, toIndex: number) => void;\n /** Remove a metric by id. */\n remove: (id: string) => void;\n /** Remove all metrics. */\n clear: () => void;\n}\n\nexport interface QueryTableApi<Row> {\n // state\n query: QueryState;\n /** Schema defaults used for reset controls. */\n defaults: QueryState;\n setQuery: (next: QueryState | ((prev: QueryState) => QueryState)) => void;\n /** Can move back to a previously applied local query state. */\n canUndo: boolean;\n /** Can move forward to a previously undone local query state. */\n canRedo: boolean;\n /** Move backward through local, unsaved query history. */\n undo: () => void;\n /** Move forward through local, unsaved query history. */\n redo: () => void;\n\n // data\n rows: Row[];\n total: number | null;\n loading: boolean;\n error: Error | null;\n refresh: () => void;\n autoRefresh: AutoRefreshApi;\n refreshRow: (id: RowId) => Promise<void>;\n\n // aggregation metrics (the dashboard panel above the table)\n aggregations: AggregationsApi;\n\n // filters\n addFilter: (clause: WhereClause) => void;\n updateFilter: (index: number, clause: WhereClause) => void;\n removeFilter: (index: number) => void;\n clearFilters: () => void;\n /** Reset the complete query state back to schema defaults. */\n resetAll: () => void;\n /** Keystroke-driven filter-value autocomplete (Transport.fetchDistinctValues). */\n filterValues: (field: string, search: string) => Promise<DistinctValuesResult>;\n\n // sorting (multi-sort): click header sets/cycles primary; shift-click appends.\n toggleSort: (field: string, additive?: boolean) => void;\n setSort: (orderBy: OrderByClause[]) => void;\n\n // select + pagination + selection + saved\n select: SelectApi<Row>;\n setLimit: (limit: number) => void;\n setOffset: (offset: number) => void;\n nextPage: () => void;\n prevPage: () => void;\n selection: SelectionApi;\n saved: SavedQueriesApi;\n /** Shared, transient column-reorder drag state (table headers + select chips). */\n columnDrag: ColumnDragApi;\n\n // resolved view\n visibleFields: FieldDef<Row>[];\n rowId: (row: Row) => RowId | null;\n}\n\nfunction defaultsFor<Row>(schema: FieldSchema<Row>): QueryState {\n return normalizeQueryState({\n select: schema.defaultSelect ?? [],\n where: [],\n orderBy: schema.defaultSort ?? [],\n limit: schema.defaultLimit ?? EMPTY_QUERY.limit,\n offset: 0,\n });\n}\n\nfunction cloneQueryState(q: QueryState): QueryState {\n const clone: QueryState = {\n select: q.select.map((column) => ({ ...column })),\n where: q.where.map((clause) => ({ ...clause })),\n orderBy: q.orderBy.map((term) => ({ ...term })),\n limit: q.limit,\n offset: q.offset,\n };\n if (q.aggregations) clone.aggregations = q.aggregations.map((a) => ({ ...a, groupBy: [...a.groupBy] }));\n return clone;\n}\n\n/** Apply a patch to a metric, building a fresh clause so optional `field`/`label`\n * can be dropped (passing them as `undefined` clears; omitting the key keeps the\n * current value) without ever materializing an `undefined`-valued property. */\nfunction mergeAggregation(a: AggregationClause, patch: AggregationPatch): AggregationClause {\n const next: AggregationClause = {\n id: a.id,\n op: patch.op ?? a.op,\n groupBy: patch.groupBy ?? a.groupBy,\n };\n const field = \"field\" in patch ? patch.field : a.field;\n if (field) next.field = field;\n const label = \"label\" in patch ? patch.label : a.label;\n if (label) next.label = label;\n return next;\n}\n\nfunction isNullLike(value: unknown): boolean {\n return value == null || (Array.isArray(value) && value.length === 0);\n}\n\nfunction asDistinctValue(value: unknown): string | null {\n if (isNullLike(value)) return null;\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n return null;\n}\n\nfunction fieldHasNullInRows<Row>(field: FieldDef<Row>, rows: Row[]): boolean {\n for (const row of rows) {\n if (isNullLike(readFieldValue(field, row))) return true;\n }\n return false;\n}\n\nfunction distinctValuesFromRows<Row>(field: FieldDef<Row>, rows: Row[], search: string): { values: string[]; hasMore: boolean } {\n const target = search.trim().toLowerCase();\n const values: string[] = [];\n const seen = new Set<string>();\n let hasMore = false;\n\n for (const row of rows) {\n const raw = asDistinctValue(readFieldValue(field, row));\n if (raw == null || seen.has(raw)) continue;\n if (target && !raw.toLowerCase().includes(target)) continue;\n\n if (values.length < AUTOCOMPLETE_LIMIT) {\n seen.add(raw);\n values.push(raw);\n continue;\n }\n\n hasMore = true;\n break;\n }\n\n return { values: values.sort((a, b) => a.localeCompare(b)), hasMore };\n}\n\nfunction resolveInitial<Row>(opts: UseQueryTableOptions<Row>): { q: QueryState; fromUrl: boolean } {\n const defaults = defaultsFor(opts.schema);\n if (opts.initialQuery) return { q: normalizeQueryState(opts.initialQuery, defaults), fromUrl: false };\n if (opts.syncUrl === true && typeof window !== \"undefined\") {\n const token = new URLSearchParams(window.location.search).get(\"q\");\n if (token) return { q: normalizeQueryState(decodeQuery(token), defaults), fromUrl: true };\n }\n return { q: defaults, fromUrl: false };\n}\n\nexport function useQueryTable<Row>(opts: UseQueryTableOptions<Row>): QueryTableApi<Row> {\n const { schema, transport, clientRows, initialQuery, syncUrl = false, debounceMs = 200 } = opts;\n const storage = useMemo(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);\n const now = opts.now ?? Date.now;\n const nowRef = useRef(now);\n const onRefreshRef = useRef(opts.onRefresh);\n const defaults = useMemo(() => defaultsFor(schema), [schema]);\n const byName = useMemo(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);\n\n const initRef = useRef(resolveInitial(opts));\n const aggIdRef = useRef(0);\n const [query, setQueryState] = useState<QueryState>(initRef.current.q);\n const undoStack = useRef<QueryState[]>([cloneQueryState(initRef.current.q)]);\n const redoStack = useRef<QueryState[]>([]);\n const [rows, setRows] = useState<Row[]>([]);\n const [total, setTotal] = useState<number | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [nonce, setNonce] = useState(0);\n const [autoRefreshStatus, setAutoRefreshStatus] = useState<AutoRefreshStatus | null>(null);\n\n useEffect(() => {\n nowRef.current = now;\n }, [now]);\n\n useEffect(() => {\n onRefreshRef.current = opts.onRefresh;\n }, [opts.onRefresh]);\n\n const setQuery = useCallback<QueryTableApi<Row>[\"setQuery\"]>((next) => {\n setQueryState((prev) => {\n const candidate = typeof next === \"function\" ? (next as (p: QueryState) => QueryState)(prev) : next;\n const nextQuery = normalizeQueryState(candidate, defaults);\n if (queriesEqual(prev, nextQuery)) return prev;\n undoStack.current.push(cloneQueryState(prev));\n redoStack.current = [];\n return cloneQueryState(nextQuery);\n });\n }, [defaults]);\n\n // Restore the default saved query (preferred) or last query on mount when\n // nothing explicit seeded the view.\n useEffect(() => {\n if (initRef.current.fromUrl || initialQuery) return;\n let cancelled = false;\n void (async () => {\n const defaultSaved = await storage.loadDefaultSaved?.(schema.name);\n const restored = defaultSaved?.query ?? (await storage.loadLast(schema.name));\n if (!cancelled && restored) {\n const normalized = normalizeQueryState(restored, defaults);\n undoStack.current = [cloneQueryState(normalized)];\n redoStack.current = [];\n setQueryState(normalized);\n }\n })();\n return () => {\n cancelled = true;\n };\n // mount-only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Fetch (debounced + abortable). Transport mode, or client-side applyQuery.\n const queryKey = useMemo(() => encodeQuery(query), [query]);\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n const run = async () => {\n setLoading(true);\n setError(null);\n try {\n if (transport) {\n const res = await transport.fetchRows(toServerQuery(query, schema), ac.signal);\n if (!cancelled) {\n setRows(res.rows);\n setTotal(res.total);\n }\n } else {\n const res = applyQuery(clientRows ?? [], query, schema);\n if (!cancelled) {\n setRows(res.rows);\n setTotal(res.total);\n }\n }\n } catch (e) {\n if (!cancelled && !ac.signal.aborted) setError(e as Error);\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n const t = setTimeout(run, debounceMs);\n return () => {\n cancelled = true;\n clearTimeout(t);\n ac.abort();\n };\n // queryKey captures query value; clientRows/nonce force refetch.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [queryKey, transport, clientRows, schema, debounceMs, nonce]);\n\n // URL sync.\n useEffect(() => {\n if (!syncUrl || typeof window === \"undefined\") return;\n const url = new URL(window.location.href);\n const token = encodeQuery(query);\n if (token) url.searchParams.set(\"q\", token);\n else url.searchParams.delete(\"q\");\n window.history.replaceState(null, \"\", url.toString());\n }, [queryKey, syncUrl, query]);\n\n // Persist last query (debounced).\n useEffect(() => {\n const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);\n return () => clearTimeout(t);\n }, [queryKey, storage, schema.name, query]);\n\n // Resolved id accessor.\n const idField = useMemo(() => schema.fields.find((f) => f.name === schema.idField), [schema]);\n const rowId = useCallback(\n (row: Row): RowId | null => (idField ? (readFieldValue(idField, row) as RowId | null) : null),\n [idField],\n );\n\n // Sub-APIs.\n const displayedIds = useMemo(() => rows.map(rowId).filter((id): id is RowId => id != null), [rows, rowId]);\n const selection = useSelection(displayedIds);\n const select = useSelect(query, setQuery, schema);\n const columnDrag = useColumnDrag();\n const saved = useSavedQueries(\n schema.name,\n query,\n (q) => setQuery(q),\n storage,\n now,\n );\n const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce);\n\n const canUndo = undoStack.current.length > 0;\n const canRedo = redoStack.current.length > 0;\n\n const undo = useCallback(() => {\n setQueryState((prev) => {\n const previous = undoStack.current.pop();\n if (!previous) return prev;\n redoStack.current.push(cloneQueryState(prev));\n return cloneQueryState(previous);\n });\n }, []);\n\n const redo = useCallback(() => {\n setQueryState((prev) => {\n const next = redoStack.current.pop();\n if (!next) return prev;\n undoStack.current.push(cloneQueryState(prev));\n return cloneQueryState(next);\n });\n }, []);\n\n // ---- intent helpers (keep offset/url/storage coherent) ----\n const patch = useCallback((p: Partial<QueryState>) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);\n\n const addFilter = useCallback((clause: WhereClause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);\n const updateFilter = useCallback(\n (index: number, clause: WhereClause) =>\n setQuery((q) => ({ ...q, offset: 0, where: q.where.map((c, i) => (i === index ? clause : c)) })),\n [setQuery],\n );\n const removeFilter = useCallback(\n (index: number) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),\n [setQuery],\n );\n const clearFilters = useCallback(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);\n const resetAll = useCallback(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);\n\n // ---- aggregation metric mutators (don't touch paging — scope is the whole set) ----\n const addAggregation = useCallback<AggregationsApi[\"add\"]>(\n (partial) => {\n const id = `a${nowRef.current()}-${aggIdRef.current++}`;\n const clause: AggregationClause = { id, op: \"count\", groupBy: [], ...partial };\n setQuery((q) => ({ ...q, aggregations: [...(q.aggregations ?? []), clause] }));\n return id;\n },\n [setQuery],\n );\n const updateAggregation = useCallback<AggregationsApi[\"update\"]>(\n (id, patch) =>\n setQuery((q) => ({\n ...q,\n aggregations: (q.aggregations ?? []).map((a) => (a.id === id ? mergeAggregation(a, patch) : a)),\n })),\n [setQuery],\n );\n const moveAggregation = useCallback<AggregationsApi[\"move\"]>(\n (id, toIndex) =>\n setQuery((q) => {\n const current = q.aggregations ?? [];\n const from = current.findIndex((a) => a.id === id);\n if (from === -1) return q;\n const next = [...current];\n const [item] = next.splice(from, 1);\n next.splice(Math.max(0, Math.min(toIndex, next.length)), 0, item!);\n return { ...q, aggregations: next };\n }),\n [setQuery],\n );\n const removeAggregation = useCallback<AggregationsApi[\"remove\"]>(\n (id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),\n [setQuery],\n );\n const clearAggregations = useCallback<AggregationsApi[\"clear\"]>(\n () => setQuery((q) => ({ ...q, aggregations: [] })),\n [setQuery],\n );\n\n const setSort = useCallback((orderBy: OrderByClause[]) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);\n const toggleSort = useCallback(\n (field: string, additive = false) =>\n setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),\n [setQuery],\n );\n\n const setLimit = useCallback((limit: number) => patch({ limit, offset: 0 }), [patch]);\n const setOffset = useCallback((offset: number) => patch({ offset }), [patch]);\n const nextPage = useCallback(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);\n const prevPage = useCallback(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);\n\n const filterValues = useCallback(\n async (field: string, search: string): Promise<DistinctValuesResult> => {\n if (!transport?.fetchDistinctValues) {\n const f = byName.get(field);\n if (!f || !clientRows) return { values: [], hasMore: false };\n\n const fieldHasNull = fieldHasNullInRows(f, clientRows);\n const matches = distinctValuesFromRows(f, clientRows, search);\n\n return {\n values: matches.values,\n hasMore: matches.hasMore,\n hasNull: fieldHasNull,\n };\n }\n return transport.fetchDistinctValues({ field, search });\n },\n [transport, byName, clientRows],\n );\n\n // Single refresh seam: re-runs the internal query (setNonce) and fires the\n // consumer's onRefresh, if any. Both the manual `api.refresh()` and every\n // auto-refresh tick go through here, so they stay observably identical. Read\n // onRefresh through a ref to keep this callback (and the interval that depends\n // on it) stable across renders.\n const refresh = useCallback(() => {\n setNonce((n) => n + 1);\n onRefreshRef.current?.();\n }, []);\n const stopAutoRefresh = useCallback(() => setAutoRefreshStatus(null), []);\n const startAutoRefresh = useCallback(\n ({ frequencyMs, turnOffAfterMs }: AutoRefreshConfig) => {\n const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;\n if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {\n throw new Error(\"Auto-refresh must schedule between 1 and 1000 polls.\");\n }\n\n const startedAt = nowRef.current();\n refresh();\n setAutoRefreshStatus({\n frequencyMs,\n turnOffAfterMs,\n startedAt,\n stopsAt: startedAt + turnOffAfterMs,\n pollCount: 1,\n });\n },\n [refresh],\n );\n\n const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;\n const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;\n\n useEffect(() => {\n if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;\n\n const tick = () => {\n if (nowRef.current() >= autoRefreshStopsAt) {\n setAutoRefreshStatus((current) => (current?.stopsAt === autoRefreshStopsAt ? null : current));\n return;\n }\n\n refresh();\n setAutoRefreshStatus((current) => {\n if (!current) return current;\n if (current.stopsAt !== autoRefreshStopsAt) return current;\n return { ...current, pollCount: current.pollCount + 1 };\n });\n };\n\n const interval = setInterval(tick, autoRefreshFrequencyMs);\n const timeout = setTimeout(\n () => setAutoRefreshStatus((current) => (current?.stopsAt === autoRefreshStopsAt ? null : current)),\n Math.max(0, autoRefreshStopsAt - nowRef.current()),\n );\n\n return () => {\n clearInterval(interval);\n clearTimeout(timeout);\n };\n }, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);\n\n const autoRefresh = useMemo<AutoRefreshApi>(\n () => ({\n status: autoRefreshStatus,\n start: startAutoRefresh,\n stop: stopAutoRefresh,\n }),\n [autoRefreshStatus, startAutoRefresh, stopAutoRefresh],\n );\n\n const refreshRow = useCallback(\n async (id: RowId) => {\n if (!transport?.fetchRow) return;\n const updated = await transport.fetchRow(id);\n setRows((prev) => prev.map((r) => (rowId(r) === id ? updated ?? r : r)));\n },\n [transport, rowId],\n );\n\n const visibleFields = useMemo(() => selectedFields(schema, query), [schema, query]);\n\n const aggregations = useMemo<AggregationsApi>(\n () => ({\n clauses: query.aggregations ?? [],\n results: aggState.results,\n loading: aggState.loading,\n error: aggState.error,\n add: addAggregation,\n update: updateAggregation,\n move: moveAggregation,\n remove: removeAggregation,\n clear: clearAggregations,\n }),\n [\n query.aggregations,\n aggState.results,\n aggState.loading,\n aggState.error,\n addAggregation,\n updateAggregation,\n moveAggregation,\n removeAggregation,\n clearAggregations,\n ],\n );\n\n return {\n query,\n defaults,\n setQuery,\n rows,\n total,\n loading,\n error,\n canUndo,\n canRedo,\n undo,\n redo,\n refresh,\n autoRefresh,\n refreshRow,\n aggregations,\n addFilter,\n updateFilter,\n removeFilter,\n clearFilters,\n resetAll,\n filterValues,\n toggleSort,\n setSort,\n select,\n setLimit,\n setOffset,\n nextPage,\n prevPage,\n selection,\n saved,\n columnDrag,\n visibleFields,\n rowId,\n };\n}\n\n/** Multi-sort header click logic.\n * - plain click on the sole sorted column → flip its direction\n * - plain click on another column → make it the only sort (desc)\n * - shift-click a new column → append it (desc) as a secondary term\n * - shift-click an existing term → cycle desc → asc → remove */\nfunction nextOrderBy(existing: OrderByClause[], field: string, additive: boolean): OrderByClause[] {\n const i = existing.findIndex((s) => s.field === field);\n if (!additive) {\n if (i === 0 && existing.length === 1) {\n const flipped: \"asc\" | \"desc\" = existing[0]!.dir === \"desc\" ? \"asc\" : \"desc\";\n return [{ field, dir: flipped }];\n }\n return [{ field, dir: \"desc\" }];\n }\n if (i === -1) return [...existing, { field, dir: \"desc\" }];\n const cur = existing[i]!;\n if (cur.dir === \"desc\") {\n const next = [...existing];\n next[i] = { field, dir: \"asc\" };\n return next;\n }\n return existing.filter((_, k) => k !== i); // asc → remove\n}\n","// useAggregations — the metric panel's data side.\n//\n// Runs the optional GROUP BY queries that back the metrics shown above the table.\n// Kept separate from the rows fetch so the panel loads/refreshes independently:\n// metrics depend ONLY on the WHERE filter + the aggregation specs (scope is the\n// whole filtered set — never the page window), so paging or re-sorting the table\n// does not re-issue them. Debounced + abortable, mirroring the rows fetch.\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { applyAggregations, toAggregationQuery } from \"@pythia-software/query-table-core\";\nimport type { AggregationResult, FieldSchema, QueryState, Transport } from \"@pythia-software/query-table-core\";\n\nexport interface AggregationsApiState {\n results: AggregationResult | null;\n loading: boolean;\n error: Error | null;\n}\n\nconst EMPTY_RESULT: AggregationResult = { metrics: [] };\n\nexport function useAggregations<Row>(\n query: QueryState,\n schema: FieldSchema<Row>,\n transport: Transport<Row> | undefined,\n clientRows: Row[] | undefined,\n debounceMs: number,\n nonce: number,\n): AggregationsApiState {\n const [results, setResults] = useState<AggregationResult | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n // Metrics depend only on WHERE + the agg specs. Keying on just those avoids\n // refetching when the user pages or re-sorts the table.\n const key = useMemo(\n () => JSON.stringify({ where: query.where, aggregations: query.aggregations ?? [] }),\n [query.where, query.aggregations],\n );\n const hasAggregations = (query.aggregations?.length ?? 0) > 0;\n\n useEffect(() => {\n if (!hasAggregations) {\n setResults(null);\n setError(null);\n setLoading(false);\n return;\n }\n\n const ac = new AbortController();\n let cancelled = false;\n const run = async () => {\n setLoading(true);\n setError(null);\n try {\n let res: AggregationResult;\n if (transport?.fetchAggregations) {\n res = await transport.fetchAggregations(toAggregationQuery(query, schema), ac.signal);\n } else if (clientRows) {\n res = applyAggregations(clientRows, query, schema);\n } else {\n res = EMPTY_RESULT;\n }\n if (!cancelled) setResults(res);\n } catch (e) {\n if (!cancelled && !ac.signal.aborted) setError(e as Error);\n } finally {\n if (!cancelled) setLoading(false);\n }\n };\n const t = setTimeout(run, debounceMs);\n return () => {\n cancelled = true;\n clearTimeout(t);\n ac.abort();\n };\n // `key` captures the relevant query subset; clientRows/nonce force a refetch.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [key, hasAggregations, transport, clientRows, schema, debounceMs, nonce]);\n\n return { results, loading, error };\n}\n","// useSelection — row selection for paged and re-sortable datasets.\n//\n// - Shift-click range select, anchored by ROW ID (not index) so a\n// range survives re-sort / re-filter / paging. Index anchors break the moment\n// the data reorders; id anchors don't.\n// - Indeterminate header checkbox + select-all-on-page.\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { RowId } from \"@pythia-software/query-table-core\";\n\nexport interface SelectionApi {\n selected: Set<RowId>;\n isSelected: (id: RowId) => boolean;\n /** Toggle one row. With `shiftKey`, extend from the last-toggled anchor to `id`\n * across the *currently displayed* order (falls back to a plain toggle if the\n * anchor row is no longer present). */\n toggle: (id: RowId, shiftKey?: boolean) => void;\n /** Select / clear every row currently on the page. */\n setPage: (ids: RowId[], selected: boolean) => void;\n /** Header checkbox tri-state for the current page. */\n pageState: (pageIds: RowId[]) => \"none\" | \"some\" | \"all\";\n clear: () => void;\n count: number;\n}\n\n/** `displayedIds` is the page's id list in display order; the hook uses it to\n * resolve shift-ranges and header tri-state against what the user sees. */\nexport function useSelection(displayedIds: RowId[]): SelectionApi {\n const [selected, setSelected] = useState<Set<RowId>>(() => new Set());\n const anchor = useRef<RowId | null>(null);\n\n const toggle = useCallback(\n (id: RowId, shiftKey = false) => {\n setSelected((prev) => {\n const next = new Set(prev);\n const a = anchor.current;\n if (shiftKey && a != null) {\n const i = displayedIds.indexOf(a);\n const j = displayedIds.indexOf(id);\n if (i !== -1 && j !== -1) {\n const [lo, hi] = i <= j ? [i, j] : [j, i];\n const span = displayedIds.slice(lo, hi + 1);\n const allChecked = span.every((rowId) => prev.has(rowId));\n for (const rowId of span) {\n if (allChecked) next.delete(rowId);\n else next.add(rowId);\n }\n anchor.current = id;\n return next;\n }\n }\n if (next.has(id)) next.delete(id);\n else next.add(id);\n anchor.current = id;\n return next;\n });\n },\n [displayedIds],\n );\n\n const setPage = useCallback((ids: RowId[], select: boolean) => {\n setSelected((prev) => {\n const next = new Set(prev);\n for (const id of ids) {\n if (select) next.add(id);\n else next.delete(id);\n }\n return next;\n });\n }, []);\n\n const pageState = useCallback(\n (pageIds: RowId[]): \"none\" | \"some\" | \"all\" => {\n if (pageIds.length === 0) return \"none\";\n let n = 0;\n for (const id of pageIds) if (selected.has(id)) n++;\n return n === 0 ? \"none\" : n === pageIds.length ? \"all\" : \"some\";\n },\n [selected],\n );\n\n const clear = useCallback(() => {\n anchor.current = null;\n setSelected(new Set());\n }, []);\n\n return useMemo<SelectionApi>(\n () => ({\n selected,\n isSelected: (id) => selected.has(id),\n toggle,\n setPage,\n pageState,\n clear,\n count: selected.size,\n }),\n [selected, toggle, setPage, pageState, clear],\n );\n}\n","// useSelect — the SELECT list: visible columns, their order, and their widths,\n// all persisted INTO the query (decision #5, so one URL renders identically\n// anywhere). This API mutates QueryState.select; it holds no state of its own.\n// Width changes should be debounced by the caller before they hit the query to\n// avoid flooding history during a drag (useQueryTable does this).\n\nimport { useCallback, useMemo } from \"react\";\nimport type { QueryState, SelectColumn, FieldDef, FieldSchema } from \"@pythia-software/query-table-core\";\nimport { selectedFields } from \"@pythia-software/query-table-core\";\n\nexport interface SelectApi<Row> {\n /** Ordered visible columns (resolved: schema defaults when query.select is empty). */\n visible: SelectColumn[];\n /** The corresponding FieldDefs, same order. */\n fields: FieldDef<Row>[];\n /** Selectable fields not currently shown (for the \"add column\" picker). */\n hidden: FieldDef<Row>[];\n\n show: (field: string) => void;\n hide: (field: string) => void;\n /** Reorder via drag-drop: move `field` to `toIndex` in the visible order. */\n move: (field: string, toIndex: number) => void;\n /** Set a column's pixel width. */\n setWidth: (field: string, width: number) => void;\n /** Reset to the schema's default columns (clears query.select). */\n reset: () => void;\n}\n\ntype SetQuery = (next: QueryState | ((prev: QueryState) => QueryState)) => void;\n\nexport function useSelect<Row>(query: QueryState, setQuery: SetQuery, schema: FieldSchema<Row>): SelectApi<Row> {\n const fields = useMemo(() => selectedFields(schema, query), [schema, query]);\n\n // The materialized SelectColumn[] (resolve defaults so the UI always has a\n // concrete, reorderable list even before the user customizes it).\n const visible = useMemo<SelectColumn[]>(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);\n\n const hidden = useMemo<FieldDef<Row>[]>(() => {\n const shown = new Set(fields.map((f) => f.name));\n return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));\n }, [schema, fields]);\n\n const writeSelect = useCallback(\n (mutate: (cols: SelectColumn[]) => SelectColumn[]) => {\n setQuery((prev) => {\n // Materialize current (resolved) columns the first time we diverge from defaults.\n const current = prev.select.length ? prev.select : visible;\n return { ...prev, select: mutate([...current]) };\n });\n },\n [setQuery, visible],\n );\n\n const show = useCallback((field: string) => writeSelect((c) => (c.some((x) => x.field === field) ? c : [...c, { field }])), [writeSelect]);\n const hide = useCallback((field: string) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);\n const move = useCallback(\n (field: string, toIndex: number) =>\n writeSelect((c) => {\n const from = c.findIndex((x) => x.field === field);\n if (from === -1) return c;\n const [item] = c.splice(from, 1);\n c.splice(Math.max(0, Math.min(toIndex, c.length)), 0, item!);\n return c;\n }),\n [writeSelect],\n );\n const setWidth = useCallback(\n (field: string, width: number) =>\n writeSelect((c) => c.map((x) => (x.field === field ? { ...x, width: Math.round(width) } : x))),\n [writeSelect],\n );\n const reset = useCallback(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);\n\n return { visible, fields, hidden, show, hide, move, setWidth, reset };\n}\n\nfunction columnFor(select: SelectColumn[], name: string): SelectColumn {\n const found = select.find((c) => c.field === name);\n return found ?? { field: name };\n}\n","// useColumnDrag — transient, SHARED state for a column-reorder drag. One instance\n// is owned by useQueryTable and read by BOTH the table headers and the\n// QueryBuilder select chips, so a drag started in either surface live-previews\n// (and dims) the same column in the other. It holds no query state; the reorder\n// is committed to QueryState.select by whichever surface handles the drop.\n\nimport { useCallback, useMemo, useState } from \"react\";\n\nexport interface ColumnDragApi {\n /** Field name currently being dragged, or null when idle. */\n source: string | null;\n /** Insertion index within the order with `source` removed (0..n; n = append). */\n overIndex: number | null;\n /** True while a column drag is in progress. */\n active: boolean;\n /** Begin dragging `field`, initially previewed at `index` (its own slot). */\n start: (field: string, index: number) => void;\n /** Update the previewed insertion index (no-op if unchanged). */\n over: (index: number) => void;\n /** End the drag (on drop or cancel). */\n end: () => void;\n /** The live order to render while dragging: `source` moved to `overIndex`.\n * Returns `order` unchanged when idle, or when `source` isn't in `order`. */\n preview: (order: string[]) => string[];\n}\n\ninterface DragState {\n source: string;\n overIndex: number;\n}\n\nexport function useColumnDrag(): ColumnDragApi {\n const [state, setState] = useState<DragState | null>(null);\n\n const start = useCallback((field: string, index: number) => setState({ source: field, overIndex: index }), []);\n const over = useCallback(\n (index: number) =>\n // Keep the SAME object reference when unchanged so React can bail out of the\n // re-render — onDragOver fires continuously while the cursor moves.\n setState((s) => (s && s.overIndex !== index ? { source: s.source, overIndex: index } : s)),\n [],\n );\n const end = useCallback(() => setState(null), []);\n\n const preview = useCallback(\n (order: string[]): string[] => {\n if (!state) return order;\n const without = order.filter((n) => n !== state.source);\n if (without.length === order.length) return order; // source not part of this list\n const at = Math.max(0, Math.min(state.overIndex, without.length));\n return [...without.slice(0, at), state.source, ...without.slice(at)];\n },\n [state],\n );\n\n return useMemo(\n () => ({\n source: state?.source ?? null,\n overIndex: state?.overIndex ?? null,\n active: state != null,\n start,\n over,\n end,\n preview,\n }),\n [state, start, over, end, preview],\n );\n}\n","// useSavedQueries — named query snapshots over a StorageAdapter.\n//\n// \"Last query\" auto-restore and named saves both flow through the injected\n// StorageAdapter (in-memory by default, explicitly durable when the caller\n// provides one). Loading a saved query pushes its QueryState into the controller.\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport type { QueryState, SavedQuery, StorageAdapter } from \"@pythia-software/query-table-core\";\n\nexport interface SavedQueriesApi {\n items: SavedQuery[];\n loading: boolean;\n /** Saved query id used as the default view when no query is specified. */\n defaultId: string | null;\n /** Persist the current query under a name. Rejects on duplicate names. */\n save: (name: string) => Promise<SavedQuery>;\n /** Load a saved query into the live controller. */\n load: (id: string) => void;\n /** Mark a saved query as the default view. No-op when storage lacks support. */\n setDefault: (id: string) => Promise<void>;\n /** Clear the saved-query default view. */\n clearDefault: () => Promise<void>;\n remove: (id: string) => Promise<void>;\n}\n\nexport function useSavedQueries(\n key: string,\n currentQuery: QueryState,\n applyQueryState: (q: QueryState) => void,\n storage: StorageAdapter,\n /** Injected clock so saves are deterministic/testable. */\n now: () => number,\n): SavedQueriesApi {\n const [items, setItems] = useState<SavedQuery[]>([]);\n const [defaultId, setDefaultId] = useState<string | null>(null);\n const [loading, setLoading] = useState(true);\n\n const refresh = useCallback(async () => {\n setLoading(true);\n try {\n const [savedItems, defaultSaved] = await Promise.all([\n storage.listSaved(key),\n storage.loadDefaultSaved?.(key) ?? Promise.resolve(null),\n ]);\n setItems(savedItems);\n setDefaultId(defaultSaved?.id ?? null);\n } finally {\n setLoading(false);\n }\n }, [storage, key]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const save = useCallback(\n async (name: string) => {\n const saved = await storage.saveNamed(key, name, currentQuery, now());\n await refresh();\n return saved;\n },\n [storage, key, currentQuery, now, refresh],\n );\n\n const load = useCallback(\n (id: string) => {\n const found = items.find((q) => q.id === id);\n if (found) applyQueryState(found.query);\n },\n [items, applyQueryState],\n );\n\n const setDefault = useCallback(\n async (id: string) => {\n await storage.setDefaultSaved?.(key, id);\n await refresh();\n },\n [storage, key, refresh],\n );\n\n const clearDefault = useCallback(async () => {\n await storage.setDefaultSaved?.(key, null);\n await refresh();\n }, [storage, key, refresh]);\n\n const remove = useCallback(\n async (id: string) => {\n if (id === defaultId) await storage.setDefaultSaved?.(key, null);\n await storage.deleteSaved(key, id);\n await refresh();\n },\n [storage, key, defaultId, refresh],\n );\n\n return { items, loading, defaultId, save, load, setDefault, clearDefault, remove };\n}\n"],"mappings":"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","names":["useCallback","useEffect","useMemo","useRef","useState","selectedFields","useMemo","useState","useCallback","useMemo","useCallback","useMemo","useState","useCallback","useEffect","useState","useMemo","useRef","useState","useEffect","useCallback","patch","selectedFields"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pythia-software/query-table-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Schema-driven data table — headless React hooks.",
|
|
5
|
+
"keywords": ["data-table", "query-builder", "react", "hooks", "typescript"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Pythia Software",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Pythia-Software/query-table.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Pythia-Software/query-table#readme",
|
|
14
|
+
"bugs": { "url": "https://github.com/Pythia-Software/query-table/issues" },
|
|
15
|
+
"engines": { "node": ">=22" },
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
29
|
+
"publishConfig": { "access": "public" },
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/index.ts --format esm --dts --sourcemap --clean --target es2020 --external react --external @pythia-software/query-table-core",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@pythia-software/query-table-core": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18 <20"
|
|
40
|
+
}
|
|
41
|
+
}
|