@esheet/builder 0.0.4-0 → 0.0.4-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +19 -2
- package/dist/index.js +22105 -7909
- package/dist/lib/EsheetBuilder.js +111 -0
- package/dist/lib/builder-tools.js +321 -0
- package/dist/lib/components/BuilderHeader.js +390 -0
- package/dist/lib/components/Canvas.js +288 -0
- package/dist/lib/components/CodeView.js +197 -0
- package/dist/lib/components/FeedbackModal.js +22 -0
- package/dist/lib/components/FieldWrapper.js +152 -0
- package/dist/lib/components/MobileBottomDrawer.js +6 -0
- package/dist/lib/components/ToolPanel.js +117 -0
- package/dist/lib/components/edit-panel/CommonEditor.js +16 -0
- package/dist/lib/components/edit-panel/DraftIdEditor.js +32 -0
- package/dist/lib/components/edit-panel/EditPanel.js +156 -0
- package/dist/lib/components/edit-panel/InputTypeEditor.js +32 -0
- package/dist/lib/components/edit-panel/LogicEditor.js +392 -0
- package/dist/lib/components/edit-panel/MatrixEditor.js +32 -0
- package/dist/lib/components/edit-panel/OptionListEditor.js +27 -0
- package/dist/lib/hooks/useFormApi.js +80 -0
- package/dist/lib/hooks/useUiApi.js +37 -0
- package/dist/lib/hooks/useVisibleRootIds.js +32 -0
- package/dist/lib/icons.js +47 -0
- package/dist/lib/mcp/index.js +4 -0
- package/dist/lib/mcp/system-prompt.js +9 -0
- package/dist/lib/mcp/tool-definitions.js +436 -0
- package/dist/lib/mcp/tool-executor.js +482 -0
- package/dist/lib/mcp/useBuilderToolBridge.js +56 -0
- package/dist/lib/register-defaults.js +37 -0
- package/package.json +6 -5
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Sortable from 'sortablejs';
|
|
4
|
+
import { useFormApi } from '../hooks/useFormApi.js';
|
|
5
|
+
import { useUiApi } from '../hooks/useUiApi.js';
|
|
6
|
+
import { useVisibleRootIds } from '../hooks/useVisibleRootIds.js';
|
|
7
|
+
import { FieldWrapper } from './FieldWrapper.js';
|
|
8
|
+
import { getFieldComponent } from '@esheet/fields';
|
|
9
|
+
import { ViewBigIcon, ViewSmallIcon } from '../icons.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// DraggableFieldItem — each field is both draggable and a drop target
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function DraggableFieldItem({ id, form, ui, parentId, dragEnabled, isSelected = false, isActiveChild = false, forceExpandVersion, forceCollapseVersion, nestedChildren, }) {
|
|
14
|
+
const handleRef = React.useRef(null);
|
|
15
|
+
const field = form.getState().getField(id);
|
|
16
|
+
const handleSelectOverride = React.useCallback((e) => {
|
|
17
|
+
if (!parentId)
|
|
18
|
+
return;
|
|
19
|
+
e.stopPropagation();
|
|
20
|
+
ui.getState().selectFieldChild(parentId, id);
|
|
21
|
+
}, [id, parentId, ui]);
|
|
22
|
+
if (!field)
|
|
23
|
+
return null;
|
|
24
|
+
return (_jsx("div", { className: "field-canvas-wrapper ms:relative ms:pb-1 ms:last:pb-0", "data-field-id": id, "data-field-type": field.definition.fieldType, "data-selected": isSelected ? 'true' : 'false', children: _jsx(FieldWrapper, { fieldId: id, form: form, ui: ui, dragHandleRef: handleRef, forceExpandVersion: forceExpandVersion, forceCollapseVersion: forceCollapseVersion, isSelectedOverride: parentId ? isActiveChild : undefined, onSelectOverride: parentId ? handleSelectOverride : undefined, selectedVariant: parentId ? 'nested' : 'default', children: (props) => {
|
|
25
|
+
const Component = getFieldComponent(props.field.definition.fieldType);
|
|
26
|
+
if (props.field.definition.fieldType === 'section') {
|
|
27
|
+
const SectionComponent = Component;
|
|
28
|
+
return (_jsx(SectionComponent, { ...props, nestedChildren: nestedChildren }));
|
|
29
|
+
}
|
|
30
|
+
return _jsx(Component, { ...props });
|
|
31
|
+
} }) }));
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Canvas — main field list panel with Sheet DnD
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export const Canvas = React.memo(function Canvas({ form, ui, dragEnabled = true, }) {
|
|
37
|
+
const canvasRef = React.useRef(null);
|
|
38
|
+
const rootIds = useVisibleRootIds();
|
|
39
|
+
const { normalized, responses } = useFormApi();
|
|
40
|
+
const { mode, selectedFieldId, selectedFieldChildId } = useUiApi();
|
|
41
|
+
const [sectionExpandSignal, setSectionExpandSignal] = React.useState(null);
|
|
42
|
+
const [expandAllVersion, setExpandAllVersion] = React.useState(undefined);
|
|
43
|
+
const [collapseAllVersion, setCollapseAllVersion] = React.useState(undefined);
|
|
44
|
+
const [allExpanded, setAllExpanded] = React.useState(false);
|
|
45
|
+
const normalizedRef = React.useRef(normalized);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
normalizedRef.current = normalized;
|
|
48
|
+
}, [normalized]);
|
|
49
|
+
// Clear drag state when mode changes or modal closes
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
ui.getState().clearDragState();
|
|
52
|
+
}, [dragEnabled, ui]);
|
|
53
|
+
// SortableJS setup for root and section child lists.
|
|
54
|
+
// Re-runs whenever `normalized` changes so newly added section child
|
|
55
|
+
// containers always get their own Sortable instance.
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const el = canvasRef.current;
|
|
58
|
+
if (!el || !dragEnabled)
|
|
59
|
+
return;
|
|
60
|
+
const resolveScrollContainer = (fromEl) => {
|
|
61
|
+
let node = fromEl;
|
|
62
|
+
while (node) {
|
|
63
|
+
const style = window.getComputedStyle(node);
|
|
64
|
+
const canScrollY = style.overflowY === 'auto' ||
|
|
65
|
+
style.overflowY === 'scroll' ||
|
|
66
|
+
style.overflowY === 'overlay';
|
|
67
|
+
if (canScrollY && node.scrollHeight > node.clientHeight)
|
|
68
|
+
return node;
|
|
69
|
+
node = node.parentElement;
|
|
70
|
+
}
|
|
71
|
+
return document.scrollingElement instanceof HTMLElement
|
|
72
|
+
? document.scrollingElement
|
|
73
|
+
: null;
|
|
74
|
+
};
|
|
75
|
+
const getParentId = (listEl) => {
|
|
76
|
+
const attr = listEl.getAttribute('data-parent-id');
|
|
77
|
+
return attr && attr.length > 0 ? attr : null;
|
|
78
|
+
};
|
|
79
|
+
const restoreDomToSource = (evt) => {
|
|
80
|
+
if (typeof evt.oldIndex !== 'number')
|
|
81
|
+
return;
|
|
82
|
+
const { item, from: sourceList, to: targetList } = evt;
|
|
83
|
+
if (sourceList !== targetList && item.parentElement === targetList) {
|
|
84
|
+
targetList.removeChild(item);
|
|
85
|
+
}
|
|
86
|
+
const clamped = Math.max(0, Math.min(evt.oldIndex, sourceList.children.length));
|
|
87
|
+
const ref = sourceList.children.item(clamped);
|
|
88
|
+
if (ref)
|
|
89
|
+
sourceList.insertBefore(item, ref);
|
|
90
|
+
else
|
|
91
|
+
sourceList.appendChild(item);
|
|
92
|
+
};
|
|
93
|
+
const listEls = [
|
|
94
|
+
el,
|
|
95
|
+
...Array.from(el.querySelectorAll('[data-sortable-list="true"]')),
|
|
96
|
+
];
|
|
97
|
+
const instances = listEls.map((listEl) => {
|
|
98
|
+
const scrollContainer = resolveScrollContainer(listEl);
|
|
99
|
+
return Sortable.create(listEl, {
|
|
100
|
+
group: 'builder-fields',
|
|
101
|
+
handle: '.drag-handle',
|
|
102
|
+
draggable: '.field-canvas-wrapper',
|
|
103
|
+
dataIdAttr: 'data-field-id',
|
|
104
|
+
animation: 150,
|
|
105
|
+
forceFallback: true,
|
|
106
|
+
fallbackOnBody: true,
|
|
107
|
+
fallbackTolerance: 3,
|
|
108
|
+
scroll: scrollContainer ?? true,
|
|
109
|
+
bubbleScroll: scrollContainer === null,
|
|
110
|
+
forceAutoScrollFallback: true,
|
|
111
|
+
scrollSensitivity: 220,
|
|
112
|
+
scrollSpeed: 13,
|
|
113
|
+
invertSwap: true,
|
|
114
|
+
swapThreshold: getParentId(listEl) !== null ? 0.21 : 0.5,
|
|
115
|
+
invertedSwapThreshold: getParentId(listEl) !== null ? 0.21 : 0.5,
|
|
116
|
+
emptyInsertThreshold: getParentId(listEl) !== null ? 40 : 18,
|
|
117
|
+
onChoose: (evt) => {
|
|
118
|
+
const sourceId = evt.item.getAttribute('data-field-id');
|
|
119
|
+
if (!sourceId)
|
|
120
|
+
return;
|
|
121
|
+
const sourceNode = normalizedRef.current.byId[sourceId];
|
|
122
|
+
if (sourceNode?.parentId) {
|
|
123
|
+
ui.getState().selectFieldChild(sourceNode.parentId, sourceId);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
ui.getState().selectField(sourceId);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
onMove: (evt) => {
|
|
130
|
+
// Toggle placeholder visibility without triggering a React re-render.
|
|
131
|
+
// A re-render here would shift DOM indices and break restoreDomToSource.
|
|
132
|
+
// Reset ALL placeholders first so any section we just left is restored.
|
|
133
|
+
el.querySelectorAll('.section-empty-placeholder').forEach((ph) => {
|
|
134
|
+
ph.style.display = '';
|
|
135
|
+
});
|
|
136
|
+
// Hide placeholder in the list currently being dragged into.
|
|
137
|
+
if (getParentId(evt.to) !== null) {
|
|
138
|
+
const ph = evt.to.querySelector('.section-empty-placeholder');
|
|
139
|
+
if (ph)
|
|
140
|
+
ph.style.display = 'none';
|
|
141
|
+
}
|
|
142
|
+
// Show placeholder when dragging the last child out of a section.
|
|
143
|
+
if (evt.from !== evt.to &&
|
|
144
|
+
getParentId(evt.from) !== null &&
|
|
145
|
+
evt.from.querySelectorAll('.field-canvas-wrapper').length <= 1) {
|
|
146
|
+
const ph = evt.from.querySelector('.section-empty-placeholder');
|
|
147
|
+
if (ph)
|
|
148
|
+
ph.style.display = '';
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
onEnd: (evt) => {
|
|
153
|
+
// Clear any inline display overrides set during drag.
|
|
154
|
+
el.querySelectorAll('.section-empty-placeholder').forEach((ph) => {
|
|
155
|
+
ph.style.display = '';
|
|
156
|
+
});
|
|
157
|
+
const sourceId = evt.item.getAttribute('data-field-id');
|
|
158
|
+
if (!sourceId)
|
|
159
|
+
return;
|
|
160
|
+
const newIndex = evt.newDraggableIndex ?? evt.newIndex;
|
|
161
|
+
const oldIndex = evt.oldDraggableIndex ?? evt.oldIndex;
|
|
162
|
+
if (typeof newIndex !== 'number')
|
|
163
|
+
return;
|
|
164
|
+
const fromParentId = getParentId(evt.from);
|
|
165
|
+
const toParentId = getParentId(evt.to);
|
|
166
|
+
// No-op: dropped back in the same position
|
|
167
|
+
if (fromParentId === toParentId && oldIndex === newIndex)
|
|
168
|
+
return;
|
|
169
|
+
// Undo Sortable's DOM move so React can own the placement
|
|
170
|
+
restoreDomToSource(evt);
|
|
171
|
+
form.getState().moveField(sourceId, newIndex, toParentId);
|
|
172
|
+
// Update selection to follow the moved field to its new location.
|
|
173
|
+
if (toParentId !== null) {
|
|
174
|
+
setSectionExpandSignal((prev) => ({
|
|
175
|
+
sectionId: toParentId,
|
|
176
|
+
version: (prev?.version ?? 0) + 1,
|
|
177
|
+
}));
|
|
178
|
+
ui.getState().selectFieldChild(toParentId, sourceId);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
ui.getState().selectField(sourceId);
|
|
182
|
+
}
|
|
183
|
+
ui.getState().clearDragState();
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
return () => {
|
|
188
|
+
for (const instance of instances)
|
|
189
|
+
instance.destroy();
|
|
190
|
+
};
|
|
191
|
+
}, [dragEnabled, form, normalized, ui]);
|
|
192
|
+
// Preview-only renderability map
|
|
193
|
+
const previewRenderableMap = React.useMemo(() => {
|
|
194
|
+
if (mode !== 'preview')
|
|
195
|
+
return null;
|
|
196
|
+
const cache = new Map();
|
|
197
|
+
const visit = (fieldId) => {
|
|
198
|
+
const cached = cache.get(fieldId);
|
|
199
|
+
if (cached !== undefined)
|
|
200
|
+
return cached;
|
|
201
|
+
const isVisible = form.getState().isVisible(fieldId);
|
|
202
|
+
if (!isVisible) {
|
|
203
|
+
cache.set(fieldId, false);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const node = normalized.byId[fieldId];
|
|
207
|
+
if (!node) {
|
|
208
|
+
cache.set(fieldId, false);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (node.definition.fieldType !== 'section') {
|
|
212
|
+
cache.set(fieldId, true);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const hasRenderableChild = node.childIds.some((childId) => visit(childId));
|
|
216
|
+
cache.set(fieldId, hasRenderableChild);
|
|
217
|
+
return hasRenderableChild;
|
|
218
|
+
};
|
|
219
|
+
for (const id of Object.keys(normalized.byId)) {
|
|
220
|
+
visit(id);
|
|
221
|
+
}
|
|
222
|
+
return cache;
|
|
223
|
+
}, [form, mode, normalized, responses]);
|
|
224
|
+
const items = React.useMemo(() => {
|
|
225
|
+
if (mode !== 'preview' || !previewRenderableMap)
|
|
226
|
+
return [...rootIds];
|
|
227
|
+
return rootIds.filter((id) => previewRenderableMap.get(id) === true);
|
|
228
|
+
}, [mode, previewRenderableMap, rootIds]);
|
|
229
|
+
const getVisibleChildIds = React.useCallback((parentId) => {
|
|
230
|
+
const parent = normalized.byId[parentId];
|
|
231
|
+
if (!parent || parent.childIds.length === 0)
|
|
232
|
+
return [];
|
|
233
|
+
if (mode !== 'preview')
|
|
234
|
+
return parent.childIds;
|
|
235
|
+
return parent.childIds.filter((childId) => previewRenderableMap?.get(childId) === true);
|
|
236
|
+
}, [mode, normalized, previewRenderableMap]);
|
|
237
|
+
const renderNestedChildren = React.useCallback((parentId, depth = 1) => {
|
|
238
|
+
const parent = normalized.byId[parentId];
|
|
239
|
+
if (!parent || parent.definition.fieldType !== 'section')
|
|
240
|
+
return null;
|
|
241
|
+
const childIds = getVisibleChildIds(parentId);
|
|
242
|
+
if (mode === 'preview' && childIds.length === 0)
|
|
243
|
+
return null;
|
|
244
|
+
const isEmpty = mode !== 'preview' && childIds.length === 0;
|
|
245
|
+
const containerClass = depth === 1
|
|
246
|
+
? 'section-children'
|
|
247
|
+
: 'section-children ms:border-l ms:border-msborder ms:pl-3';
|
|
248
|
+
const emptyClass = isEmpty
|
|
249
|
+
? ' ms:rounded-lg ms:border-2 ms:border-dashed ms:border-msprimary/30 ms:bg-gradient-to-br ms:from-msbackground ms:to-msbackgroundsecondary'
|
|
250
|
+
: ' ms:min-h-[2rem]';
|
|
251
|
+
return (_jsxs("div", { className: `${containerClass}${emptyClass}`, "data-depth": depth, "data-sortable-list": dragEnabled ? 'true' : undefined, "data-parent-id": parentId, children: [isEmpty && (_jsxs("div", { className: "section-empty-placeholder ms:flex ms:flex-col ms:items-center ms:justify-center ms:p-8 ms:text-center ms:pointer-events-none ms:select-none", children: [_jsx("p", { className: "ms:text-sm ms:font-semibold ms:text-mstext ms:mb-2", children: "No fields in this section" }), _jsx("p", { className: "ms:text-xs ms:text-mstextmuted ms:leading-relaxed", children: "Use the Tool Panel on the left to add fields." })] })), childIds.map((childId) => (_jsx(DraggableFieldItem, { id: childId, form: form, ui: ui, parentId: parentId, dragEnabled: dragEnabled, isSelected: selectedFieldId === parentId && selectedFieldChildId === childId, isActiveChild: selectedFieldId === parentId && selectedFieldChildId === childId, forceExpandVersion: sectionExpandSignal?.sectionId === childId
|
|
252
|
+
? sectionExpandSignal.version
|
|
253
|
+
: expandAllVersion, forceCollapseVersion: collapseAllVersion, nestedChildren: renderNestedChildren(childId, depth + 1) }, childId)))] }));
|
|
254
|
+
}, [
|
|
255
|
+
collapseAllVersion,
|
|
256
|
+
dragEnabled,
|
|
257
|
+
expandAllVersion,
|
|
258
|
+
form,
|
|
259
|
+
getVisibleChildIds,
|
|
260
|
+
sectionExpandSignal,
|
|
261
|
+
selectedFieldChildId,
|
|
262
|
+
selectedFieldId,
|
|
263
|
+
ui,
|
|
264
|
+
]);
|
|
265
|
+
// Clear selection on Escape key
|
|
266
|
+
React.useEffect(() => {
|
|
267
|
+
const handleKeyDown = (e) => {
|
|
268
|
+
if (e.key === 'Escape') {
|
|
269
|
+
ui.getState().selectField(null);
|
|
270
|
+
ui.getState().clearDragState();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
274
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
275
|
+
}, [ui]);
|
|
276
|
+
return (_jsxs("div", { className: "ms:flex ms:flex-col ms:flex-1 ms:min-h-0", children: [mode === 'build' && (_jsxs("div", { className: "ms:bg-mssurface ms:border-b ms:border-msborder ms:px-4 ms:py-4 ms:flex ms:items-center ms:justify-between ms:gap-2", children: [_jsx("span", { className: "ms:text-sm ms:font-semibold ms:text-mstext ms:select-none", children: "Fields" }), items.length > 0 && (_jsx("div", { className: "ms:flex ms:items-center ms:gap-1", children: _jsxs("button", { type: "button", title: allExpanded ? 'Collapse all' : 'Expand all', className: "ms:flex ms:items-center ms:gap-1 ms:px-2 ms:py-1 ms:text-xs ms:text-mstextmuted ms:hover:text-mstext ms:rounded ms:hover:bg-msbackgroundhover ms:transition-colors", onClick: (e) => {
|
|
277
|
+
e.stopPropagation();
|
|
278
|
+
if (allExpanded) {
|
|
279
|
+
setCollapseAllVersion((v) => (v ?? 0) + 1);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
setExpandAllVersion((v) => (v ?? 0) + 1);
|
|
283
|
+
}
|
|
284
|
+
setAllExpanded((v) => !v);
|
|
285
|
+
}, children: [allExpanded ? (_jsx(ViewSmallIcon, { className: "ms:w-3.5 ms:h-3.5" })) : (_jsx(ViewBigIcon, { className: "ms:w-3.5 ms:h-3.5" })), allExpanded ? 'Collapse all' : 'Expand all'] }) }))] })), items.length === 0 ? (_jsx("div", { className: "canvas-empty ms:flex ms:flex-1 ms:items-center ms:justify-center ms:min-h-[200px] ms:text-mstextmuted ms:text-sm", children: "No fields yet. Add a field from the Tool Panel to get started." })) : (_jsx("div", { ref: canvasRef, className: "canvas-fields ms:space-y-0 ms:flex-1 ms:min-h-0 ms:overflow-y-auto ms:px-4 ms:pt-3 ms:pb-4", "data-sortable-list": dragEnabled ? 'true' : undefined, "data-parent-id": "", children: items.map((id) => (_jsx(DraggableFieldItem, { id: id, form: form, ui: ui, dragEnabled: dragEnabled, isSelected: selectedFieldId === id && selectedFieldChildId === null, forceExpandVersion: sectionExpandSignal?.sectionId === id
|
|
286
|
+
? sectionExpandSignal.version
|
|
287
|
+
: expandAllVersion, forceCollapseVersion: collapseAllVersion, nestedChildren: renderNestedChildren(id) }, id))) }))] }));
|
|
288
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Editor } from '@monaco-editor/react';
|
|
4
|
+
import YAML from 'js-yaml';
|
|
5
|
+
import { formatZodValidationError, formDefinitionJSONSchema, formDefinitionSchema, } from '@esheet/core';
|
|
6
|
+
import { FeedbackModal } from './FeedbackModal.js';
|
|
7
|
+
const FORM_SCHEMA_URI = 'inmemory://esheet/form-definition.schema.json';
|
|
8
|
+
/** Detect dark mode from the document root and re-render on changes. */
|
|
9
|
+
function useIsDark() {
|
|
10
|
+
const [isDark, setIsDark] = React.useState(() => document.documentElement.classList.contains('dark'));
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
const observer = new MutationObserver(() => {
|
|
13
|
+
setIsDark(document.documentElement.classList.contains('dark'));
|
|
14
|
+
});
|
|
15
|
+
observer.observe(document.documentElement, {
|
|
16
|
+
attributes: true,
|
|
17
|
+
attributeFilter: ['class'],
|
|
18
|
+
});
|
|
19
|
+
return () => observer.disconnect();
|
|
20
|
+
}, []);
|
|
21
|
+
return isDark;
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function serialize(data, format) {
|
|
27
|
+
return format === 'json'
|
|
28
|
+
? JSON.stringify(data, null, 2)
|
|
29
|
+
: YAML.dump(data, { indent: 2, lineWidth: -1 });
|
|
30
|
+
}
|
|
31
|
+
function parse(text, format) {
|
|
32
|
+
return format === 'json' ? JSON.parse(text) : YAML.load(text);
|
|
33
|
+
}
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Component
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* CodeView — Monaco-based JSON/YAML editor for the form definition.
|
|
39
|
+
*
|
|
40
|
+
* Serialize on mount, live-validate on edit, auto-save on unmount.
|
|
41
|
+
*/
|
|
42
|
+
export function CodeView({ form, ui }) {
|
|
43
|
+
// Track whether user actually edited (avoids spurious saves from StrictMode double-mount)
|
|
44
|
+
const dirtyRef = React.useRef(false);
|
|
45
|
+
const isDark = useIsDark();
|
|
46
|
+
const [format, setFormat] = React.useState('yaml');
|
|
47
|
+
const initialCode = React.useMemo(() => {
|
|
48
|
+
try {
|
|
49
|
+
return serialize(form.getState().hydrateDefinition(), 'yaml');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
const codeRef = React.useRef(initialCode);
|
|
56
|
+
const formatRef = React.useRef('yaml');
|
|
57
|
+
const formRef = React.useRef(form);
|
|
58
|
+
const uiRef = React.useRef(ui);
|
|
59
|
+
const [code, setCode] = React.useState(initialCode);
|
|
60
|
+
const [error, setError] = React.useState('');
|
|
61
|
+
const [feedback, setFeedback] = React.useState({
|
|
62
|
+
open: false,
|
|
63
|
+
title: '',
|
|
64
|
+
message: '',
|
|
65
|
+
details: undefined,
|
|
66
|
+
variant: 'info',
|
|
67
|
+
});
|
|
68
|
+
const showFeedback = React.useCallback((variant, title, message, details) => {
|
|
69
|
+
setFeedback({
|
|
70
|
+
open: true,
|
|
71
|
+
title,
|
|
72
|
+
message,
|
|
73
|
+
details,
|
|
74
|
+
variant,
|
|
75
|
+
});
|
|
76
|
+
}, []);
|
|
77
|
+
// Keep refs in sync
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
formRef.current = form;
|
|
80
|
+
uiRef.current = ui;
|
|
81
|
+
formatRef.current = format;
|
|
82
|
+
});
|
|
83
|
+
// Clear error flag on mount
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
ui.getState().setCodeEditorHasError(false);
|
|
86
|
+
}, []);
|
|
87
|
+
// --- Handlers ---
|
|
88
|
+
/** Register JSON schema IntelliSense before Monaco creates the editor. */
|
|
89
|
+
const handleBeforeMount = (monaco) => {
|
|
90
|
+
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
|
91
|
+
validate: true,
|
|
92
|
+
schemas: [
|
|
93
|
+
{
|
|
94
|
+
uri: FORM_SCHEMA_URI,
|
|
95
|
+
fileMatch: ['*'],
|
|
96
|
+
schema: formDefinitionJSONSchema,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const handleCodeChange = (value) => {
|
|
102
|
+
const text = value ?? '';
|
|
103
|
+
setCode(text);
|
|
104
|
+
codeRef.current = text;
|
|
105
|
+
dirtyRef.current = true;
|
|
106
|
+
// Live validation
|
|
107
|
+
try {
|
|
108
|
+
const parsed = parse(text || '{}', format);
|
|
109
|
+
const validated = formDefinitionSchema.safeParse(parsed);
|
|
110
|
+
if (!validated.success) {
|
|
111
|
+
const issues = validated.error.issues.map(formatZodValidationError);
|
|
112
|
+
setError(`Invalid ${format.toUpperCase()}: ${issues[0] ?? 'Schema validation failed'}`);
|
|
113
|
+
ui.getState().setCodeEditorHasError(true);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setError('');
|
|
117
|
+
ui.getState().setCodeEditorHasError(false);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const message = err.message;
|
|
121
|
+
setError(`Invalid ${format.toUpperCase()}: ${message}`);
|
|
122
|
+
ui.getState().setCodeEditorHasError(true);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const handleFormatChange = (newFormat) => {
|
|
126
|
+
try {
|
|
127
|
+
const data = parse(code, format);
|
|
128
|
+
const converted = serialize(data, newFormat);
|
|
129
|
+
setCode(converted);
|
|
130
|
+
codeRef.current = converted;
|
|
131
|
+
setFormat(newFormat);
|
|
132
|
+
setError('');
|
|
133
|
+
ui.getState().setCodeEditorHasError(false);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const message = `Cannot convert: ${err.message}`;
|
|
137
|
+
setError(message);
|
|
138
|
+
ui.getState().setCodeEditorHasError(true);
|
|
139
|
+
showFeedback('error', 'Format Conversion Failed', message);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
// Auto-save on unmount (switching away from Code mode)
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
return () => {
|
|
145
|
+
if (!dirtyRef.current)
|
|
146
|
+
return;
|
|
147
|
+
const text = codeRef.current.trim();
|
|
148
|
+
const fs = formRef.current;
|
|
149
|
+
const uiApi = uiRef.current;
|
|
150
|
+
const fmt = formatRef.current;
|
|
151
|
+
if (!text) {
|
|
152
|
+
const currentId = fs.getState().hydrateDefinition().id;
|
|
153
|
+
fs.getState().loadDefinition({
|
|
154
|
+
id: currentId,
|
|
155
|
+
fields: [],
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const parsed = parse(text, fmt);
|
|
161
|
+
const validated = formDefinitionSchema.safeParse(parsed);
|
|
162
|
+
if (!validated.success)
|
|
163
|
+
return;
|
|
164
|
+
const next = validated.data;
|
|
165
|
+
// Only save if different from current definition
|
|
166
|
+
const current = fs.getState().hydrateDefinition();
|
|
167
|
+
if (JSON.stringify(current) === JSON.stringify(next))
|
|
168
|
+
return;
|
|
169
|
+
fs.getState().loadDefinition(next);
|
|
170
|
+
uiApi.getState().setCodeEditorHasError(false);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Error already shown in the editor header — don't push invalid data
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
177
|
+
return (_jsxs("div", { className: "code-view-container ms:flex ms:flex-col ms:flex-1 ms:min-h-0 ms:bg-msbackground", children: [_jsx(FeedbackModal, { open: feedback.open, title: feedback.title, message: feedback.message, details: feedback.details, variant: feedback.variant, onClose: () => setFeedback((prev) => ({
|
|
178
|
+
...prev,
|
|
179
|
+
open: false,
|
|
180
|
+
})) }), _jsxs("div", { className: "code-view-header ms:flex ms:items-center ms:justify-between ms:gap-3 ms:p-3 ms:bg-mssurface ms:border-b ms:border-msborder", children: [_jsx("div", { className: "format-toggle ms:flex ms:gap-1 ms:rounded-lg ms:border ms:border-msborder ms:bg-msbackground ms:p-1", children: ['yaml', 'json'].map((fmt) => (_jsx("button", { type: "button", onClick: () => handleFormatChange(fmt), className: `format-btn ms:px-3 ms:py-1 ms:rounded-md ms:text-sm ms:font-medium ms:transition-colors ms:border-0 ms:outline-none ms:focus:outline-none ms:cursor-pointer ${format === fmt
|
|
181
|
+
? 'ms:bg-msprimary ms:text-mstextsecondary ms:shadow-sm'
|
|
182
|
+
: 'ms:bg-transparent ms:text-mstextmuted ms:hover:text-mstext ms:hover:bg-mssurface'}`, children: fmt.toUpperCase() }, fmt))) }), _jsxs("div", { className: "code-view-status ms:flex ms:items-center ms:gap-2", children: [_jsx("span", { className: "ms:text-xs ms:text-mstextmuted", children: "Auto-saves when switching tabs" }), error && (_jsx("span", { className: "ms:text-xs ms:text-msdanger ms:bg-msdanger/10 ms:px-3 ms:py-1 ms:rounded-lg", children: error }))] })] }), _jsx("div", { className: "code-view-editor ms:flex-1 ms:overflow-hidden", children: _jsx(Editor, { height: "100%", language: format === 'yaml' ? 'yaml' : 'json', value: code, onChange: handleCodeChange, beforeMount: handleBeforeMount, theme: isDark ? 'vs-dark' : 'light', options: {
|
|
183
|
+
minimap: { enabled: false },
|
|
184
|
+
fontSize: 13,
|
|
185
|
+
lineHeight: 1.5,
|
|
186
|
+
wordWrap: 'on',
|
|
187
|
+
formatOnPaste: false,
|
|
188
|
+
formatOnType: false,
|
|
189
|
+
tabSize: 2,
|
|
190
|
+
automaticLayout: true,
|
|
191
|
+
scrollBeyondLastLine: false,
|
|
192
|
+
padding: { top: 16 },
|
|
193
|
+
contextmenu: true,
|
|
194
|
+
accessibilitySupport: 'auto',
|
|
195
|
+
ariaLabel: 'Code editor for form schema',
|
|
196
|
+
} }) })] }));
|
|
197
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ZodIssuesPanel } from '@esheet/fields';
|
|
3
|
+
const VARIANT_STYLES = {
|
|
4
|
+
info: 'ms:text-msprimary ms:bg-msprimary/10',
|
|
5
|
+
success: 'ms:text-msaccent ms:bg-msaccent/10',
|
|
6
|
+
warning: 'ms:text-mswarning ms:bg-mswarning/10',
|
|
7
|
+
error: 'ms:text-msdanger ms:bg-msdanger/10',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Reusable modal used for import and validation feedback.
|
|
11
|
+
*/
|
|
12
|
+
export function FeedbackModal({ open, title, message, details, issues, issuesTitle, issuesHint, content, variant = 'info', confirmLabel = 'OK', cancelLabel = 'Cancel', showCancel = false, onConfirm, onClose, }) {
|
|
13
|
+
if (!open)
|
|
14
|
+
return null;
|
|
15
|
+
return (_jsx("div", { className: "feedback-modal-overlay ms:fixed ms:inset-0 ms:z-50 ms:flex ms:items-center ms:justify-center ms:bg-msoverlay ms:px-4 ms:py-8", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: onClose, children: _jsxs("div", { className: "feedback-modal-content ms:w-full ms:max-w-2xl ms:rounded-xl ms:bg-mssurface ms:border ms:border-msborder ms:shadow-2xl ms:p-5", onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { className: "ms:flex ms:items-start ms:gap-3 ms:mb-3", children: [_jsx("div", { className: `ms:inline-flex ms:h-7 ms:min-w-7 ms:items-center ms:justify-center ms:rounded-full ms:text-xs ms:font-semibold ${VARIANT_STYLES[variant]}`, children: "i" }), _jsxs("div", { className: "ms:min-w-0", children: [_jsx("h3", { className: "ms:text-base ms:font-semibold ms:text-mstext", children: title }), _jsx("p", { className: "ms:text-sm ms:text-mstextmuted ms:mt-1 ms:whitespace-pre-wrap", children: message })] })] }), issues && issues.length > 0 && (_jsx(ZodIssuesPanel, { title: issuesTitle ?? 'Validation Issues', issues: issues, hint: issuesHint ?? 'Please resolve these issues and try again.', className: "ms:mb-3 ms:p-4", variant: variant === 'error'
|
|
16
|
+
? 'error'
|
|
17
|
+
: variant === 'warning'
|
|
18
|
+
? 'warning'
|
|
19
|
+
: variant === 'success'
|
|
20
|
+
? 'success'
|
|
21
|
+
: 'info' })), details && (_jsx("pre", { className: "ms:text-xs ms:text-mstext ms:bg-msbackground ms:border ms:border-msborder ms:rounded ms:p-2 ms:whitespace-pre-wrap ms:break-words ms:max-h-96 ms:overflow-auto", children: details })), content, _jsxs("div", { className: "ms:mt-4 ms:flex ms:justify-end ms:gap-2", children: [showCancel && (_jsx("button", { type: "button", onClick: onClose, className: "ms:px-4 ms:py-2 ms:rounded-lg ms:border ms:border-msborder ms:bg-mssurface ms:text-mstext ms:text-sm ms:font-medium ms:hover:bg-msbackground ms:transition-colors ms:outline-none ms:focus:outline-none ms:cursor-pointer", children: cancelLabel })), _jsx("button", { type: "button", onClick: onConfirm ?? onClose, className: "ms:px-4 ms:py-2 ms:rounded-lg ms:bg-msprimary ms:text-mstextsecondary ms:text-sm ms:font-medium ms:hover:bg-msprimary/90 ms:transition-colors ms:border-0 ms:outline-none ms:focus:outline-none ms:cursor-pointer", children: confirmLabel })] })] }) }));
|
|
22
|
+
}
|