@open-mercato/ui 0.6.5-develop.5116.1.f0af9e5080 → 0.6.5-develop.5155.1.148d10a46d
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.
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
SelectValue
|
|
14
14
|
} from "../primitives/select.js";
|
|
15
15
|
import { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from "lucide-react";
|
|
16
|
+
import { useConfirmDialog } from "./confirm-dialog/index.js";
|
|
16
17
|
function cn(...classes) {
|
|
17
18
|
return classes.filter(Boolean).join(" ");
|
|
18
19
|
}
|
|
@@ -21,6 +22,27 @@ function getJsonType(value) {
|
|
|
21
22
|
if (Array.isArray(value)) return "array";
|
|
22
23
|
return typeof value;
|
|
23
24
|
}
|
|
25
|
+
function defaultValueForType(type) {
|
|
26
|
+
switch (type) {
|
|
27
|
+
case "string":
|
|
28
|
+
return "";
|
|
29
|
+
case "number":
|
|
30
|
+
return 0;
|
|
31
|
+
case "boolean":
|
|
32
|
+
return false;
|
|
33
|
+
case "object":
|
|
34
|
+
return {};
|
|
35
|
+
case "array":
|
|
36
|
+
return [];
|
|
37
|
+
case "null":
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function toRawString(value) {
|
|
42
|
+
if (value === null || value === void 0) return "{}";
|
|
43
|
+
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
24
46
|
function JsonBuilder({
|
|
25
47
|
value,
|
|
26
48
|
onChange,
|
|
@@ -28,26 +50,24 @@ function JsonBuilder({
|
|
|
28
50
|
error
|
|
29
51
|
}) {
|
|
30
52
|
const [mode, setMode] = React.useState("raw");
|
|
31
|
-
const [rawString, setRawString] = React.useState(() =>
|
|
32
|
-
|
|
33
|
-
return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value || "{}");
|
|
34
|
-
});
|
|
53
|
+
const [rawString, setRawString] = React.useState(() => toRawString(value));
|
|
54
|
+
const [rawDirty, setRawDirty] = React.useState(false);
|
|
35
55
|
const [parseError, setParseError] = React.useState(null);
|
|
56
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
36
57
|
React.useEffect(() => {
|
|
37
|
-
if (value === null) {
|
|
38
|
-
|
|
39
|
-
onChange({});
|
|
40
|
-
}
|
|
41
|
-
setRawString("{}");
|
|
42
|
-
setParseError(null);
|
|
43
|
-
return;
|
|
58
|
+
if (value === null && !disabled) {
|
|
59
|
+
onChange({});
|
|
44
60
|
}
|
|
45
|
-
|
|
61
|
+
}, [value, disabled, onChange]);
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (rawDirty) return;
|
|
64
|
+
if (value !== null && typeof value === "object") {
|
|
46
65
|
setRawString(JSON.stringify(value, null, 2));
|
|
47
66
|
setParseError(null);
|
|
48
67
|
}
|
|
49
|
-
}, [value,
|
|
68
|
+
}, [value, rawDirty]);
|
|
50
69
|
const handleRawChange = (str) => {
|
|
70
|
+
setRawDirty(true);
|
|
51
71
|
setRawString(str);
|
|
52
72
|
try {
|
|
53
73
|
if (str.trim() === "") {
|
|
@@ -63,6 +83,12 @@ function JsonBuilder({
|
|
|
63
83
|
setParseError("Invalid JSON");
|
|
64
84
|
}
|
|
65
85
|
};
|
|
86
|
+
const switchToRaw = () => {
|
|
87
|
+
setRawDirty(false);
|
|
88
|
+
setRawString(toRawString(value));
|
|
89
|
+
setParseError(null);
|
|
90
|
+
setMode("raw");
|
|
91
|
+
};
|
|
66
92
|
const switchToBuilder = () => {
|
|
67
93
|
try {
|
|
68
94
|
if (typeof value === "string") {
|
|
@@ -82,7 +108,7 @@ function JsonBuilder({
|
|
|
82
108
|
variant: "ghost",
|
|
83
109
|
size: "sm",
|
|
84
110
|
className: cn(mode === "raw" && "bg-muted text-foreground"),
|
|
85
|
-
onClick:
|
|
111
|
+
onClick: switchToRaw,
|
|
86
112
|
children: [
|
|
87
113
|
/* @__PURE__ */ jsx(Code, { className: "w-4 h-4" }),
|
|
88
114
|
"Raw JSON"
|
|
@@ -129,39 +155,32 @@ function JsonBuilder({
|
|
|
129
155
|
data: value,
|
|
130
156
|
onChange,
|
|
131
157
|
readOnly: disabled,
|
|
158
|
+
confirm,
|
|
132
159
|
isRoot: true
|
|
133
160
|
}
|
|
134
161
|
) : /* @__PURE__ */ jsx("div", { className: "text-muted-foreground italic p-4 text-center", children: "Value is not an object or array. Switch to Raw to edit." }) }),
|
|
135
|
-
error && /* @__PURE__ */ jsx("div", { className: "text-xs text-red-600", children: error })
|
|
162
|
+
error && /* @__PURE__ */ jsx("div", { className: "text-xs text-red-600", children: error }),
|
|
163
|
+
ConfirmDialogElement
|
|
136
164
|
] });
|
|
137
165
|
}
|
|
138
|
-
function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }) {
|
|
166
|
+
function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot, confirm }) {
|
|
139
167
|
const type = getJsonType(data);
|
|
140
168
|
const isContainer = type === "object" || type === "array";
|
|
141
169
|
const [collapsed, setCollapsed] = React.useState(false);
|
|
142
|
-
const handleTypeChange = (newType) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
case "object":
|
|
155
|
-
newVal = {};
|
|
156
|
-
break;
|
|
157
|
-
case "array":
|
|
158
|
-
newVal = [];
|
|
159
|
-
break;
|
|
160
|
-
case "null":
|
|
161
|
-
newVal = null;
|
|
162
|
-
break;
|
|
170
|
+
const handleTypeChange = async (newType) => {
|
|
171
|
+
if (newType === type) return;
|
|
172
|
+
const itemCount = type === "object" ? Object.keys(data).length : type === "array" ? data.length : 0;
|
|
173
|
+
if (itemCount > 0 && confirm) {
|
|
174
|
+
const noun = type === "object" ? `${itemCount} ${itemCount === 1 ? "property" : "properties"}` : `${itemCount} ${itemCount === 1 ? "item" : "items"}`;
|
|
175
|
+
const confirmed = await confirm({
|
|
176
|
+
title: "Change value type?",
|
|
177
|
+
text: `Changing the type from ${type} to ${newType} will discard the ${noun} currently configured. This cannot be undone.`,
|
|
178
|
+
confirmText: "Discard and change",
|
|
179
|
+
variant: "destructive"
|
|
180
|
+
});
|
|
181
|
+
if (!confirmed) return;
|
|
163
182
|
}
|
|
164
|
-
onChange(
|
|
183
|
+
onChange(defaultValueForType(newType));
|
|
165
184
|
};
|
|
166
185
|
const handleAddKey = () => {
|
|
167
186
|
if (type === "object") {
|
|
@@ -216,7 +235,9 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }) {
|
|
|
216
235
|
Select,
|
|
217
236
|
{
|
|
218
237
|
value: type,
|
|
219
|
-
onValueChange: (next) =>
|
|
238
|
+
onValueChange: (next) => {
|
|
239
|
+
void handleTypeChange(next);
|
|
240
|
+
},
|
|
220
241
|
children: [
|
|
221
242
|
/* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "w-auto min-w-[6rem]", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
222
243
|
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
@@ -300,7 +321,8 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }) {
|
|
|
300
321
|
data: val,
|
|
301
322
|
onChange: (v) => handleChildChange(key, v),
|
|
302
323
|
onDelete: () => handleChildDelete(key),
|
|
303
|
-
readOnly
|
|
324
|
+
readOnly,
|
|
325
|
+
confirm
|
|
304
326
|
}
|
|
305
327
|
) })
|
|
306
328
|
] }, idx)),
|
|
@@ -311,7 +333,8 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }) {
|
|
|
311
333
|
data: val,
|
|
312
334
|
onChange: (v) => handleChildChange(idx, v),
|
|
313
335
|
onDelete: () => handleChildDelete(idx),
|
|
314
|
-
readOnly
|
|
336
|
+
readOnly,
|
|
337
|
+
confirm
|
|
315
338
|
},
|
|
316
339
|
idx
|
|
317
340
|
)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/backend/JsonBuilder.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Input } from '../primitives/input'\nimport { InlineInput } from '../primitives/inline-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'\n\nfunction cn(...classes: (string | undefined | null | false)[]) {\n return classes.filter(Boolean).join(' ')\n}\n\nexport type JsonBuilderProps = {\n value: any\n onChange: (value: any) => void\n disabled?: boolean\n error?: string\n}\n\ntype JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'\n\nfunction getJsonType(value: any): JsonNodeType {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonNodeType\n}\n\nexport function JsonBuilder({\n value,\n onChange,\n disabled,\n error\n}: JsonBuilderProps) {\n const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')\n const [rawString, setRawString] = React.useState(() => {\n if (value === null) return '{}'\n return typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '{}')\n })\n const [parseError, setParseError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (value === null) {\n if (!disabled) {\n onChange({})\n }\n setRawString('{}')\n setParseError(null)\n return\n }\n if (typeof value === 'object') {\n setRawString(JSON.stringify(value, null, 2))\n setParseError(null)\n }\n }, [value, disabled, onChange])\n\n const handleRawChange = (str: string) => {\n setRawString(str)\n try {\n if (str.trim() === '') {\n onChange({})\n setParseError(null)\n } else {\n const parsed = JSON.parse(str)\n onChange(parsed)\n setParseError(null)\n }\n } catch (e) {\n onChange(str)\n setParseError(\"Invalid JSON\")\n }\n }\n\n const switchToBuilder = () => {\n try {\n if (typeof value === 'string') {\n JSON.parse(value)\n }\n setMode('builder')\n } catch (e) {\n alert(\"Cannot switch to Builder mode: Invalid JSON\")\n }\n }\n\n return (\n <div className=\"space-y-4 border rounded-md p-4 bg-card\">\n <div className=\"flex items-center space-x-2 border-b pb-2 mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'raw' && \"bg-muted text-foreground\")}\n onClick={() => setMode('raw')}\n >\n <Code className=\"w-4 h-4\" />\n Raw JSON\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'builder' && \"bg-muted text-foreground\")}\n onClick={switchToBuilder}\n >\n <LayoutList className=\"w-4 h-4\" />\n Builder\n </Button>\n </div>\n\n {mode === 'raw' ? (\n <div className=\"space-y-2\">\n <textarea\n value={rawString}\n onChange={(e) => handleRawChange(e.target.value)}\n onBlur={() => {\n try {\n const parsed = JSON.parse(rawString)\n setRawString(JSON.stringify(parsed, null, 2))\n } catch { }\n }}\n placeholder='{\"key\": \"value\"}'\n className=\"w-full rounded border px-3 py-2 min-h-[300px] text-sm font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n disabled={disabled}\n />\n {parseError && (\n <div className=\"text-xs text-red-600\">Invalid JSON format</div>\n )}\n </div>\n ) : (\n <div className=\"min-h-[300px] text-sm overflow-x-auto\">\n {typeof value === 'object' && value !== null ? (\n <JsonNode\n data={value}\n onChange={onChange}\n readOnly={disabled}\n isRoot\n />\n ) : (\n <div className=\"text-muted-foreground italic p-4 text-center\">\n Value is not an object or array. Switch to Raw to edit.\n </div>\n )}\n </div>\n )}\n\n {error && <div className=\"text-xs text-red-600\">{error}</div>}\n </div>\n )\n}\n\ninterface JsonNodeProps {\n data: any\n onChange: (val: any) => void\n onDelete?: () => void\n readOnly?: boolean\n label?: string\n isRoot?: boolean\n}\n\nfunction JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNodeProps) {\n const type = getJsonType(data)\n const isContainer = type === 'object' || type === 'array'\n const [collapsed, setCollapsed] = React.useState(false)\n\n const handleTypeChange = (newType: JsonNodeType) => {\n let newVal: any = ''\n switch (newType) {\n case 'string': newVal = \"\"; break;\n case 'number': newVal = 0; break;\n case 'boolean': newVal = false; break;\n case 'object': newVal = {}; break;\n case 'array': newVal = []; break;\n case 'null': newVal = null; break;\n }\n onChange(newVal)\n }\n\n const handleAddKey = () => {\n if (type === 'object') {\n const newKey = `newKey_${Object.keys(data).length}`\n onChange({ ...data, [newKey]: \"\" })\n } else if (type === 'array') {\n onChange([...data, \"\"])\n }\n }\n\n const handleChildChange = (key: string | number, newVal: any) => {\n if (type === 'object') {\n onChange({ ...data, [key]: newVal })\n } else if (type === 'array') {\n const arr = [...data]\n arr[Number(key)] = newVal\n onChange(arr)\n }\n }\n\n const handleKeyRename = (oldKey: string, newKey: string) => {\n if (oldKey === newKey) return\n const keys = Object.keys(data)\n const newData: any = {}\n keys.forEach(k => {\n if (k === oldKey) {\n newData[newKey] = data[k]\n } else {\n newData[k] = data[k]\n }\n })\n onChange(newData)\n }\n\n const handleChildDelete = (key: string | number) => {\n if (type === 'object') {\n const newData = { ...data }\n delete newData[key as string]\n onChange(newData)\n } else if (type === 'array') {\n onChange(data.filter((_: any, i: number) => i !== key))\n }\n }\n\n return (\n <div className={cn(\"pl-0\", !isRoot && \"pl-4 border-l border-border ml-1\")}>\n <div className=\"flex items-start gap-2 py-1 group\">\n\n {isContainer && (\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" className=\"mt-1 text-muted-foreground hover:text-foreground\" onClick={() => setCollapsed(!collapsed)}>\n {collapsed ? <ChevronRight className=\"w-3 h-3\" /> : <ChevronDown className=\"w-3 h-3\" />}\n </IconButton>\n )}\n {!isContainer && !isRoot && <div className=\"w-3\" />} {/* Spacer */}\n\n {label !== undefined && !isRoot && (\n <div className=\"flex items-center gap-1\">\n <span className=\"text-xs text-muted-foreground font-mono\">\n {label}\n </span>\n <span className=\"text-muted-foreground text-xs\">:</span>\n </div>\n )}\n\n <div className=\"flex-1 flex gap-2 items-center flex-wrap\">\n\n {!readOnly && (\n <Select\n value={type}\n onValueChange={(next) => handleTypeChange(next as JsonNodeType)}\n >\n <SelectTrigger size=\"sm\" className=\"w-auto min-w-[6rem]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"string\">String</SelectItem>\n <SelectItem value=\"number\">Number</SelectItem>\n <SelectItem value=\"boolean\">Boolean</SelectItem>\n <SelectItem value=\"object\">Object</SelectItem>\n <SelectItem value=\"array\">Array</SelectItem>\n <SelectItem value=\"null\">Null</SelectItem>\n </SelectContent>\n </Select>\n )}\n\n {type === 'string' && (\n <Input\n size=\"sm\"\n className=\"flex-1 min-w-0 sm:min-w-[120px]\"\n value={data}\n onChange={e => onChange(e.target.value)}\n disabled={readOnly}\n />\n )}\n {type === 'number' && (\n <Input\n type=\"number\"\n size=\"sm\"\n className=\"flex-1 w-full sm:w-[100px]\"\n value={data}\n onChange={e => onChange(parseFloat(e.target.value) || 0)}\n disabled={readOnly}\n />\n )}\n {type === 'boolean' && (\n <Select\n value={String(data)}\n onValueChange={(next) => onChange(next === 'true')}\n disabled={readOnly}\n >\n <SelectTrigger size=\"sm\" className=\"flex-1 w-full sm:w-[100px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">true</SelectItem>\n <SelectItem value=\"false\">false</SelectItem>\n </SelectContent>\n </Select>\n )}\n {type === 'null' && <span className=\"text-xs text-muted-foreground\">null</span>}\n {isContainer && (\n <span className=\"text-xs text-muted-foreground\">\n {type === 'object' ? `{ ${Object.keys(data).length} items }` : `[ ${data.length} items ]`}\n </span>\n )}\n\n {onDelete && !readOnly && (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity\"\n title=\"Remove item\"\n onClick={onDelete}\n >\n <Trash2 className=\"w-3.5 h-3.5\" />\n </IconButton>\n )}\n </div>\n </div>\n\n {isContainer && !collapsed && (\n <div className=\"flex flex-col gap-1 w-full pl-2\">\n {type === 'object' && Object.entries(data).map(([key, val], idx) => (\n <div key={idx} className=\"flex\">\n <div className=\"pt-2\">\n {/* Key Renamer */}\n <InlineInput\n className=\"w-full sm:w-[100px]\"\n inputClassName=\"text-right text-xs font-mono pr-1\"\n value={key}\n onChange={(e) => handleKeyRename(key, e.target.value)}\n disabled={readOnly}\n />\n </div>\n <div className=\"flex-1\">\n <JsonNode\n data={val}\n onChange={(v) => handleChildChange(key, v)}\n onDelete={() => handleChildDelete(key)}\n readOnly={readOnly}\n />\n </div>\n </div>\n ))}\n\n {type === 'array' && (data as any[]).map((val, idx) => (\n <JsonNode\n key={idx}\n label={String(idx)}\n data={val}\n onChange={(v) => handleChildChange(idx, v)}\n onDelete={() => handleChildDelete(idx)}\n readOnly={readOnly}\n />\n ))}\n\n {!readOnly && (\n <div className=\"pl-4 mt-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleAddKey}\n className=\"h-6 text-xs\"\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n Add {type === 'object' ? 'Property' : 'Item'}\n </Button>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { Input } from '../primitives/input'\nimport { InlineInput } from '../primitives/inline-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../primitives/select'\nimport { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'\nimport { useConfirmDialog } from './confirm-dialog'\nimport type { ConfirmDialogOptions } from './confirm-dialog'\n\nfunction cn(...classes: (string | undefined | null | false)[]) {\n return classes.filter(Boolean).join(' ')\n}\n\nexport type JsonBuilderProps = {\n value: any\n onChange: (value: any) => void\n disabled?: boolean\n error?: string\n}\n\ntype JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'\n\ntype ConfirmFn = (options?: ConfirmDialogOptions) => Promise<boolean>\n\nfunction getJsonType(value: any): JsonNodeType {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonNodeType\n}\n\nfunction defaultValueForType(type: JsonNodeType): any {\n switch (type) {\n case 'string': return \"\"\n case 'number': return 0\n case 'boolean': return false\n case 'object': return {}\n case 'array': return []\n case 'null': return null\n }\n}\n\nfunction toRawString(value: any): string {\n if (value === null || value === undefined) return '{}'\n if (typeof value === 'object') return JSON.stringify(value, null, 2)\n return String(value)\n}\n\nexport function JsonBuilder({\n value,\n onChange,\n disabled,\n error\n}: JsonBuilderProps) {\n const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')\n const [rawString, setRawString] = React.useState(() => toRawString(value))\n const [rawDirty, setRawDirty] = React.useState(false)\n const [parseError, setParseError] = React.useState<string | null>(null)\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n React.useEffect(() => {\n if (value === null && !disabled) {\n onChange({})\n }\n }, [value, disabled, onChange])\n\n // Mirror external value changes (e.g. async record load, builder edits) into\n // the raw textarea only while the user has NOT started typing in it. Once the\n // textarea is dirty it becomes the source of truth, so re-deriving it from\n // `value` on every keystroke \u2014 the parent's onChange identity changes each\n // render \u2014 would clobber typing and make Raw JSON uneditable (issue #2817).\n React.useEffect(() => {\n if (rawDirty) return\n if (value !== null && typeof value === 'object') {\n setRawString(JSON.stringify(value, null, 2))\n setParseError(null)\n }\n }, [value, rawDirty])\n\n const handleRawChange = (str: string) => {\n setRawDirty(true)\n setRawString(str)\n try {\n if (str.trim() === '') {\n onChange({})\n setParseError(null)\n } else {\n const parsed = JSON.parse(str)\n onChange(parsed)\n setParseError(null)\n }\n } catch (e) {\n onChange(str)\n setParseError(\"Invalid JSON\")\n }\n }\n\n const switchToRaw = () => {\n // Re-sync the textarea from the current value and let it mirror external\n // changes again until the user edits it.\n setRawDirty(false)\n setRawString(toRawString(value))\n setParseError(null)\n setMode('raw')\n }\n\n const switchToBuilder = () => {\n try {\n if (typeof value === 'string') {\n JSON.parse(value)\n }\n setMode('builder')\n } catch (e) {\n alert(\"Cannot switch to Builder mode: Invalid JSON\")\n }\n }\n\n return (\n <div className=\"space-y-4 border rounded-md p-4 bg-card\">\n <div className=\"flex items-center space-x-2 border-b pb-2 mb-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'raw' && \"bg-muted text-foreground\")}\n onClick={switchToRaw}\n >\n <Code className=\"w-4 h-4\" />\n Raw JSON\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={cn(mode === 'builder' && \"bg-muted text-foreground\")}\n onClick={switchToBuilder}\n >\n <LayoutList className=\"w-4 h-4\" />\n Builder\n </Button>\n </div>\n\n {mode === 'raw' ? (\n <div className=\"space-y-2\">\n <textarea\n value={rawString}\n onChange={(e) => handleRawChange(e.target.value)}\n onBlur={() => {\n try {\n const parsed = JSON.parse(rawString)\n setRawString(JSON.stringify(parsed, null, 2))\n } catch { }\n }}\n placeholder='{\"key\": \"value\"}'\n className=\"w-full rounded border px-3 py-2 min-h-[300px] text-sm font-mono focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n disabled={disabled}\n />\n {parseError && (\n <div className=\"text-xs text-red-600\">Invalid JSON format</div>\n )}\n </div>\n ) : (\n <div className=\"min-h-[300px] text-sm overflow-x-auto\">\n {typeof value === 'object' && value !== null ? (\n <JsonNode\n data={value}\n onChange={onChange}\n readOnly={disabled}\n confirm={confirm}\n isRoot\n />\n ) : (\n <div className=\"text-muted-foreground italic p-4 text-center\">\n Value is not an object or array. Switch to Raw to edit.\n </div>\n )}\n </div>\n )}\n\n {error && <div className=\"text-xs text-red-600\">{error}</div>}\n {ConfirmDialogElement}\n </div>\n )\n}\n\ninterface JsonNodeProps {\n data: any\n onChange: (val: any) => void\n onDelete?: () => void\n readOnly?: boolean\n label?: string\n isRoot?: boolean\n confirm?: ConfirmFn\n}\n\nfunction JsonNode({ data, onChange, onDelete, readOnly, label, isRoot, confirm }: JsonNodeProps) {\n const type = getJsonType(data)\n const isContainer = type === 'object' || type === 'array'\n const [collapsed, setCollapsed] = React.useState(false)\n\n const handleTypeChange = async (newType: JsonNodeType) => {\n if (newType === type) return\n // Switching away from a non-empty container discards its contents. Ask\n // for confirmation first so configured properties aren't lost silently\n // (issue #2817). The Select is controlled by the derived type, so when\n // the user cancels we simply leave the data untouched and the control\n // snaps back to its previous value.\n const itemCount = type === 'object'\n ? Object.keys(data).length\n : type === 'array'\n ? (data as any[]).length\n : 0\n if (itemCount > 0 && confirm) {\n const noun = type === 'object'\n ? `${itemCount} ${itemCount === 1 ? 'property' : 'properties'}`\n : `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`\n const confirmed = await confirm({\n title: 'Change value type?',\n text: `Changing the type from ${type} to ${newType} will discard the ${noun} currently configured. This cannot be undone.`,\n confirmText: 'Discard and change',\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n onChange(defaultValueForType(newType))\n }\n\n const handleAddKey = () => {\n if (type === 'object') {\n const newKey = `newKey_${Object.keys(data).length}`\n onChange({ ...data, [newKey]: \"\" })\n } else if (type === 'array') {\n onChange([...data, \"\"])\n }\n }\n\n const handleChildChange = (key: string | number, newVal: any) => {\n if (type === 'object') {\n onChange({ ...data, [key]: newVal })\n } else if (type === 'array') {\n const arr = [...data]\n arr[Number(key)] = newVal\n onChange(arr)\n }\n }\n\n const handleKeyRename = (oldKey: string, newKey: string) => {\n if (oldKey === newKey) return\n const keys = Object.keys(data)\n const newData: any = {}\n keys.forEach(k => {\n if (k === oldKey) {\n newData[newKey] = data[k]\n } else {\n newData[k] = data[k]\n }\n })\n onChange(newData)\n }\n\n const handleChildDelete = (key: string | number) => {\n if (type === 'object') {\n const newData = { ...data }\n delete newData[key as string]\n onChange(newData)\n } else if (type === 'array') {\n onChange(data.filter((_: any, i: number) => i !== key))\n }\n }\n\n return (\n <div className={cn(\"pl-0\", !isRoot && \"pl-4 border-l border-border ml-1\")}>\n <div className=\"flex items-start gap-2 py-1 group\">\n\n {isContainer && (\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" className=\"mt-1 text-muted-foreground hover:text-foreground\" onClick={() => setCollapsed(!collapsed)}>\n {collapsed ? <ChevronRight className=\"w-3 h-3\" /> : <ChevronDown className=\"w-3 h-3\" />}\n </IconButton>\n )}\n {!isContainer && !isRoot && <div className=\"w-3\" />} {/* Spacer */}\n\n {label !== undefined && !isRoot && (\n <div className=\"flex items-center gap-1\">\n <span className=\"text-xs text-muted-foreground font-mono\">\n {label}\n </span>\n <span className=\"text-muted-foreground text-xs\">:</span>\n </div>\n )}\n\n <div className=\"flex-1 flex gap-2 items-center flex-wrap\">\n\n {!readOnly && (\n <Select\n value={type}\n onValueChange={(next) => { void handleTypeChange(next as JsonNodeType) }}\n >\n <SelectTrigger size=\"sm\" className=\"w-auto min-w-[6rem]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"string\">String</SelectItem>\n <SelectItem value=\"number\">Number</SelectItem>\n <SelectItem value=\"boolean\">Boolean</SelectItem>\n <SelectItem value=\"object\">Object</SelectItem>\n <SelectItem value=\"array\">Array</SelectItem>\n <SelectItem value=\"null\">Null</SelectItem>\n </SelectContent>\n </Select>\n )}\n\n {type === 'string' && (\n <Input\n size=\"sm\"\n className=\"flex-1 min-w-0 sm:min-w-[120px]\"\n value={data}\n onChange={e => onChange(e.target.value)}\n disabled={readOnly}\n />\n )}\n {type === 'number' && (\n <Input\n type=\"number\"\n size=\"sm\"\n className=\"flex-1 w-full sm:w-[100px]\"\n value={data}\n onChange={e => onChange(parseFloat(e.target.value) || 0)}\n disabled={readOnly}\n />\n )}\n {type === 'boolean' && (\n <Select\n value={String(data)}\n onValueChange={(next) => onChange(next === 'true')}\n disabled={readOnly}\n >\n <SelectTrigger size=\"sm\" className=\"flex-1 w-full sm:w-[100px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"true\">true</SelectItem>\n <SelectItem value=\"false\">false</SelectItem>\n </SelectContent>\n </Select>\n )}\n {type === 'null' && <span className=\"text-xs text-muted-foreground\">null</span>}\n {isContainer && (\n <span className=\"text-xs text-muted-foreground\">\n {type === 'object' ? `{ ${Object.keys(data).length} items }` : `[ ${data.length} items ]`}\n </span>\n )}\n\n {onDelete && !readOnly && (\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"text-muted-foreground hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity\"\n title=\"Remove item\"\n onClick={onDelete}\n >\n <Trash2 className=\"w-3.5 h-3.5\" />\n </IconButton>\n )}\n </div>\n </div>\n\n {isContainer && !collapsed && (\n <div className=\"flex flex-col gap-1 w-full pl-2\">\n {type === 'object' && Object.entries(data).map(([key, val], idx) => (\n <div key={idx} className=\"flex\">\n <div className=\"pt-2\">\n {/* Key Renamer */}\n <InlineInput\n className=\"w-full sm:w-[100px]\"\n inputClassName=\"text-right text-xs font-mono pr-1\"\n value={key}\n onChange={(e) => handleKeyRename(key, e.target.value)}\n disabled={readOnly}\n />\n </div>\n <div className=\"flex-1\">\n <JsonNode\n data={val}\n onChange={(v) => handleChildChange(key, v)}\n onDelete={() => handleChildDelete(key)}\n readOnly={readOnly}\n confirm={confirm}\n />\n </div>\n </div>\n ))}\n\n {type === 'array' && (data as any[]).map((val, idx) => (\n <JsonNode\n key={idx}\n label={String(idx)}\n data={val}\n onChange={(v) => handleChildChange(idx, v)}\n onDelete={() => handleChildDelete(idx)}\n readOnly={readOnly}\n confirm={confirm}\n />\n ))}\n\n {!readOnly && (\n <div className=\"pl-4 mt-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={handleAddKey}\n className=\"h-6 text-xs\"\n >\n <Plus className=\"w-3 h-3 mr-1\" />\n Add {type === 'object' ? 'Property' : 'Item'}\n </Button>\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.5-develop.
|
|
3
|
+
"version": "0.6.5-develop.5155.1.148d10a46d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -132,15 +132,15 @@
|
|
|
132
132
|
"@dnd-kit/sortable": "^10.0.0",
|
|
133
133
|
"@dnd-kit/utilities": "^3.2.2",
|
|
134
134
|
"@mdxeditor/editor": "^4.0.2",
|
|
135
|
-
"@radix-ui/react-accordion": "^1.2.
|
|
136
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
137
|
-
"@radix-ui/react-popover": "^1.1.
|
|
138
|
-
"@radix-ui/react-radio-group": "^1.
|
|
135
|
+
"@radix-ui/react-accordion": "^1.2.13",
|
|
136
|
+
"@radix-ui/react-dialog": "^1.1.16",
|
|
137
|
+
"@radix-ui/react-popover": "^1.1.16",
|
|
138
|
+
"@radix-ui/react-radio-group": "^1.4.0",
|
|
139
139
|
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
140
|
-
"@radix-ui/react-select": "^2.
|
|
140
|
+
"@radix-ui/react-select": "^2.3.0",
|
|
141
141
|
"@radix-ui/react-slider": "^1.2.0",
|
|
142
|
-
"@radix-ui/react-tooltip": "^1.2.
|
|
143
|
-
"@tanstack/react-virtual": "^3.14.
|
|
142
|
+
"@radix-ui/react-tooltip": "^1.2.9",
|
|
143
|
+
"@tanstack/react-virtual": "^3.14.2",
|
|
144
144
|
"cmdk": "^1.0.0",
|
|
145
145
|
"date-fns": "4.4.0",
|
|
146
146
|
"react-big-calendar": "^1.20.0",
|
|
@@ -150,18 +150,18 @@
|
|
|
150
150
|
"remark-gfm": "^4.0.1"
|
|
151
151
|
},
|
|
152
152
|
"peerDependencies": {
|
|
153
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
153
|
+
"@open-mercato/shared": "0.6.5-develop.5155.1.148d10a46d",
|
|
154
154
|
"react": ">=18.0.0",
|
|
155
155
|
"react-dom": ">=18.0.0",
|
|
156
156
|
"react-is": ">=18.0.0"
|
|
157
157
|
},
|
|
158
158
|
"devDependencies": {
|
|
159
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
159
|
+
"@open-mercato/shared": "0.6.5-develop.5155.1.148d10a46d",
|
|
160
160
|
"@testing-library/dom": "^10.4.1",
|
|
161
161
|
"@testing-library/jest-dom": "^6.9.1",
|
|
162
162
|
"@testing-library/react": "^16.3.1",
|
|
163
163
|
"@types/jest": "^30.0.0",
|
|
164
|
-
"@types/react": "^19.2.
|
|
164
|
+
"@types/react": "^19.2.17",
|
|
165
165
|
"@types/react-dom": "^19.2.3",
|
|
166
166
|
"jest": "^30.4.2",
|
|
167
167
|
"jest-environment-jsdom": "^30.4.1",
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
SelectValue,
|
|
14
14
|
} from '../primitives/select'
|
|
15
15
|
import { Plus, Trash2, ChevronRight, ChevronDown, Code, LayoutList } from 'lucide-react'
|
|
16
|
+
import { useConfirmDialog } from './confirm-dialog'
|
|
17
|
+
import type { ConfirmDialogOptions } from './confirm-dialog'
|
|
16
18
|
|
|
17
19
|
function cn(...classes: (string | undefined | null | false)[]) {
|
|
18
20
|
return classes.filter(Boolean).join(' ')
|
|
@@ -27,12 +29,31 @@ export type JsonBuilderProps = {
|
|
|
27
29
|
|
|
28
30
|
type JsonNodeType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null'
|
|
29
31
|
|
|
32
|
+
type ConfirmFn = (options?: ConfirmDialogOptions) => Promise<boolean>
|
|
33
|
+
|
|
30
34
|
function getJsonType(value: any): JsonNodeType {
|
|
31
35
|
if (value === null) return 'null'
|
|
32
36
|
if (Array.isArray(value)) return 'array'
|
|
33
37
|
return typeof value as JsonNodeType
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
function defaultValueForType(type: JsonNodeType): any {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'string': return ""
|
|
43
|
+
case 'number': return 0
|
|
44
|
+
case 'boolean': return false
|
|
45
|
+
case 'object': return {}
|
|
46
|
+
case 'array': return []
|
|
47
|
+
case 'null': return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function toRawString(value: any): string {
|
|
52
|
+
if (value === null || value === undefined) return '{}'
|
|
53
|
+
if (typeof value === 'object') return JSON.stringify(value, null, 2)
|
|
54
|
+
return String(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
export function JsonBuilder({
|
|
37
58
|
value,
|
|
38
59
|
onChange,
|
|
@@ -40,28 +61,32 @@ export function JsonBuilder({
|
|
|
40
61
|
error
|
|
41
62
|
}: JsonBuilderProps) {
|
|
42
63
|
const [mode, setMode] = React.useState<'raw' | 'builder'>('raw')
|
|
43
|
-
const [rawString, setRawString] = React.useState(() =>
|
|
44
|
-
|
|
45
|
-
return typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '{}')
|
|
46
|
-
})
|
|
64
|
+
const [rawString, setRawString] = React.useState(() => toRawString(value))
|
|
65
|
+
const [rawDirty, setRawDirty] = React.useState(false)
|
|
47
66
|
const [parseError, setParseError] = React.useState<string | null>(null)
|
|
67
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
48
68
|
|
|
49
69
|
React.useEffect(() => {
|
|
50
|
-
if (value === null) {
|
|
51
|
-
|
|
52
|
-
onChange({})
|
|
53
|
-
}
|
|
54
|
-
setRawString('{}')
|
|
55
|
-
setParseError(null)
|
|
56
|
-
return
|
|
70
|
+
if (value === null && !disabled) {
|
|
71
|
+
onChange({})
|
|
57
72
|
}
|
|
58
|
-
|
|
73
|
+
}, [value, disabled, onChange])
|
|
74
|
+
|
|
75
|
+
// Mirror external value changes (e.g. async record load, builder edits) into
|
|
76
|
+
// the raw textarea only while the user has NOT started typing in it. Once the
|
|
77
|
+
// textarea is dirty it becomes the source of truth, so re-deriving it from
|
|
78
|
+
// `value` on every keystroke — the parent's onChange identity changes each
|
|
79
|
+
// render — would clobber typing and make Raw JSON uneditable (issue #2817).
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (rawDirty) return
|
|
82
|
+
if (value !== null && typeof value === 'object') {
|
|
59
83
|
setRawString(JSON.stringify(value, null, 2))
|
|
60
84
|
setParseError(null)
|
|
61
85
|
}
|
|
62
|
-
}, [value,
|
|
86
|
+
}, [value, rawDirty])
|
|
63
87
|
|
|
64
88
|
const handleRawChange = (str: string) => {
|
|
89
|
+
setRawDirty(true)
|
|
65
90
|
setRawString(str)
|
|
66
91
|
try {
|
|
67
92
|
if (str.trim() === '') {
|
|
@@ -78,6 +103,15 @@ export function JsonBuilder({
|
|
|
78
103
|
}
|
|
79
104
|
}
|
|
80
105
|
|
|
106
|
+
const switchToRaw = () => {
|
|
107
|
+
// Re-sync the textarea from the current value and let it mirror external
|
|
108
|
+
// changes again until the user edits it.
|
|
109
|
+
setRawDirty(false)
|
|
110
|
+
setRawString(toRawString(value))
|
|
111
|
+
setParseError(null)
|
|
112
|
+
setMode('raw')
|
|
113
|
+
}
|
|
114
|
+
|
|
81
115
|
const switchToBuilder = () => {
|
|
82
116
|
try {
|
|
83
117
|
if (typeof value === 'string') {
|
|
@@ -97,7 +131,7 @@ export function JsonBuilder({
|
|
|
97
131
|
variant="ghost"
|
|
98
132
|
size="sm"
|
|
99
133
|
className={cn(mode === 'raw' && "bg-muted text-foreground")}
|
|
100
|
-
onClick={
|
|
134
|
+
onClick={switchToRaw}
|
|
101
135
|
>
|
|
102
136
|
<Code className="w-4 h-4" />
|
|
103
137
|
Raw JSON
|
|
@@ -140,6 +174,7 @@ export function JsonBuilder({
|
|
|
140
174
|
data={value}
|
|
141
175
|
onChange={onChange}
|
|
142
176
|
readOnly={disabled}
|
|
177
|
+
confirm={confirm}
|
|
143
178
|
isRoot
|
|
144
179
|
/>
|
|
145
180
|
) : (
|
|
@@ -151,6 +186,7 @@ export function JsonBuilder({
|
|
|
151
186
|
)}
|
|
152
187
|
|
|
153
188
|
{error && <div className="text-xs text-red-600">{error}</div>}
|
|
189
|
+
{ConfirmDialogElement}
|
|
154
190
|
</div>
|
|
155
191
|
)
|
|
156
192
|
}
|
|
@@ -162,24 +198,39 @@ interface JsonNodeProps {
|
|
|
162
198
|
readOnly?: boolean
|
|
163
199
|
label?: string
|
|
164
200
|
isRoot?: boolean
|
|
201
|
+
confirm?: ConfirmFn
|
|
165
202
|
}
|
|
166
203
|
|
|
167
|
-
function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNodeProps) {
|
|
204
|
+
function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot, confirm }: JsonNodeProps) {
|
|
168
205
|
const type = getJsonType(data)
|
|
169
206
|
const isContainer = type === 'object' || type === 'array'
|
|
170
207
|
const [collapsed, setCollapsed] = React.useState(false)
|
|
171
208
|
|
|
172
|
-
const handleTypeChange = (newType: JsonNodeType) => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
const handleTypeChange = async (newType: JsonNodeType) => {
|
|
210
|
+
if (newType === type) return
|
|
211
|
+
// Switching away from a non-empty container discards its contents. Ask
|
|
212
|
+
// for confirmation first so configured properties aren't lost silently
|
|
213
|
+
// (issue #2817). The Select is controlled by the derived type, so when
|
|
214
|
+
// the user cancels we simply leave the data untouched and the control
|
|
215
|
+
// snaps back to its previous value.
|
|
216
|
+
const itemCount = type === 'object'
|
|
217
|
+
? Object.keys(data).length
|
|
218
|
+
: type === 'array'
|
|
219
|
+
? (data as any[]).length
|
|
220
|
+
: 0
|
|
221
|
+
if (itemCount > 0 && confirm) {
|
|
222
|
+
const noun = type === 'object'
|
|
223
|
+
? `${itemCount} ${itemCount === 1 ? 'property' : 'properties'}`
|
|
224
|
+
: `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`
|
|
225
|
+
const confirmed = await confirm({
|
|
226
|
+
title: 'Change value type?',
|
|
227
|
+
text: `Changing the type from ${type} to ${newType} will discard the ${noun} currently configured. This cannot be undone.`,
|
|
228
|
+
confirmText: 'Discard and change',
|
|
229
|
+
variant: 'destructive',
|
|
230
|
+
})
|
|
231
|
+
if (!confirmed) return
|
|
181
232
|
}
|
|
182
|
-
onChange(
|
|
233
|
+
onChange(defaultValueForType(newType))
|
|
183
234
|
}
|
|
184
235
|
|
|
185
236
|
const handleAddKey = () => {
|
|
@@ -250,7 +301,7 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNod
|
|
|
250
301
|
{!readOnly && (
|
|
251
302
|
<Select
|
|
252
303
|
value={type}
|
|
253
|
-
onValueChange={(next) => handleTypeChange(next as JsonNodeType)}
|
|
304
|
+
onValueChange={(next) => { void handleTypeChange(next as JsonNodeType) }}
|
|
254
305
|
>
|
|
255
306
|
<SelectTrigger size="sm" className="w-auto min-w-[6rem]">
|
|
256
307
|
<SelectValue />
|
|
@@ -342,6 +393,7 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNod
|
|
|
342
393
|
onChange={(v) => handleChildChange(key, v)}
|
|
343
394
|
onDelete={() => handleChildDelete(key)}
|
|
344
395
|
readOnly={readOnly}
|
|
396
|
+
confirm={confirm}
|
|
345
397
|
/>
|
|
346
398
|
</div>
|
|
347
399
|
</div>
|
|
@@ -355,6 +407,7 @@ function JsonNode({ data, onChange, onDelete, readOnly, label, isRoot }: JsonNod
|
|
|
355
407
|
onChange={(v) => handleChildChange(idx, v)}
|
|
356
408
|
onDelete={() => handleChildDelete(idx)}
|
|
357
409
|
readOnly={readOnly}
|
|
410
|
+
confirm={confirm}
|
|
358
411
|
/>
|
|
359
412
|
))}
|
|
360
413
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
6
|
+
import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
|
|
7
|
+
import { JsonBuilder } from '../JsonBuilder'
|
|
8
|
+
|
|
9
|
+
// Radix Select uses pointer capture / scrollIntoView APIs that jsdom doesn't implement.
|
|
10
|
+
// The confirm dialog uses the native <dialog> showModal/close APIs. Polyfill both so the
|
|
11
|
+
// component can be exercised end to end (issue #2817).
|
|
12
|
+
if (typeof window !== 'undefined') {
|
|
13
|
+
if (!Element.prototype.hasPointerCapture) {
|
|
14
|
+
Element.prototype.hasPointerCapture = () => false
|
|
15
|
+
}
|
|
16
|
+
if (!Element.prototype.releasePointerCapture) {
|
|
17
|
+
Element.prototype.releasePointerCapture = () => undefined
|
|
18
|
+
}
|
|
19
|
+
if (!Element.prototype.scrollIntoView) {
|
|
20
|
+
Element.prototype.scrollIntoView = () => undefined
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
|
|
23
|
+
configurable: true,
|
|
24
|
+
value(this: HTMLDialogElement) {
|
|
25
|
+
this.setAttribute('open', '')
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
Object.defineProperty(HTMLDialogElement.prototype, 'close', {
|
|
29
|
+
configurable: true,
|
|
30
|
+
value(this: HTMLDialogElement) {
|
|
31
|
+
this.removeAttribute('open')
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ControlledHarness({ initial = {} as any }: { initial?: any }) {
|
|
37
|
+
const [value, setValue] = React.useState<any>(initial)
|
|
38
|
+
// Inline onChange recreated each render — mirrors how WebhookCustomHeadersField
|
|
39
|
+
// wires the builder. A stale effect dependency on this identity is what made
|
|
40
|
+
// Raw JSON uneditable before the fix.
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<JsonBuilder value={value} onChange={(next) => setValue(next)} />
|
|
44
|
+
<div data-testid="value">{JSON.stringify(value)}</div>
|
|
45
|
+
</>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('JsonBuilder', () => {
|
|
50
|
+
it('lets the user type JSON in Raw mode without clobbering the text', () => {
|
|
51
|
+
renderWithProviders(<ControlledHarness />)
|
|
52
|
+
|
|
53
|
+
const textarea = screen.getByPlaceholderText('{"key": "value"}') as HTMLTextAreaElement
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
fireEvent.change(textarea, { target: { value: '{"a":"1"}' } })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// The exact typed text is preserved (not reformatted/reset by a value echo).
|
|
60
|
+
expect(textarea.value).toBe('{"a":"1"}')
|
|
61
|
+
// ...and the parsed object propagated to the parent.
|
|
62
|
+
expect(screen.getByTestId('value').textContent).toBe('{"a":"1"}')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('mirrors an external value into the Raw textarea until the user edits it', () => {
|
|
66
|
+
function ExternalHarness() {
|
|
67
|
+
const [value, setValue] = React.useState<any>({})
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<button type="button" onClick={() => setValue({ injected: 'hello' })}>
|
|
71
|
+
load
|
|
72
|
+
</button>
|
|
73
|
+
<JsonBuilder value={value} onChange={setValue} />
|
|
74
|
+
</>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
renderWithProviders(<ExternalHarness />)
|
|
79
|
+
const textarea = screen.getByPlaceholderText('{"key": "value"}') as HTMLTextAreaElement
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
fireEvent.click(screen.getByRole('button', { name: 'load' }))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(textarea.value).toContain('injected')
|
|
86
|
+
expect(textarea.value).toContain('hello')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
async function openBuilderAndChangeRootType() {
|
|
90
|
+
renderWithProviders(<ControlledHarness initial={{ foo: 'bar' }} />)
|
|
91
|
+
|
|
92
|
+
act(() => {
|
|
93
|
+
fireEvent.click(screen.getByRole('button', { name: 'Builder' }))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const rootTrigger = screen
|
|
97
|
+
.getAllByRole('combobox')
|
|
98
|
+
.find((element) => element.textContent?.trim() === 'Object')
|
|
99
|
+
expect(rootTrigger).toBeDefined()
|
|
100
|
+
|
|
101
|
+
fireEvent.pointerDown(rootTrigger!, { button: 0, ctrlKey: false })
|
|
102
|
+
fireEvent.click(rootTrigger!)
|
|
103
|
+
|
|
104
|
+
const stringOption = screen.getByRole('option', { name: 'String' })
|
|
105
|
+
await act(async () => {
|
|
106
|
+
fireEvent.pointerDown(stringOption)
|
|
107
|
+
fireEvent.click(stringOption)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it('asks for confirmation before a destructive root type change and preserves data on cancel', async () => {
|
|
112
|
+
await openBuilderAndChangeRootType()
|
|
113
|
+
|
|
114
|
+
const cancelButton = await screen.findByRole('button', { name: 'Cancel' })
|
|
115
|
+
await act(async () => {
|
|
116
|
+
fireEvent.click(cancelButton)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Cancelling leaves the configured properties intact.
|
|
120
|
+
expect(screen.getByTestId('value').textContent).toBe('{"foo":"bar"}')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('discards data only after the destructive root type change is confirmed', async () => {
|
|
124
|
+
await openBuilderAndChangeRootType()
|
|
125
|
+
|
|
126
|
+
const confirmButton = await screen.findByRole('button', { name: 'Discard and change' })
|
|
127
|
+
await act(async () => {
|
|
128
|
+
fireEvent.click(confirmButton)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByTestId('value').textContent).toBe('""')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|