@oneuptime/common 10.2.20 → 10.2.21

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.
@@ -0,0 +1,694 @@
1
+ import React, { useEffect, useMemo, useRef, useState, } from "react";
2
+ import API from "../../Utils/API/API";
3
+ import { WORKFLOW_URL } from "../../Config";
4
+ import URL from "../../../Types/API/URL";
5
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
6
+ import ComponentLoader from "../ComponentLoader/ComponentLoader";
7
+ import CodeEditor from "../CodeEditor/CodeEditor";
8
+ import CodeType from "../../../Types/Code/CodeType";
9
+ import Icon from "../Icon/Icon";
10
+ import IconProp from "../../../Types/Icon/IconProp";
11
+ /*
12
+ * normalizeInitialValue turns the raw stored value (which may be a JSON
13
+ * string, an already-parsed object, null, or empty) into a `{ text, parsed }`
14
+ * pair. `parsed` is null when the text doesn't parse as a JSON object.
15
+ */
16
+ const normalizeInitialValue = (value) => {
17
+ if (value === null || value === undefined || value === "") {
18
+ return { text: "", parsed: null };
19
+ }
20
+ if (typeof value === "string") {
21
+ try {
22
+ const parsed = JSON.parse(value);
23
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
24
+ return { text: value, parsed: parsed };
25
+ }
26
+ return { text: value, parsed: null };
27
+ }
28
+ catch (_a) {
29
+ return { text: value, parsed: null };
30
+ }
31
+ }
32
+ // Already an object.
33
+ if (typeof value === "object" && !Array.isArray(value)) {
34
+ return {
35
+ text: JSON.stringify(value, null, 2),
36
+ parsed: value,
37
+ };
38
+ }
39
+ return { text: String(value), parsed: null };
40
+ };
41
+ /*
42
+ * Decide whether an existing select value can be represented by the picker.
43
+ * Returns a list of human-readable reasons when not — we surface those in a
44
+ * banner so the user understands why we're keeping their JSON as-is.
45
+ */
46
+ const classifyCompatibility = (text, parsed, columns) => {
47
+ // Empty value: picker can represent it (as nothing selected).
48
+ if (text.trim() === "" || parsed === null) {
49
+ if (text.trim() === "") {
50
+ return { compatible: true, reasons: [], parsed: {} };
51
+ }
52
+ return {
53
+ compatible: false,
54
+ reasons: ["The current value isn't valid JSON."],
55
+ parsed: null,
56
+ };
57
+ }
58
+ const reasons = [];
59
+ const columnsById = {};
60
+ for (const c of columns) {
61
+ columnsById[c.id] = c;
62
+ }
63
+ for (const key of Object.keys(parsed)) {
64
+ const column = columnsById[key];
65
+ if (!column) {
66
+ reasons.push(`"${key}" isn't a readable field on this model.`);
67
+ continue;
68
+ }
69
+ const value = parsed[key];
70
+ if (column.isRelation) {
71
+ if (value === true) {
72
+ /*
73
+ * The picker requires at least one sub-field for a relation. Hidden in
74
+ * the picker UI on purpose.
75
+ */
76
+ reasons.push(`"${key}" selects the whole relation. The picker requires you to pick specific sub-fields.`);
77
+ continue;
78
+ }
79
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
80
+ reasons.push(`"${key}" has an unexpected value.`);
81
+ continue;
82
+ }
83
+ const subColumnsById = {};
84
+ for (const sub of column.relatedColumns || []) {
85
+ subColumnsById[sub.id] = sub;
86
+ }
87
+ for (const subKey of Object.keys(value)) {
88
+ const subValue = value[subKey];
89
+ const subColumn = subColumnsById[subKey];
90
+ if (!subColumn) {
91
+ reasons.push(`"${key}.${subKey}" isn't a readable field on the related model.`);
92
+ continue;
93
+ }
94
+ if (subColumn.isRelation) {
95
+ reasons.push(`"${key}.${subKey}" is nested more than one level deep.`);
96
+ continue;
97
+ }
98
+ if (typeof subValue !== "boolean") {
99
+ if (typeof subValue === "object" && subValue !== null) {
100
+ reasons.push(`"${key}.${subKey}" is nested more than one level deep.`);
101
+ }
102
+ else {
103
+ reasons.push(`"${key}.${subKey}" has an unexpected value.`);
104
+ }
105
+ }
106
+ }
107
+ continue;
108
+ }
109
+ // Scalar column.
110
+ if (typeof value !== "boolean") {
111
+ reasons.push(`"${key}" has an unexpected value.`);
112
+ }
113
+ }
114
+ return {
115
+ compatible: reasons.length === 0,
116
+ reasons,
117
+ parsed,
118
+ };
119
+ };
120
+ /*
121
+ * Convert the picker's checkbox state into the JSON object that goes back
122
+ * into the workflow argument. Empty selections collapse to an empty string
123
+ * (rather than "{}") so existing "required" validation continues to work.
124
+ */
125
+ const buildSelectJson = (scalarChecks, relationChecks) => {
126
+ const out = {};
127
+ for (const id of scalarChecks) {
128
+ out[id] = true;
129
+ }
130
+ for (const relation of Object.keys(relationChecks)) {
131
+ const set = relationChecks[relation];
132
+ if (!set || set.size === 0) {
133
+ continue;
134
+ }
135
+ const subObj = {};
136
+ for (const subId of set) {
137
+ subObj[subId] = true;
138
+ }
139
+ out[relation] = subObj;
140
+ }
141
+ if (Object.keys(out).length === 0) {
142
+ return "";
143
+ }
144
+ return JSON.stringify(out);
145
+ };
146
+ const PickerCheckbox = (props) => {
147
+ const ref = useRef(null);
148
+ useEffect(() => {
149
+ if (ref.current) {
150
+ ref.current.indeterminate = Boolean(props.indeterminate);
151
+ }
152
+ }, [props.indeterminate]);
153
+ const sizeClass = props.size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
154
+ return (React.createElement("input", { ref: ref, type: "checkbox", checked: props.checked, onChange: props.onChange, onClick: (e) => {
155
+ // Prevent toggling parent label/row when the click originates here.
156
+ e.stopPropagation();
157
+ }, "aria-label": props.ariaLabel, className: `${sizeClass} rounded border-gray-300 text-indigo-600 focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 cursor-pointer` }));
158
+ };
159
+ const ModelFieldPicker = (props) => {
160
+ const [columns, setColumns] = useState(null);
161
+ const [isLoading, setIsLoading] = useState(true);
162
+ const [loadError, setLoadError] = useState(null);
163
+ const initial = useMemo(() => {
164
+ return normalizeInitialValue(props.initialValue);
165
+ }, []);
166
+ const [jsonText, setJsonText] = useState(initial.text);
167
+ const [viewMode, setViewMode] = useState("picker");
168
+ const [incompatibilityReasons, setIncompatibilityReasons] = useState([]);
169
+ const [isLockedToJson, setIsLockedToJson] = useState(false);
170
+ // Checkbox state for picker mode.
171
+ const [scalarChecks, setScalarChecks] = useState(new Set());
172
+ const [relationChecks, setRelationChecks] = useState({});
173
+ const [expandedRelations, setExpandedRelations] = useState(new Set());
174
+ const [searchQuery, setSearchQuery] = useState("");
175
+ // Avoid re-emitting onChange for the initial value (preserves byte-for-byte).
176
+ const hasInitializedFromColumns = useRef(false);
177
+ useEffect(() => {
178
+ let cancelled = false;
179
+ const loadSchema = async () => {
180
+ setIsLoading(true);
181
+ setLoadError(null);
182
+ try {
183
+ const url = URL.fromString(WORKFLOW_URL.toString()).addRoute(`/model-schema/${encodeURIComponent(props.tableName)}`);
184
+ const result = await API.get({ url });
185
+ if (cancelled) {
186
+ return;
187
+ }
188
+ if (result instanceof HTTPErrorResponse) {
189
+ throw result;
190
+ }
191
+ const data = result.data;
192
+ setColumns(data.columns || []);
193
+ }
194
+ catch (err) {
195
+ if (cancelled) {
196
+ return;
197
+ }
198
+ setLoadError(API.getFriendlyMessage(err));
199
+ setColumns([]);
200
+ }
201
+ finally {
202
+ if (!cancelled) {
203
+ setIsLoading(false);
204
+ }
205
+ }
206
+ };
207
+ void loadSchema();
208
+ return () => {
209
+ cancelled = true;
210
+ };
211
+ }, [props.tableName]);
212
+ /*
213
+ * Apply a classification result to picker state: either seed the checkboxes
214
+ * (compatible) or lock the editor to JSON mode with reasons (incompatible).
215
+ */
216
+ const applyClassification = (result, cols) => {
217
+ if (!result.compatible) {
218
+ setIncompatibilityReasons(result.reasons);
219
+ setIsLockedToJson(true);
220
+ setViewMode("json");
221
+ return;
222
+ }
223
+ const parsed = result.parsed || {};
224
+ const nextScalars = new Set();
225
+ const nextRelations = {};
226
+ const nextExpanded = new Set();
227
+ const columnsById = {};
228
+ for (const c of cols) {
229
+ columnsById[c.id] = c;
230
+ }
231
+ for (const key of Object.keys(parsed)) {
232
+ const column = columnsById[key];
233
+ if (!column) {
234
+ continue;
235
+ }
236
+ if (column.isRelation) {
237
+ const sub = parsed[key] || {};
238
+ const set = new Set();
239
+ for (const subKey of Object.keys(sub)) {
240
+ if (sub[subKey] === true) {
241
+ set.add(subKey);
242
+ }
243
+ }
244
+ if (set.size > 0) {
245
+ nextRelations[key] = set;
246
+ nextExpanded.add(key);
247
+ }
248
+ }
249
+ else if (parsed[key] === true) {
250
+ nextScalars.add(key);
251
+ }
252
+ }
253
+ setScalarChecks(nextScalars);
254
+ setRelationChecks(nextRelations);
255
+ setExpandedRelations(nextExpanded);
256
+ setIncompatibilityReasons([]);
257
+ setIsLockedToJson(false);
258
+ setViewMode("picker");
259
+ };
260
+ // Once we have columns, classify the initial value and seed picker state.
261
+ useEffect(() => {
262
+ if (columns === null) {
263
+ return;
264
+ }
265
+ if (hasInitializedFromColumns.current) {
266
+ return;
267
+ }
268
+ hasInitializedFromColumns.current = true;
269
+ const result = classifyCompatibility(initial.text, initial.parsed, columns);
270
+ applyClassification(result, columns);
271
+ }, [columns, initial.text, initial.parsed]);
272
+ // When the picker emits, push JSON upward.
273
+ const emitFromPicker = (scalars, relations) => {
274
+ const next = buildSelectJson(scalars, relations);
275
+ setJsonText(next);
276
+ props.onChange(next);
277
+ };
278
+ const toggleScalar = (id) => {
279
+ const next = new Set(scalarChecks);
280
+ if (next.has(id)) {
281
+ next.delete(id);
282
+ }
283
+ else {
284
+ next.add(id);
285
+ }
286
+ setScalarChecks(next);
287
+ emitFromPicker(next, relationChecks);
288
+ };
289
+ const toggleRelationField = (relation, sub) => {
290
+ const existing = new Set(relationChecks[relation] || []);
291
+ if (existing.has(sub)) {
292
+ existing.delete(sub);
293
+ }
294
+ else {
295
+ existing.add(sub);
296
+ }
297
+ const next = Object.assign({}, relationChecks);
298
+ if (existing.size === 0) {
299
+ delete next[relation];
300
+ }
301
+ else {
302
+ next[relation] = existing;
303
+ }
304
+ setRelationChecks(next);
305
+ emitFromPicker(scalarChecks, next);
306
+ };
307
+ const toggleExpand = (relation) => {
308
+ const next = new Set(expandedRelations);
309
+ if (next.has(relation)) {
310
+ next.delete(relation);
311
+ }
312
+ else {
313
+ next.add(relation);
314
+ }
315
+ setExpandedRelations(next);
316
+ };
317
+ /*
318
+ * Select-all / clear helpers operate on whatever's currently visible so
319
+ * the actions feel scoped to what the user can see.
320
+ */
321
+ const setScalarSelection = (ids) => {
322
+ const next = new Set(ids);
323
+ setScalarChecks(next);
324
+ emitFromPicker(next, relationChecks);
325
+ };
326
+ const setRelationSelection = (relation, ids) => {
327
+ const next = Object.assign({}, relationChecks);
328
+ if (ids.length === 0) {
329
+ delete next[relation];
330
+ }
331
+ else {
332
+ next[relation] = new Set(ids);
333
+ }
334
+ setRelationChecks(next);
335
+ emitFromPicker(scalarChecks, next);
336
+ };
337
+ const expandRelation = (relation) => {
338
+ if (expandedRelations.has(relation)) {
339
+ return;
340
+ }
341
+ const next = new Set(expandedRelations);
342
+ next.add(relation);
343
+ setExpandedRelations(next);
344
+ };
345
+ const onSearchChange = (value) => {
346
+ setSearchQuery(value);
347
+ };
348
+ /*
349
+ * Friendly type-pill label. Returns null for plain text-like columns so the
350
+ * common case stays uncluttered.
351
+ */
352
+ const getTypeLabel = (type) => {
353
+ const map = {
354
+ Date: "Date",
355
+ Boolean: "Yes / No",
356
+ Number: "Number",
357
+ "Big Number": "Number",
358
+ "Small Number": "Number",
359
+ "Positive Number": "Number",
360
+ "Small Positive Number": "Number",
361
+ "Big Positive Number": "Number",
362
+ "Object ID": "ID",
363
+ Slug: "Slug",
364
+ URL: "URL",
365
+ Email: "Email",
366
+ Phone: "Phone",
367
+ IP: "IP",
368
+ Port: "Port",
369
+ Color: "Color",
370
+ JSON: "JSON",
371
+ Markdown: "Markdown",
372
+ HTML: "HTML",
373
+ "Entity Array": "List",
374
+ };
375
+ return map[type] || null;
376
+ };
377
+ const matchesQuery = (column, query) => {
378
+ if (!query) {
379
+ return true;
380
+ }
381
+ return (column.title.toLowerCase().includes(query) ||
382
+ column.id.toLowerCase().includes(query) ||
383
+ (column.description || "").toLowerCase().includes(query));
384
+ };
385
+ const onJsonChange = (next) => {
386
+ setJsonText(next);
387
+ props.onChange(next);
388
+ };
389
+ const resetToPicker = () => {
390
+ setIncompatibilityReasons([]);
391
+ setIsLockedToJson(false);
392
+ setScalarChecks(new Set());
393
+ setRelationChecks({});
394
+ setExpandedRelations(new Set());
395
+ setJsonText("");
396
+ props.onChange("");
397
+ setViewMode("picker");
398
+ };
399
+ if (isLoading) {
400
+ return (React.createElement("div", { className: "py-10" },
401
+ React.createElement(ComponentLoader, null)));
402
+ }
403
+ const scalarColumns = (columns || []).filter((c) => {
404
+ return !c.isRelation;
405
+ });
406
+ const relationColumns = (columns || []).filter((c) => {
407
+ return c.isRelation;
408
+ });
409
+ const totalSelected = scalarChecks.size +
410
+ Object.values(relationChecks).reduce((acc, s) => {
411
+ return acc + s.size;
412
+ }, 0);
413
+ const totalAvailable = scalarColumns.length +
414
+ relationColumns.reduce((acc, r) => {
415
+ var _a;
416
+ return acc + (((_a = r.relatedColumns) === null || _a === void 0 ? void 0 : _a.length) || 0);
417
+ }, 0);
418
+ const onUsePickerClicked = () => {
419
+ if (!columns) {
420
+ return;
421
+ }
422
+ // Re-classify the current JSON text — the user may have edited it.
423
+ const norm = normalizeInitialValue(jsonText);
424
+ const result = classifyCompatibility(norm.text, norm.parsed, columns);
425
+ applyClassification(result, columns);
426
+ };
427
+ /*
428
+ * Apply search filter. For relations, the relation passes if its own title
429
+ * matches OR any sub-field matches; in the latter case we auto-expand it so
430
+ * the matching child is visible.
431
+ */
432
+ const query = searchQuery.trim().toLowerCase();
433
+ const visibleScalars = scalarColumns.filter((c) => {
434
+ return matchesQuery(c, query);
435
+ });
436
+ const visibleRelations = relationColumns
437
+ .map((column) => {
438
+ const subMatches = (column.relatedColumns || []).filter((sub) => {
439
+ return matchesQuery(sub, query);
440
+ });
441
+ const matchesSelf = matchesQuery(column, query);
442
+ if (query && !matchesSelf && subMatches.length === 0) {
443
+ return null;
444
+ }
445
+ return {
446
+ column,
447
+ visibleSubs: query && !matchesSelf ? subMatches : column.relatedColumns || [],
448
+ matchedByChild: query.length > 0 && !matchesSelf && subMatches.length > 0,
449
+ };
450
+ })
451
+ .filter((x) => {
452
+ return x !== null;
453
+ });
454
+ const hasNoVisibleResults = visibleScalars.length === 0 && visibleRelations.length === 0;
455
+ const selectAllScalars = () => {
456
+ const ids = visibleScalars.map((c) => {
457
+ return c.id;
458
+ });
459
+ // Merge with existing so out-of-view (filtered) selections aren't lost.
460
+ const merged = new Set(scalarChecks);
461
+ for (const id of ids) {
462
+ merged.add(id);
463
+ }
464
+ setScalarSelection(Array.from(merged));
465
+ };
466
+ const clearAllScalars = () => {
467
+ // Only clear what's visible — preserves selections hidden by search.
468
+ const next = new Set(scalarChecks);
469
+ for (const c of visibleScalars) {
470
+ next.delete(c.id);
471
+ }
472
+ setScalarSelection(Array.from(next));
473
+ };
474
+ /*
475
+ * Renders one selectable field row. Used for both top-level scalars and
476
+ * relation sub-fields (which pass a denser variant via `dense`).
477
+ */
478
+ const renderFieldRow = (args) => {
479
+ const { column, checked, onToggle, dense } = args;
480
+ const typeLabel = getTypeLabel(column.type);
481
+ return (React.createElement("label", { className: `group relative flex items-center gap-3 ${dense ? "py-1.5" : "py-2"} px-3 cursor-pointer select-none transition-colors ${checked ? "bg-indigo-50/40" : "hover:bg-gray-50/80"}` },
482
+ checked && (React.createElement("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-indigo-500", "aria-hidden": "true" })),
483
+ React.createElement("input", { type: "checkbox", checked: checked, onChange: onToggle, className: "h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 cursor-pointer" }),
484
+ React.createElement("span", { className: "flex-1 min-w-0" },
485
+ React.createElement("span", { className: "flex items-baseline gap-2" },
486
+ React.createElement("span", { className: `truncate text-sm ${checked
487
+ ? "font-medium text-gray-900"
488
+ : "font-medium text-gray-800"}` }, column.title),
489
+ typeLabel && (React.createElement("span", { className: "ml-auto flex-shrink-0 text-[11px] uppercase tracking-wider text-gray-400 group-hover:text-gray-500" }, typeLabel))),
490
+ column.description && (React.createElement("span", { className: "block text-xs text-gray-500 truncate mt-0.5", title: column.description }, column.description)))));
491
+ };
492
+ /*
493
+ * Top-level toolbar above everything: search on the left (when in picker
494
+ * mode), then count, then the view-mode toggle. Kept as a single tight row
495
+ * so it doesn't visually compete with the field list below.
496
+ */
497
+ const renderToolbar = () => {
498
+ const showSearch = viewMode === "picker" && !isLockedToJson && totalAvailable > 0;
499
+ let toggleLabel;
500
+ let toggleIcon;
501
+ let toggleHandler;
502
+ if (isLockedToJson) {
503
+ toggleLabel = "Reset to picker";
504
+ toggleIcon = IconProp.Refresh;
505
+ toggleHandler = resetToPicker;
506
+ }
507
+ else if (viewMode === "picker") {
508
+ toggleLabel = "JSON";
509
+ toggleIcon = IconProp.Code;
510
+ toggleHandler = () => {
511
+ setViewMode("json");
512
+ };
513
+ }
514
+ else {
515
+ toggleLabel = "Picker";
516
+ toggleIcon = IconProp.ListBullet;
517
+ toggleHandler = onUsePickerClicked;
518
+ }
519
+ return (React.createElement("div", { className: "flex items-center gap-2" },
520
+ showSearch && (React.createElement("div", { className: "relative flex-1" },
521
+ React.createElement("span", { className: "pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3" },
522
+ React.createElement(Icon, { icon: IconProp.MagnifyingGlass, className: "h-4 w-4 text-gray-400" })),
523
+ React.createElement("input", { type: "text", value: searchQuery, onChange: (e) => {
524
+ return onSearchChange(e.target.value);
525
+ }, placeholder: `Search ${totalAvailable} fields`, className: "block w-full rounded-md border border-gray-200 bg-white pl-9 pr-8 py-1.5 text-sm placeholder-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" }),
526
+ searchQuery && (React.createElement("button", { type: "button", onClick: () => {
527
+ return setSearchQuery("");
528
+ }, className: "absolute inset-y-0 right-0 flex items-center pr-2.5 text-gray-400 hover:text-gray-600", "aria-label": "Clear search" },
529
+ React.createElement(Icon, { icon: IconProp.Close, className: "h-3.5 w-3.5" }))))),
530
+ !showSearch && React.createElement("div", { className: "flex-1" }),
531
+ !isLockedToJson && totalAvailable > 0 && viewMode === "picker" && (React.createElement("span", { className: "text-xs text-gray-500 whitespace-nowrap font-mono" },
532
+ React.createElement("span", { className: `${totalSelected > 0
533
+ ? "font-semibold text-indigo-600"
534
+ : "text-gray-700"}` }, totalSelected),
535
+ React.createElement("span", { className: "text-gray-300 mx-1" }, "/"),
536
+ React.createElement("span", null, totalAvailable))),
537
+ React.createElement("button", { type: "button", onClick: toggleHandler, className: "inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:ring-1 focus:ring-indigo-500" },
538
+ React.createElement(Icon, { icon: toggleIcon, className: "h-3.5 w-3.5 text-gray-500" }),
539
+ toggleLabel)));
540
+ };
541
+ const renderSectionHeader = (args) => {
542
+ return (React.createElement("div", { className: "flex items-center justify-between px-3 pt-3 pb-1.5" },
543
+ React.createElement("span", { className: "text-[10px] font-semibold uppercase tracking-[0.08em] text-gray-400" }, args.label),
544
+ args.actions ? (React.createElement("div", { className: "flex items-center gap-1 text-[11px]" }, args.actions)) : null));
545
+ };
546
+ const renderSectionActions = (args) => {
547
+ return (React.createElement(React.Fragment, null,
548
+ React.createElement("button", { type: "button", onClick: args.onSelectAll, className: "font-medium text-indigo-600 hover:text-indigo-700" }, "Select all"),
549
+ React.createElement("span", { className: "text-gray-300" }, "\u00B7"),
550
+ React.createElement("button", { type: "button", onClick: args.onClear, className: "font-medium text-gray-500 hover:text-gray-700" }, "Clear")));
551
+ };
552
+ return (React.createElement("div", { className: "space-y-2" },
553
+ renderToolbar(),
554
+ loadError && (React.createElement("div", { className: "flex items-start gap-2.5 rounded-md border border-red-200 bg-red-50/60 px-3 py-2 text-xs text-red-800" },
555
+ React.createElement(Icon, { icon: IconProp.AltGlobe, className: "h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" }),
556
+ React.createElement("div", null,
557
+ React.createElement("p", { className: "font-semibold" }, "Could not load fields"),
558
+ React.createElement("p", { className: "text-red-700" }, loadError)))),
559
+ isLockedToJson && (React.createElement("div", { className: "rounded-md border border-amber-200 bg-amber-50/60 px-3 py-2.5" },
560
+ React.createElement("div", { className: "flex items-start gap-2.5" },
561
+ React.createElement(Icon, { icon: IconProp.Info, className: "h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" }),
562
+ React.createElement("div", { className: "text-xs text-amber-900 space-y-1" },
563
+ React.createElement("p", { className: "font-semibold" }, "Keeping your selection as JSON"),
564
+ React.createElement("ul", { className: "text-amber-800 space-y-0.5 list-disc pl-4" }, incompatibilityReasons.map((reason, i) => {
565
+ return React.createElement("li", { key: i }, reason);
566
+ })),
567
+ React.createElement("p", { className: "pt-0.5 text-amber-700/80" },
568
+ "Your workflow keeps running unchanged. Edit the JSON below, or click ",
569
+ React.createElement("strong", null, "Reset to picker"),
570
+ " to start fresh."))))),
571
+ (viewMode === "json" || loadError) && (React.createElement("div", { className: "rounded-md border border-gray-200 overflow-hidden" },
572
+ React.createElement(CodeEditor, { type: CodeType.JSON, value: jsonText, onChange: onJsonChange, placeholder: props.placeholder || 'Example: {"columnName": true, ...}', error: props.error, tabIndex: props.tabIndex }))),
573
+ viewMode === "picker" && !loadError && (React.createElement("div", { className: "rounded-md border border-gray-200 bg-white" },
574
+ totalAvailable === 0 && (React.createElement("div", { className: "px-6 py-12 text-center" },
575
+ React.createElement(Icon, { icon: IconProp.Database, className: "h-6 w-6 mx-auto text-gray-300 mb-2" }),
576
+ React.createElement("p", { className: "text-sm text-gray-500" }, "No readable fields are available for this model."))),
577
+ totalAvailable > 0 && hasNoVisibleResults && (React.createElement("div", { className: "px-6 py-10 text-center" },
578
+ React.createElement(Icon, { icon: IconProp.MagnifyingGlass, className: "h-5 w-5 mx-auto text-gray-300 mb-2" }),
579
+ React.createElement("p", { className: "text-sm text-gray-600" },
580
+ "No fields match",
581
+ " ",
582
+ React.createElement("span", { className: "font-medium text-gray-900" },
583
+ "\u201C",
584
+ searchQuery,
585
+ "\u201D")),
586
+ React.createElement("button", { type: "button", onClick: () => {
587
+ return setSearchQuery("");
588
+ }, className: "mt-2 text-xs font-medium text-indigo-600 hover:text-indigo-700" }, "Clear search"))),
589
+ visibleScalars.length > 0 && (React.createElement(React.Fragment, null,
590
+ renderSectionHeader({
591
+ label: "Fields",
592
+ actions: renderSectionActions({
593
+ onSelectAll: selectAllScalars,
594
+ onClear: clearAllScalars,
595
+ }),
596
+ }),
597
+ React.createElement("div", { className: "divide-y divide-gray-100" }, visibleScalars.map((column) => {
598
+ return (React.createElement(React.Fragment, { key: column.id }, renderFieldRow({
599
+ column,
600
+ checked: scalarChecks.has(column.id),
601
+ onToggle: () => {
602
+ toggleScalar(column.id);
603
+ },
604
+ })));
605
+ })))),
606
+ visibleScalars.length > 0 && visibleRelations.length > 0 && (React.createElement("div", { className: "border-t border-gray-100 mt-2" })),
607
+ visibleRelations.length > 0 && (React.createElement(React.Fragment, null,
608
+ renderSectionHeader({
609
+ label: "Related records",
610
+ actions: (React.createElement("span", { className: "text-[10px] font-medium uppercase tracking-wider text-gray-300" }, "1 level")),
611
+ }),
612
+ React.createElement("div", { className: "divide-y divide-gray-100" }, visibleRelations.map((entry) => {
613
+ const column = entry.column;
614
+ const visibleSubs = entry.visibleSubs;
615
+ const totalSubs = (column.relatedColumns || [])
616
+ .length;
617
+ const selectedSubs = relationChecks[column.id] || new Set();
618
+ const isExpanded = expandedRelations.has(column.id) || entry.matchedByChild;
619
+ const allChecked = totalSubs > 0 && selectedSubs.size === totalSubs;
620
+ const someChecked = selectedSubs.size > 0 && !allChecked;
621
+ const toggleAllSubs = () => {
622
+ if (allChecked) {
623
+ setRelationSelection(column.id, []);
624
+ }
625
+ else {
626
+ const ids = (column.relatedColumns || []).map((s) => {
627
+ return s.id;
628
+ });
629
+ setRelationSelection(column.id, ids);
630
+ expandRelation(column.id);
631
+ }
632
+ };
633
+ const selectAllSubs = () => {
634
+ const ids = visibleSubs.map((s) => {
635
+ return s.id;
636
+ });
637
+ const merged = new Set(selectedSubs);
638
+ for (const id of ids) {
639
+ merged.add(id);
640
+ }
641
+ setRelationSelection(column.id, Array.from(merged));
642
+ };
643
+ const clearSubs = () => {
644
+ const next = new Set(selectedSubs);
645
+ for (const s of visibleSubs) {
646
+ next.delete(s.id);
647
+ }
648
+ setRelationSelection(column.id, Array.from(next));
649
+ };
650
+ return (React.createElement("div", { key: column.id, className: `${isExpanded ? "bg-gray-50/40" : ""}` },
651
+ React.createElement("div", { className: `relative flex items-center gap-3 px-3 py-2 transition-colors ${isExpanded ? "" : "hover:bg-gray-50/80"}` },
652
+ (allChecked || someChecked) && (React.createElement("span", { className: "absolute left-0 top-1 bottom-1 w-0.5 rounded-r bg-indigo-500", "aria-hidden": "true" })),
653
+ React.createElement(PickerCheckbox, { checked: allChecked, indeterminate: someChecked, onChange: toggleAllSubs, ariaLabel: `Select all from ${column.title}` }),
654
+ React.createElement("button", { type: "button", onClick: () => {
655
+ return toggleExpand(column.id);
656
+ }, className: "flex-1 flex items-center justify-between gap-2 text-left min-w-0" },
657
+ React.createElement("span", { className: "min-w-0 flex items-baseline gap-2" },
658
+ React.createElement("span", { className: `truncate text-sm font-medium ${allChecked || someChecked
659
+ ? "text-gray-900"
660
+ : "text-gray-800"}` }, column.title),
661
+ column.relatedTableName && (React.createElement("span", { className: "text-[11px] text-gray-400" }, column.relatedTableName))),
662
+ React.createElement("span", { className: "flex items-center gap-2 flex-shrink-0" },
663
+ React.createElement("span", { className: "text-[11px] text-gray-500 font-mono whitespace-nowrap" },
664
+ React.createElement("span", { className: selectedSubs.size > 0
665
+ ? "font-semibold text-indigo-600"
666
+ : "text-gray-400" }, selectedSubs.size),
667
+ React.createElement("span", { className: "text-gray-300 mx-1" }, "/"),
668
+ React.createElement("span", null, totalSubs)),
669
+ React.createElement(Icon, { icon: isExpanded
670
+ ? IconProp.ChevronDown
671
+ : IconProp.ChevronRight, className: "h-3.5 w-3.5 text-gray-400" })))),
672
+ isExpanded && (React.createElement("div", { className: "pl-7 pr-0 pb-1 border-l-2 border-indigo-100 ml-5 mb-1" },
673
+ React.createElement("div", { className: "flex items-center justify-between px-3 py-1.5" },
674
+ React.createElement("span", { className: "text-[10px] uppercase tracking-wider text-gray-400 font-medium" }, "Sub-fields"),
675
+ React.createElement("div", { className: "flex items-center gap-1 text-[11px]" }, renderSectionActions({
676
+ onSelectAll: selectAllSubs,
677
+ onClear: clearSubs,
678
+ }))),
679
+ React.createElement("div", { className: "divide-y divide-gray-100" }, visibleSubs.map((sub) => {
680
+ return (React.createElement(React.Fragment, { key: sub.id }, renderFieldRow({
681
+ column: sub,
682
+ checked: selectedSubs.has(sub.id),
683
+ onToggle: () => {
684
+ toggleRelationField(column.id, sub.id);
685
+ },
686
+ dense: true,
687
+ })));
688
+ }))))));
689
+ })))),
690
+ (visibleScalars.length > 0 || visibleRelations.length > 0) && (React.createElement("div", { className: "py-1" })))),
691
+ props.error && (React.createElement("p", { className: "text-xs text-red-600 mt-0.5", "data-testid": "error-message" }, props.error))));
692
+ };
693
+ export default ModelFieldPicker;
694
+ //# sourceMappingURL=ModelFieldPicker.js.map