@open-mercato/ui 0.6.5-develop.5139.1.g7925348493 → 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
- if (value === null) return "{}";
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
- if (!disabled) {
39
- onChange({});
40
- }
41
- setRawString("{}");
42
- setParseError(null);
43
- return;
58
+ if (value === null && !disabled) {
59
+ onChange({});
44
60
  }
45
- if (typeof value === "object") {
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, disabled, onChange]);
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: () => setMode("raw"),
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
- let newVal = "";
144
- switch (newType) {
145
- case "string":
146
- newVal = "";
147
- break;
148
- case "number":
149
- newVal = 0;
150
- break;
151
- case "boolean":
152
- newVal = false;
153
- break;
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(newVal);
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) => handleTypeChange(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": "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",
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.5139.1.g7925348493",
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.4",
136
- "@radix-ui/react-dialog": "^1.1.6",
137
- "@radix-ui/react-popover": "^1.1.6",
138
- "@radix-ui/react-radio-group": "^1.2.3",
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.1.6",
140
+ "@radix-ui/react-select": "^2.3.0",
141
141
  "@radix-ui/react-slider": "^1.2.0",
142
- "@radix-ui/react-tooltip": "^1.2.8",
143
- "@tanstack/react-virtual": "^3.14.1",
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.5139.1.g7925348493",
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.5139.1.g7925348493",
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.16",
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
- if (value === null) return '{}'
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
- if (!disabled) {
52
- onChange({})
53
- }
54
- setRawString('{}')
55
- setParseError(null)
56
- return
70
+ if (value === null && !disabled) {
71
+ onChange({})
57
72
  }
58
- if (typeof value === 'object') {
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, disabled, onChange])
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={() => setMode('raw')}
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
- let newVal: any = ''
174
- switch (newType) {
175
- case 'string': newVal = ""; break;
176
- case 'number': newVal = 0; break;
177
- case 'boolean': newVal = false; break;
178
- case 'object': newVal = {}; break;
179
- case 'array': newVal = []; break;
180
- case 'null': newVal = null; break;
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(newVal)
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
+ })