@fusedio/widget-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.js +14 -9
- package/dist/bundle.js +2 -2
- package/dist/define-catalog.d.ts +5 -3
- package/dist/define-catalog.js +4 -1
- package/dist/define-component.js +4 -1
- package/dist/form.js +18 -12
- package/dist/hooks/sql-source-overrides.d.ts +16 -0
- package/dist/hooks/sql-source-overrides.js +29 -0
- package/dist/hooks/use-allowed-sources.js +12 -9
- package/dist/hooks/use-allowed-udf-names.js +11 -8
- package/dist/hooks/use-canvas-params.js +12 -9
- package/dist/hooks/use-duckdb-sql.d.ts +1 -1
- package/dist/hooks/use-duckdb-sql.js +78 -58
- package/dist/hooks/use-fused-param-with-form.d.ts +18 -0
- package/dist/hooks/use-fused-param-with-form.js +56 -0
- package/dist/hooks/use-fused-param.js +34 -31
- package/dist/hooks/use-json-ui-edge-animation.js +6 -3
- package/dist/hooks/use-json-ui-log.js +23 -18
- package/dist/hooks/use-json-ui-udf-info.js +6 -3
- package/dist/hooks/use-param-substitution.js +25 -22
- package/dist/hooks/use-udf-output.js +33 -24
- package/dist/hooks/use-upload-access-check.js +9 -6
- package/dist/hooks/use-url-signing.js +22 -17
- package/dist/index.d.ts +3 -0
- package/dist/index.js +69 -19
- package/dist/protocol.js +8 -4
- package/dist/types.js +2 -1
- package/dist/utils/parse-style.d.ts +9 -0
- package/dist/utils/parse-style.js +27 -0
- package/dist/utils/sql-placeholders.js +36 -22
- package/package.json +2 -3
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useDuckDbSqlQueryPreprocessing = useDuckDbSqlQueryPreprocessing;
|
|
4
|
+
exports.useDuckDbSqlQuery = useDuckDbSqlQuery;
|
|
5
|
+
exports.useVfsRegistration = useVfsRegistration;
|
|
1
6
|
/**
|
|
2
7
|
* DuckDB SQL execution against UDF Parquet outputs.
|
|
3
8
|
*
|
|
@@ -13,13 +18,14 @@
|
|
|
13
18
|
* the host side via the bridge. Both hooks work identically in the
|
|
14
19
|
* workbench, the catalog-template test harness, or any other host.
|
|
15
20
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const react_1 = require("react");
|
|
22
|
+
const bridge_1 = require("../bridge");
|
|
23
|
+
const form_1 = require("../form");
|
|
24
|
+
const use_json_ui_log_1 = require("./use-json-ui-log");
|
|
25
|
+
const use_json_ui_edge_animation_1 = require("./use-json-ui-edge-animation");
|
|
26
|
+
const use_canvas_params_1 = require("./use-canvas-params");
|
|
27
|
+
const sql_source_overrides_1 = require("./sql-source-overrides");
|
|
28
|
+
const sql_placeholders_1 = require("../utils/sql-placeholders");
|
|
23
29
|
const DEFAULT_MAX_ROWS = 500;
|
|
24
30
|
const EMPTY_ROWS = Object.freeze([]);
|
|
25
31
|
const EMPTY_COLUMNS = Object.freeze([]);
|
|
@@ -63,53 +69,67 @@ function buildProcessedSql(sql, resolved, fileNameMap, sourceOverrides, sqlParam
|
|
|
63
69
|
processedSql.slice(raw.end);
|
|
64
70
|
}
|
|
65
71
|
processedSql = appendLimitIfMissing(processedSql, maxRows);
|
|
66
|
-
return substituteSqlParams(processedSql, sqlParamValues);
|
|
72
|
+
return (0, sql_placeholders_1.substituteSqlParams)(processedSql, sqlParamValues);
|
|
67
73
|
}
|
|
68
74
|
/**
|
|
69
75
|
* Preprocess SQL: resolve placeholders, register UDFs via the bridge,
|
|
70
76
|
* substitute params, sign URLs, append LIMIT. Returns the prepared SQL.
|
|
71
77
|
*/
|
|
72
|
-
|
|
73
|
-
const bridge = useFusedWidgetBridge();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
78
|
+
function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows = DEFAULT_MAX_ROWS, sourceOverrides: explicitSourceOverrides, }) {
|
|
79
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
80
|
+
// Auto-detect host-provided source overrides (e.g. the workbench's
|
|
81
|
+
// sql-runner exposes in-memory relations to descendants) and merge them with
|
|
82
|
+
// any explicit option. Explicit overrides win on key collision. Component
|
|
83
|
+
// authors never thread this manually.
|
|
84
|
+
const contextSourceOverrides = (0, sql_source_overrides_1.useSqlSourceOverrides)();
|
|
85
|
+
const sourceOverrides = (0, react_1.useMemo)(() => {
|
|
86
|
+
const hasContext = Object.keys(contextSourceOverrides).length > 0;
|
|
87
|
+
if (!explicitSourceOverrides) {
|
|
88
|
+
return hasContext ? contextSourceOverrides : undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!hasContext)
|
|
91
|
+
return explicitSourceOverrides;
|
|
92
|
+
return { ...contextSourceOverrides, ...explicitSourceOverrides };
|
|
93
|
+
}, [contextSourceOverrides, explicitSourceOverrides]);
|
|
94
|
+
const { startLoading: startEdgeLoading, stopLoading: stopEdgeLoading } = (0, use_json_ui_edge_animation_1.useJsonUiEdgeAnimation)();
|
|
95
|
+
const { log } = (0, use_json_ui_log_1.useJsonUiLog)();
|
|
96
|
+
const [processedSql, setProcessedSql] = (0, react_1.useState)("");
|
|
97
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
98
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
99
|
+
const [fetchKey, setFetchKey] = (0, react_1.useState)(0);
|
|
100
|
+
const sourcePlaceholders = (0, react_1.useMemo)(() => {
|
|
81
101
|
if (!sql)
|
|
82
102
|
return [];
|
|
83
|
-
return parseSqlUdfPlaceholders(sql);
|
|
103
|
+
return (0, sql_placeholders_1.parseSqlUdfPlaceholders)(sql);
|
|
84
104
|
}, [sql]);
|
|
85
105
|
// Skip placeholders that have a sourceOverride active (workbench-only path).
|
|
86
|
-
const placeholdersAfterOverride = useMemo(() => {
|
|
106
|
+
const placeholdersAfterOverride = (0, react_1.useMemo)(() => {
|
|
87
107
|
if (!sourceOverrides)
|
|
88
108
|
return sourcePlaceholders;
|
|
89
109
|
return sourcePlaceholders.filter((p) => p.overrides !== null || !sourceOverrides[p.name]);
|
|
90
110
|
}, [sourcePlaceholders, sourceOverrides]);
|
|
91
111
|
// Override values may reference $params — collect those names so we can
|
|
92
112
|
// subscribe to the same canvas/form param map the SQL body uses.
|
|
93
|
-
const overrideParamNames = useMemo(() => {
|
|
113
|
+
const overrideParamNames = (0, react_1.useMemo)(() => {
|
|
94
114
|
const set = new Set();
|
|
95
115
|
for (const p of placeholdersAfterOverride) {
|
|
96
116
|
if (!p.overrides)
|
|
97
117
|
continue;
|
|
98
118
|
for (const v of Object.values(p.overrides)) {
|
|
99
|
-
const name = getDollarRefName(v);
|
|
119
|
+
const name = (0, sql_placeholders_1.getDollarRefName)(v);
|
|
100
120
|
if (name)
|
|
101
121
|
set.add(name);
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
return Array.from(set);
|
|
105
125
|
}, [placeholdersAfterOverride]);
|
|
106
|
-
const sqlParamNames = useMemo(() => {
|
|
126
|
+
const sqlParamNames = (0, react_1.useMemo)(() => {
|
|
107
127
|
if (!sql)
|
|
108
128
|
return [];
|
|
109
|
-
return extractSqlParams(sql);
|
|
129
|
+
return (0, sql_placeholders_1.extractSqlParams)(sql);
|
|
110
130
|
}, [sql]);
|
|
111
131
|
// Subscribe to canvas + form params for both the SQL body and override refs.
|
|
112
|
-
const allParamNames = useMemo(() => {
|
|
132
|
+
const allParamNames = (0, react_1.useMemo)(() => {
|
|
113
133
|
const seen = new Set();
|
|
114
134
|
const out = [];
|
|
115
135
|
for (const n of sqlParamNames) {
|
|
@@ -126,10 +146,10 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
126
146
|
}
|
|
127
147
|
return out;
|
|
128
148
|
}, [sqlParamNames, overrideParamNames]);
|
|
129
|
-
const canvasParamValues = useCanvasParams(allParamNames);
|
|
130
|
-
const { inForm, values: formParamValues } = useFormParams(allParamNames);
|
|
131
|
-
const sqlParamValues = useMemo(() => inForm ? { ...canvasParamValues, ...formParamValues } : canvasParamValues, [inForm, canvasParamValues, formParamValues]);
|
|
132
|
-
const resolvedPlaceholders = useMemo(() => {
|
|
149
|
+
const canvasParamValues = (0, use_canvas_params_1.useCanvasParams)(allParamNames);
|
|
150
|
+
const { inForm, values: formParamValues } = (0, form_1.useFormParams)(allParamNames);
|
|
151
|
+
const sqlParamValues = (0, react_1.useMemo)(() => inForm ? { ...canvasParamValues, ...formParamValues } : canvasParamValues, [inForm, canvasParamValues, formParamValues]);
|
|
152
|
+
const resolvedPlaceholders = (0, react_1.useMemo)(() => {
|
|
133
153
|
return placeholdersAfterOverride.map((p) => {
|
|
134
154
|
if (!p.overrides) {
|
|
135
155
|
return {
|
|
@@ -142,21 +162,21 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
142
162
|
const resolvedOverrides = {};
|
|
143
163
|
let unresolved = false;
|
|
144
164
|
for (const [paramKey, rawValue] of Object.entries(p.overrides)) {
|
|
145
|
-
const r = resolveOverrideValue(rawValue, sqlParamValues);
|
|
165
|
+
const r = (0, sql_placeholders_1.resolveOverrideValue)(rawValue, sqlParamValues);
|
|
146
166
|
if (r.unresolved)
|
|
147
167
|
unresolved = true;
|
|
148
168
|
resolvedOverrides[paramKey] = r.value;
|
|
149
169
|
}
|
|
150
170
|
return {
|
|
151
171
|
raw: p,
|
|
152
|
-
key: computePlaceholderKey(p.name, resolvedOverrides),
|
|
172
|
+
key: (0, sql_placeholders_1.computePlaceholderKey)(p.name, resolvedOverrides),
|
|
153
173
|
resolvedOverrides,
|
|
154
174
|
unresolved,
|
|
155
175
|
};
|
|
156
176
|
});
|
|
157
177
|
}, [placeholdersAfterOverride, sqlParamValues]);
|
|
158
178
|
// Deduplicate refs we need to pass to the bridge for VFS registration.
|
|
159
|
-
const vfsRefs = useMemo(() => {
|
|
179
|
+
const vfsRefs = (0, react_1.useMemo)(() => {
|
|
160
180
|
const seen = new Set();
|
|
161
181
|
const out = [];
|
|
162
182
|
for (const rp of resolvedPlaceholders) {
|
|
@@ -175,7 +195,7 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
175
195
|
}, [resolvedPlaceholders]);
|
|
176
196
|
// Stable key for the refs list — avoids re-running the resolve effect when
|
|
177
197
|
// the underlying refs are structurally identical.
|
|
178
|
-
const refsKey = useMemo(() => {
|
|
198
|
+
const refsKey = (0, react_1.useMemo)(() => {
|
|
179
199
|
return vfsRefs
|
|
180
200
|
.map((r) => `${r.key}|${r.name}|${r.overrides
|
|
181
201
|
? Object.entries(r.overrides)
|
|
@@ -185,12 +205,12 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
185
205
|
: ""}`)
|
|
186
206
|
.join("\n");
|
|
187
207
|
}, [vfsRefs]);
|
|
188
|
-
const refetch = useCallback(() => {
|
|
208
|
+
const refetch = (0, react_1.useCallback)(() => {
|
|
189
209
|
setFetchKey((k) => k + 1);
|
|
190
210
|
}, []);
|
|
191
211
|
// Subscribe to UDF output changes for every referenced UDF — `resolveVfsFilenames`
|
|
192
212
|
// doesn't auto-rerun when a UDF re-executes, so we trigger a refetch.
|
|
193
|
-
useEffect(() => {
|
|
213
|
+
(0, react_1.useEffect)(() => {
|
|
194
214
|
if (!enabled || vfsRefs.length === 0)
|
|
195
215
|
return;
|
|
196
216
|
const unsubs = vfsRefs.map((ref) => bridge.udfs.subscribeOutput(ref.name, () => {
|
|
@@ -201,14 +221,14 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
201
221
|
};
|
|
202
222
|
}, [bridge, enabled, vfsRefs]);
|
|
203
223
|
// Edge animation mirrors loading.
|
|
204
|
-
useEffect(() => {
|
|
224
|
+
(0, react_1.useEffect)(() => {
|
|
205
225
|
if (loading)
|
|
206
226
|
startEdgeLoading();
|
|
207
227
|
else
|
|
208
228
|
stopEdgeLoading();
|
|
209
229
|
}, [loading, startEdgeLoading, stopEdgeLoading]);
|
|
210
230
|
// Watch for sourceOverride errors / loading so the preprocessing reflects them.
|
|
211
|
-
const sourceOverrideError = useMemo(() => {
|
|
231
|
+
const sourceOverrideError = (0, react_1.useMemo)(() => {
|
|
212
232
|
if (!sourceOverrides)
|
|
213
233
|
return null;
|
|
214
234
|
for (const p of sourcePlaceholders) {
|
|
@@ -220,13 +240,13 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
220
240
|
}
|
|
221
241
|
return null;
|
|
222
242
|
}, [sourcePlaceholders, sourceOverrides]);
|
|
223
|
-
const sourceOverrideLoading = useMemo(() => {
|
|
243
|
+
const sourceOverrideLoading = (0, react_1.useMemo)(() => {
|
|
224
244
|
if (!sourceOverrides)
|
|
225
245
|
return false;
|
|
226
246
|
return sourcePlaceholders.some((p) => p.overrides === null && sourceOverrides[p.name]?.loading);
|
|
227
247
|
}, [sourcePlaceholders, sourceOverrides]);
|
|
228
248
|
const hasUnresolvedOverride = resolvedPlaceholders.some((rp) => rp.unresolved);
|
|
229
|
-
useEffect(() => {
|
|
249
|
+
(0, react_1.useEffect)(() => {
|
|
230
250
|
if (!enabled || !sql) {
|
|
231
251
|
setProcessedSql("");
|
|
232
252
|
setLoading(false);
|
|
@@ -338,7 +358,7 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
338
358
|
log(`SQL preprocessing failed: ${msg}`, "error");
|
|
339
359
|
return;
|
|
340
360
|
}
|
|
341
|
-
const urls = extractSignableUrls(nextProcessedSql);
|
|
361
|
+
const urls = (0, sql_placeholders_1.extractSignableUrls)(nextProcessedSql);
|
|
342
362
|
if (urls.length === 0) {
|
|
343
363
|
if (cancelled)
|
|
344
364
|
return;
|
|
@@ -356,7 +376,7 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
356
376
|
urls.forEach((u, i) => {
|
|
357
377
|
signedMap[u] = signed[i].signed;
|
|
358
378
|
});
|
|
359
|
-
const signedSql = rewriteSignedUrls(nextProcessedSql, signedMap);
|
|
379
|
+
const signedSql = (0, sql_placeholders_1.rewriteSignedUrls)(nextProcessedSql, signedMap);
|
|
360
380
|
setProcessedSql(signedSql);
|
|
361
381
|
setError(null);
|
|
362
382
|
setLoading(false);
|
|
@@ -402,15 +422,15 @@ export function useDuckDbSqlQueryPreprocessing({ sql, enabled = true, maxRows =
|
|
|
402
422
|
* `useDuckDbSqlQueryPreprocessing` to prepare the SQL string, then runs
|
|
403
423
|
* it via the bridge.
|
|
404
424
|
*/
|
|
405
|
-
|
|
406
|
-
const bridge = useFusedWidgetBridge();
|
|
407
|
-
const { startLoading: startEdgeLoading, stopLoading: stopEdgeLoading } = useJsonUiEdgeAnimation();
|
|
408
|
-
const { log } = useJsonUiLog();
|
|
409
|
-
const [rows, setRows] = useState(EMPTY_ROWS);
|
|
410
|
-
const [columns, setColumns] = useState(EMPTY_COLUMNS);
|
|
411
|
-
const [queryLoading, setQueryLoading] = useState(false);
|
|
412
|
-
const [error, setError] = useState(null);
|
|
413
|
-
const [fetchKey, setFetchKey] = useState(0);
|
|
425
|
+
function useDuckDbSqlQuery({ sql, enabled = true, maxRows = DEFAULT_MAX_ROWS, sourceOverrides, }) {
|
|
426
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
427
|
+
const { startLoading: startEdgeLoading, stopLoading: stopEdgeLoading } = (0, use_json_ui_edge_animation_1.useJsonUiEdgeAnimation)();
|
|
428
|
+
const { log } = (0, use_json_ui_log_1.useJsonUiLog)();
|
|
429
|
+
const [rows, setRows] = (0, react_1.useState)(EMPTY_ROWS);
|
|
430
|
+
const [columns, setColumns] = (0, react_1.useState)(EMPTY_COLUMNS);
|
|
431
|
+
const [queryLoading, setQueryLoading] = (0, react_1.useState)(false);
|
|
432
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
433
|
+
const [fetchKey, setFetchKey] = (0, react_1.useState)(0);
|
|
414
434
|
const { processedSql, loading: preprocessingLoading, error: preprocessingError, refetch: refetchPreprocessing, } = useDuckDbSqlQueryPreprocessing({
|
|
415
435
|
sql,
|
|
416
436
|
enabled,
|
|
@@ -419,7 +439,7 @@ export function useDuckDbSqlQuery({ sql, enabled = true, maxRows = DEFAULT_MAX_R
|
|
|
419
439
|
});
|
|
420
440
|
// Bridges the single-frame gap between preprocessing completing and the
|
|
421
441
|
// query effect starting — keeps `loading` true through that frame.
|
|
422
|
-
const consumedSqlRef = useRef("");
|
|
442
|
+
const consumedSqlRef = (0, react_1.useRef)("");
|
|
423
443
|
const awaitingExecution = enabled &&
|
|
424
444
|
!!sql &&
|
|
425
445
|
!!processedSql &&
|
|
@@ -427,18 +447,18 @@ export function useDuckDbSqlQuery({ sql, enabled = true, maxRows = DEFAULT_MAX_R
|
|
|
427
447
|
!preprocessingError &&
|
|
428
448
|
processedSql !== consumedSqlRef.current;
|
|
429
449
|
const loading = preprocessingLoading || queryLoading || awaitingExecution;
|
|
430
|
-
useEffect(() => {
|
|
450
|
+
(0, react_1.useEffect)(() => {
|
|
431
451
|
if (loading)
|
|
432
452
|
startEdgeLoading();
|
|
433
453
|
else
|
|
434
454
|
stopEdgeLoading();
|
|
435
455
|
}, [loading, startEdgeLoading, stopEdgeLoading]);
|
|
436
|
-
const refetch = useCallback(() => {
|
|
456
|
+
const refetch = (0, react_1.useCallback)(() => {
|
|
437
457
|
consumedSqlRef.current = "";
|
|
438
458
|
refetchPreprocessing();
|
|
439
459
|
setFetchKey((k) => k + 1);
|
|
440
460
|
}, [refetchPreprocessing]);
|
|
441
|
-
useEffect(() => {
|
|
461
|
+
(0, react_1.useEffect)(() => {
|
|
442
462
|
if (!enabled || !sql) {
|
|
443
463
|
consumedSqlRef.current = "";
|
|
444
464
|
setRows(EMPTY_ROWS);
|
|
@@ -524,11 +544,11 @@ export function useDuckDbSqlQuery({ sql, enabled = true, maxRows = DEFAULT_MAX_R
|
|
|
524
544
|
* Resolve UDF names to VFS filenames, registering them in DuckDB if needed.
|
|
525
545
|
* Exposed for advanced use cases (e.g. building your own query string).
|
|
526
546
|
*/
|
|
527
|
-
|
|
528
|
-
const bridge = useFusedWidgetBridge();
|
|
529
|
-
const [state, setState] = useState({ filenames: new Map(), loading: false });
|
|
530
|
-
const namesKey = useMemo(() => udfNames.slice().sort().join("|"), [udfNames]);
|
|
531
|
-
useEffect(() => {
|
|
547
|
+
function useVfsRegistration(udfNames, enabled = true) {
|
|
548
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
549
|
+
const [state, setState] = (0, react_1.useState)({ filenames: new Map(), loading: false });
|
|
550
|
+
const namesKey = (0, react_1.useMemo)(() => udfNames.slice().sort().join("|"), [udfNames]);
|
|
551
|
+
(0, react_1.useEffect)(() => {
|
|
532
552
|
if (!enabled || udfNames.length === 0) {
|
|
533
553
|
setState({ filenames: new Map(), loading: false });
|
|
534
554
|
return;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { UseFusedParamOptions, UseFusedParamReturn } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Form-aware variant of {@link useFusedParam}.
|
|
4
|
+
*
|
|
5
|
+
* When the component is rendered inside a built-in Form, the field becomes
|
|
6
|
+
* local state (its value is NOT broadcast to the canvas) and its live value is
|
|
7
|
+
* mirrored into the form's subscription store, so sibling components (a
|
|
8
|
+
* dropdown's SQL options, a chart, a text binding, …) can react to it before
|
|
9
|
+
* the form is submitted. Outside a form it behaves exactly like
|
|
10
|
+
* `useFusedParam` — two-way canvas binding with debounced broadcast.
|
|
11
|
+
*
|
|
12
|
+
* Requires a `FusedWidgetBridgeContext` ancestor (like `useFusedParam`) and a
|
|
13
|
+
* `FormContext` ancestor for the in-form behavior (the built-in Form component
|
|
14
|
+
* provides one).
|
|
15
|
+
*/
|
|
16
|
+
export declare function useFusedParamWithForm<T extends string | number>(options: UseFusedParamOptions<T>): UseFusedParamReturn<T> & {
|
|
17
|
+
isInForm: boolean;
|
|
18
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFusedParamWithForm = useFusedParamWithForm;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const form_1 = require("../form");
|
|
6
|
+
const use_fused_param_1 = require("./use-fused-param");
|
|
7
|
+
/**
|
|
8
|
+
* Form-aware variant of {@link useFusedParam}.
|
|
9
|
+
*
|
|
10
|
+
* When the component is rendered inside a built-in Form, the field becomes
|
|
11
|
+
* local state (its value is NOT broadcast to the canvas) and its live value is
|
|
12
|
+
* mirrored into the form's subscription store, so sibling components (a
|
|
13
|
+
* dropdown's SQL options, a chart, a text binding, …) can react to it before
|
|
14
|
+
* the form is submitted. Outside a form it behaves exactly like
|
|
15
|
+
* `useFusedParam` — two-way canvas binding with debounced broadcast.
|
|
16
|
+
*
|
|
17
|
+
* Requires a `FusedWidgetBridgeContext` ancestor (like `useFusedParam`) and a
|
|
18
|
+
* `FormContext` ancestor for the in-form behavior (the built-in Form component
|
|
19
|
+
* provides one).
|
|
20
|
+
*/
|
|
21
|
+
function useFusedParamWithForm(options) {
|
|
22
|
+
const formContext = (0, form_1.useFormContext)();
|
|
23
|
+
const param = options.param;
|
|
24
|
+
const isFormField = Boolean(formContext.isInForm && param);
|
|
25
|
+
// Hook handles both cases: with param (canvas sync) or without (local state).
|
|
26
|
+
// Disable canvas messaging when inside a form by withholding the param.
|
|
27
|
+
const { value, setValue, broadcastNow, clearValue } = (0, use_fused_param_1.useFusedParam)({
|
|
28
|
+
...options,
|
|
29
|
+
param: isFormField ? undefined : options.param,
|
|
30
|
+
});
|
|
31
|
+
// Mirror the live value into the form's subscription store so sibling
|
|
32
|
+
// components can react to it. Value updates use a dedicated effect so we
|
|
33
|
+
// don't briefly remove the field from the store between renders —
|
|
34
|
+
// unregistration runs only when the binding (store/param) actually changes
|
|
35
|
+
// or on unmount.
|
|
36
|
+
const store = formContext.store;
|
|
37
|
+
(0, react_1.useEffect)(() => {
|
|
38
|
+
if (!isFormField || !param || !store)
|
|
39
|
+
return;
|
|
40
|
+
store.setField(param, value);
|
|
41
|
+
}, [value, isFormField, param, store]);
|
|
42
|
+
(0, react_1.useEffect)(() => {
|
|
43
|
+
if (!isFormField || !param || !store)
|
|
44
|
+
return;
|
|
45
|
+
return () => {
|
|
46
|
+
store.removeField(param);
|
|
47
|
+
};
|
|
48
|
+
}, [isFormField, param, store]);
|
|
49
|
+
return {
|
|
50
|
+
value,
|
|
51
|
+
setValue,
|
|
52
|
+
broadcastNow,
|
|
53
|
+
clearValue,
|
|
54
|
+
isInForm: formContext.isInForm,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFusedParam = useFusedParam;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const bridge_1 = require("../bridge");
|
|
6
|
+
const form_1 = require("../form");
|
|
7
|
+
const protocol_1 = require("../protocol");
|
|
5
8
|
/**
|
|
6
9
|
* Two-way bind a component to a named canvas parameter.
|
|
7
10
|
*
|
|
@@ -40,9 +43,9 @@ import { ParameterMessageType } from "../protocol";
|
|
|
40
43
|
* const { clearValue } = useFusedParam({ param: "selection", defaultValue: null });
|
|
41
44
|
* <button onClick={() => clearValue(null)}>Reset</button>
|
|
42
45
|
*/
|
|
43
|
-
|
|
44
|
-
const bridge = useFusedWidgetBridge();
|
|
45
|
-
const { configHash } = useJsonUiNode();
|
|
46
|
+
function useFusedParam({ param, debounceMs = 300, readOnly = false, defaultValue, broadcastDefaultValue = true, validate, preprocess, }) {
|
|
47
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
48
|
+
const { configHash } = (0, bridge_1.useJsonUiNode)();
|
|
46
49
|
const enabled = !!param;
|
|
47
50
|
// Stable log helper — entries appear in the workbench's runtime logs panel.
|
|
48
51
|
// configHash is passed through so nested JsonUiConfigHashOverride subtrees
|
|
@@ -61,20 +64,20 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
61
64
|
// Stable validator and preprocessor. We deliberately omit `defaultValue`
|
|
62
65
|
// from the deps so a parent re-render passing a new defaultValue literal
|
|
63
66
|
// doesn't churn these. The type semantics only depend on the original.
|
|
64
|
-
const preprocessor = useMemo(() => preprocess ?? createDefaultPreprocessor(defaultValue),
|
|
67
|
+
const preprocessor = (0, react_1.useMemo)(() => preprocess ?? createDefaultPreprocessor(defaultValue),
|
|
65
68
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
69
|
[preprocess]);
|
|
67
|
-
const validator = useMemo(() => validate ?? createDefaultValidator(defaultValue),
|
|
70
|
+
const validator = (0, react_1.useMemo)(() => validate ?? createDefaultValidator(defaultValue),
|
|
68
71
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
72
|
[validate]);
|
|
70
73
|
// Form-scoped value shadows canvas value when inside a form.
|
|
71
|
-
const paramNamesForForm = useMemo(() => (param ? [param] : []), [param]);
|
|
72
|
-
const { inForm, values: formParams } = useFormParams(paramNamesForForm);
|
|
74
|
+
const paramNamesForForm = (0, react_1.useMemo)(() => (param ? [param] : []), [param]);
|
|
75
|
+
const { inForm, values: formParams } = (0, form_1.useFormParams)(paramNamesForForm);
|
|
73
76
|
const formValue = param ? formParams[param] : undefined;
|
|
74
77
|
// ── Initial value resolution ────────────────────────────────────────────
|
|
75
78
|
// Read snapshot once to seed local state — subsequent updates come via the
|
|
76
79
|
// sync effect below. Reading the bridge here is safe (synchronous).
|
|
77
|
-
const initialCanvasValue = useMemo(() => {
|
|
80
|
+
const initialCanvasValue = (0, react_1.useMemo)(() => {
|
|
78
81
|
if (!enabled || !param)
|
|
79
82
|
return undefined;
|
|
80
83
|
return bridge.params.getSnapshot(param);
|
|
@@ -86,24 +89,24 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
86
89
|
const processed = preprocessor(raw);
|
|
87
90
|
return validator(processed) ? processed : defaultValue;
|
|
88
91
|
};
|
|
89
|
-
const [value, setValueState] = useState(computeInitialValue);
|
|
90
|
-
const valueRef = useRef(value);
|
|
92
|
+
const [value, setValueState] = (0, react_1.useState)(computeInitialValue);
|
|
93
|
+
const valueRef = (0, react_1.useRef)(value);
|
|
91
94
|
valueRef.current = value;
|
|
92
|
-
const debounceRef = useRef(null);
|
|
93
|
-
const isLocalChangeRef = useRef(false);
|
|
94
|
-
const didBroadcastOnMountRef = useRef(false);
|
|
95
|
+
const debounceRef = (0, react_1.useRef)(null);
|
|
96
|
+
const isLocalChangeRef = (0, react_1.useRef)(false);
|
|
97
|
+
const didBroadcastOnMountRef = (0, react_1.useRef)(false);
|
|
95
98
|
// Stable closure over the latest binding for the unmount cleanup.
|
|
96
99
|
// bridge is captured by ref too so the cleanup effect's deps can stay `[]`
|
|
97
100
|
// — otherwise any bridge identity flip would re-run the effect and
|
|
98
101
|
// broadcast a stray CLEAR for every bound input, cascading through the
|
|
99
102
|
// canvas-param listener fan-out.
|
|
100
|
-
const latestBindingRef = useRef({ enabled, param });
|
|
103
|
+
const latestBindingRef = (0, react_1.useRef)({ enabled, param });
|
|
101
104
|
latestBindingRef.current = { enabled, param };
|
|
102
|
-
const bridgeRef = useRef(bridge);
|
|
105
|
+
const bridgeRef = (0, react_1.useRef)(bridge);
|
|
103
106
|
bridgeRef.current = bridge;
|
|
104
107
|
// ── Clear bound param when name changes (carry-over avoidance) ──────────
|
|
105
|
-
const prevParamRef = useRef(param);
|
|
106
|
-
useEffect(() => {
|
|
108
|
+
const prevParamRef = (0, react_1.useRef)(param);
|
|
109
|
+
(0, react_1.useEffect)(() => {
|
|
107
110
|
const prev = prevParamRef.current;
|
|
108
111
|
prevParamRef.current = param;
|
|
109
112
|
if (prev && prev !== param) {
|
|
@@ -111,7 +114,7 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
111
114
|
}
|
|
112
115
|
}, [bridge, param]);
|
|
113
116
|
// ── Subscribe to canvas (and form) updates ──────────────────────────────
|
|
114
|
-
useEffect(() => {
|
|
117
|
+
(0, react_1.useEffect)(() => {
|
|
115
118
|
if (!enabled || !param)
|
|
116
119
|
return;
|
|
117
120
|
if (isLocalChangeRef.current)
|
|
@@ -131,7 +134,7 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
131
134
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
135
|
}, [bridge, param, enabled, inForm, formValue, preprocessor, validator]);
|
|
133
136
|
// Subscribe to bridge canvas updates so changes from other nodes flow in.
|
|
134
|
-
useEffect(() => {
|
|
137
|
+
(0, react_1.useEffect)(() => {
|
|
135
138
|
if (!enabled || !param)
|
|
136
139
|
return;
|
|
137
140
|
const unsub = bridge.params.subscribe(param, () => {
|
|
@@ -150,16 +153,16 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
150
153
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
154
|
}, [bridge, param, enabled, preprocessor, validator]);
|
|
152
155
|
// ── Broadcast plumbing ──────────────────────────────────────────────────
|
|
153
|
-
const broadcast = useCallback((newValue) => {
|
|
156
|
+
const broadcast = (0, react_1.useCallback)((newValue) => {
|
|
154
157
|
if (!enabled || !param)
|
|
155
158
|
return;
|
|
156
|
-
bridge.params.set(param, newValue, ParameterMessageType.PARAM);
|
|
159
|
+
bridge.params.set(param, newValue, protocol_1.ParameterMessageType.PARAM);
|
|
157
160
|
bridge.edges.stopLoading();
|
|
158
161
|
logPreview("Broadcast", newValue);
|
|
159
162
|
},
|
|
160
163
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
161
164
|
[bridge, enabled, param]);
|
|
162
|
-
const broadcastNow = useCallback(() => {
|
|
165
|
+
const broadcastNow = (0, react_1.useCallback)(() => {
|
|
163
166
|
if (!enabled || readOnly)
|
|
164
167
|
return;
|
|
165
168
|
if (debounceRef.current) {
|
|
@@ -170,7 +173,7 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
170
173
|
broadcast(valueRef.current);
|
|
171
174
|
isLocalChangeRef.current = false;
|
|
172
175
|
}, [bridge, broadcast, enabled, readOnly]);
|
|
173
|
-
const clearValue = useCallback((newValue) => {
|
|
176
|
+
const clearValue = (0, react_1.useCallback)((newValue) => {
|
|
174
177
|
if (debounceRef.current) {
|
|
175
178
|
clearTimeout(debounceRef.current);
|
|
176
179
|
debounceRef.current = null;
|
|
@@ -187,11 +190,11 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
187
190
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
191
|
[bridge, enabled, param, readOnly]);
|
|
189
192
|
// Reset one-time mount broadcast guard when binding changes.
|
|
190
|
-
useEffect(() => {
|
|
193
|
+
(0, react_1.useEffect)(() => {
|
|
191
194
|
didBroadcastOnMountRef.current = false;
|
|
192
195
|
}, [param, enabled]);
|
|
193
196
|
// Broadcast default value on mount when no canvas value exists.
|
|
194
|
-
useEffect(() => {
|
|
197
|
+
(0, react_1.useEffect)(() => {
|
|
195
198
|
if (!enabled || !param || readOnly)
|
|
196
199
|
return;
|
|
197
200
|
if (!broadcastDefaultValue)
|
|
@@ -211,7 +214,7 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
211
214
|
broadcast(valueRef.current);
|
|
212
215
|
didBroadcastOnMountRef.current = true;
|
|
213
216
|
}, [bridge, enabled, param, readOnly, broadcastDefaultValue, broadcast]);
|
|
214
|
-
const setValue = useCallback((newValue) => {
|
|
217
|
+
const setValue = (0, react_1.useCallback)((newValue) => {
|
|
215
218
|
setValueState(newValue);
|
|
216
219
|
if (!enabled || readOnly)
|
|
217
220
|
return;
|
|
@@ -228,7 +231,7 @@ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defau
|
|
|
228
231
|
// type-swaps don't leave stale canvas state behind. Deps are `[]` so this
|
|
229
232
|
// runs on unmount only — never on bridge identity flips. bridge is read
|
|
230
233
|
// from the ref at cleanup time.
|
|
231
|
-
useEffect(() => {
|
|
234
|
+
(0, react_1.useEffect)(() => {
|
|
232
235
|
return () => {
|
|
233
236
|
if (debounceRef.current)
|
|
234
237
|
clearTimeout(debounceRef.current);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useJsonUiEdgeAnimation = useJsonUiEdgeAnimation;
|
|
4
|
+
const bridge_1 = require("../bridge");
|
|
2
5
|
/**
|
|
3
6
|
* Controls the edge-animation pellet for the current canvas node.
|
|
4
7
|
*
|
|
@@ -17,8 +20,8 @@ import { useFusedWidgetBridge } from "../bridge";
|
|
|
17
20
|
* try { await heavyCompute(); } finally { stopLoading(); }
|
|
18
21
|
* }
|
|
19
22
|
*/
|
|
20
|
-
|
|
21
|
-
const bridge = useFusedWidgetBridge();
|
|
23
|
+
function useJsonUiEdgeAnimation() {
|
|
24
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
22
25
|
return {
|
|
23
26
|
startLoading: bridge.edges.startLoading,
|
|
24
27
|
stopLoading: bridge.edges.stopLoading,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useJsonUiLog = useJsonUiLog;
|
|
4
|
+
exports.useJsonUiLogs = useJsonUiLogs;
|
|
5
|
+
exports.useJsonUiLogClear = useJsonUiLogClear;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const bridge_1 = require("../bridge");
|
|
3
8
|
const EMPTY_LOGS = Object.freeze([]);
|
|
4
9
|
/**
|
|
5
10
|
* Logging hook for json-ui components. Entries appear in the workbench's
|
|
@@ -16,22 +21,22 @@ const EMPTY_LOGS = Object.freeze([]);
|
|
|
16
21
|
* log("Selected city: " + city);
|
|
17
22
|
* log("Failed to load chart data", "error");
|
|
18
23
|
*/
|
|
19
|
-
|
|
20
|
-
const bridge = useFusedWidgetBridge();
|
|
21
|
-
const { configHash } = useJsonUiNode();
|
|
24
|
+
function useJsonUiLog() {
|
|
25
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
26
|
+
const { configHash } = (0, bridge_1.useJsonUiNode)();
|
|
22
27
|
// bridge and configHash are read from refs so the returned `log` callback
|
|
23
28
|
// identity is stable across re-renders. Downstream effects with `log` in
|
|
24
29
|
// their deps (e.g. use-duckdb-sql.ts's preprocessing effect) would
|
|
25
30
|
// otherwise re-fire on every bridge flip / configHash change and cascade
|
|
26
31
|
// setState across every SQL widget in the dashboard.
|
|
27
|
-
const bridgeRef = useRef(bridge);
|
|
32
|
+
const bridgeRef = (0, react_1.useRef)(bridge);
|
|
28
33
|
bridgeRef.current = bridge;
|
|
29
|
-
const configHashRef = useRef(configHash);
|
|
34
|
+
const configHashRef = (0, react_1.useRef)(configHash);
|
|
30
35
|
configHashRef.current = configHash;
|
|
31
|
-
const log = useCallback((message, level = "info") => {
|
|
36
|
+
const log = (0, react_1.useCallback)((message, level = "info") => {
|
|
32
37
|
bridgeRef.current.log.log(message, level, configHashRef.current);
|
|
33
38
|
}, []);
|
|
34
|
-
return useMemo(() => ({ log }), [log]);
|
|
39
|
+
return (0, react_1.useMemo)(() => ({ log }), [log]);
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
42
|
* Read-only hook returning the current log entries for a given node.
|
|
@@ -41,15 +46,15 @@ export function useJsonUiLog() {
|
|
|
41
46
|
* rarely need to read logs they wrote, but the hook is exposed for
|
|
42
47
|
* completeness (e.g. building a debug view component).
|
|
43
48
|
*/
|
|
44
|
-
|
|
45
|
-
const bridge = useFusedWidgetBridge();
|
|
46
|
-
const subscribe = useCallback((cb) => {
|
|
49
|
+
function useJsonUiLogs(nodeId) {
|
|
50
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
51
|
+
const subscribe = (0, react_1.useCallback)((cb) => {
|
|
47
52
|
if (!nodeId)
|
|
48
53
|
return () => { };
|
|
49
54
|
return bridge.log.subscribeLogs(nodeId, cb);
|
|
50
55
|
}, [bridge, nodeId]);
|
|
51
|
-
const snapshotRef = useRef(EMPTY_LOGS);
|
|
52
|
-
const getSnapshot = useCallback(() => {
|
|
56
|
+
const snapshotRef = (0, react_1.useRef)(EMPTY_LOGS);
|
|
57
|
+
const getSnapshot = (0, react_1.useCallback)(() => {
|
|
53
58
|
if (!nodeId)
|
|
54
59
|
return EMPTY_LOGS;
|
|
55
60
|
const next = bridge.log.getLogsSnapshot(nodeId);
|
|
@@ -58,15 +63,15 @@ export function useJsonUiLogs(nodeId) {
|
|
|
58
63
|
snapshotRef.current = next;
|
|
59
64
|
return next;
|
|
60
65
|
}, [bridge, nodeId]);
|
|
61
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
66
|
+
return (0, react_1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
62
67
|
}
|
|
63
68
|
/**
|
|
64
69
|
* Returns a stable callback that clears all log entries for the given node.
|
|
65
70
|
* Returns a no-op function when `nodeId` is undefined.
|
|
66
71
|
*/
|
|
67
|
-
|
|
68
|
-
const bridge = useFusedWidgetBridge();
|
|
69
|
-
return useCallback(() => {
|
|
72
|
+
function useJsonUiLogClear(nodeId) {
|
|
73
|
+
const bridge = (0, bridge_1.useFusedWidgetBridge)();
|
|
74
|
+
return (0, react_1.useCallback)(() => {
|
|
70
75
|
if (!nodeId)
|
|
71
76
|
return;
|
|
72
77
|
bridge.log.clearLogs(nodeId);
|