@jxsuite/studio 0.10.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/studio.js +5114 -3424
- package/dist/studio.js.map +65 -49
- package/package.json +6 -3
- package/src/browse/browse.js +27 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +136 -17
- package/src/canvas/canvas-render.js +154 -21
- package/src/canvas/canvas-utils.js +21 -23
- package/src/editor/component-inline-edit.js +54 -41
- package/src/editor/content-inline-edit.js +46 -47
- package/src/editor/context-menu.js +63 -39
- package/src/editor/convert-to-component.js +11 -14
- package/src/editor/insertion-helper.js +8 -10
- package/src/editor/shortcuts.js +69 -39
- package/src/files/components.js +15 -4
- package/src/files/files.js +72 -24
- package/src/panels/activity-bar.js +29 -7
- package/src/panels/block-action-bar.js +104 -80
- package/src/panels/canvas-dnd.js +132 -50
- package/src/panels/dnd.js +32 -28
- package/src/panels/editors.js +7 -14
- package/src/panels/elements-panel.js +9 -3
- package/src/panels/events-panel.js +44 -39
- package/src/panels/git-panel.js +45 -3
- package/src/panels/layers-panel.js +16 -11
- package/src/panels/left-panel.js +108 -43
- package/src/panels/overlays.js +97 -35
- package/src/panels/panel-events.js +18 -11
- package/src/panels/properties-panel.js +467 -98
- package/src/panels/right-panel.js +85 -37
- package/src/panels/shared.js +0 -22
- package/src/panels/signals-panel.js +125 -54
- package/src/panels/statusbar.js +23 -8
- package/src/panels/style-inputs.js +4 -2
- package/src/panels/style-panel.js +128 -105
- package/src/panels/stylebook-panel.js +42 -17
- package/src/panels/tab-strip.js +124 -0
- package/src/panels/toolbar.js +39 -9
- package/src/reactivity.js +28 -0
- package/src/settings/content-types-editor.js +78 -8
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +99 -11
- package/src/site-context.js +105 -0
- package/src/state.js +0 -456
- package/src/store.js +105 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/ui/spectrum.js +2 -0
- package/src/view.js +3 -0
- package/src/workspace/workspace.js +89 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab strip — renders open tabs above the canvas area.
|
|
3
|
+
*
|
|
4
|
+
* Uses the reactive workspace model: reads from workspace.tabOrder, workspace.tabs,
|
|
5
|
+
* workspace.activeTabId. Clicks call activateTab/closeTab from workspace.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
9
|
+
import { effect, effectScope } from "../reactivity.js";
|
|
10
|
+
import { workspace, activateTab, closeTab } from "../workspace/workspace.js";
|
|
11
|
+
|
|
12
|
+
/** @typedef {import("../tabs/tab.js").Tab} Tab */
|
|
13
|
+
|
|
14
|
+
/** @type {HTMLElement | null} */
|
|
15
|
+
let _host = null;
|
|
16
|
+
|
|
17
|
+
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
18
|
+
let _scope = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mount the tab strip into the given host element.
|
|
22
|
+
*
|
|
23
|
+
* @param {HTMLElement} host
|
|
24
|
+
*/
|
|
25
|
+
export function mount(host) {
|
|
26
|
+
_host = host;
|
|
27
|
+
_scope = effectScope();
|
|
28
|
+
_scope.run(() => {
|
|
29
|
+
effect(() => {
|
|
30
|
+
void workspace.tabOrder;
|
|
31
|
+
void workspace.activeTabId;
|
|
32
|
+
for (const tab of workspace.tabs.values()) {
|
|
33
|
+
void tab.doc.dirty;
|
|
34
|
+
void tab.documentPath;
|
|
35
|
+
}
|
|
36
|
+
render();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function unmount() {
|
|
42
|
+
_scope?.stop();
|
|
43
|
+
_scope = null;
|
|
44
|
+
_host = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function render() {
|
|
48
|
+
if (!_host) return;
|
|
49
|
+
|
|
50
|
+
if (workspace.tabOrder.length <= 1) {
|
|
51
|
+
litRender(nothing, _host);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
litRender(
|
|
56
|
+
html`
|
|
57
|
+
<div class="tab-strip">
|
|
58
|
+
${workspace.tabOrder.map((id) => {
|
|
59
|
+
const tab = workspace.tabs.get(id);
|
|
60
|
+
if (!tab) return nothing;
|
|
61
|
+
const isActive = id === workspace.activeTabId;
|
|
62
|
+
const isDirty = tab.doc.dirty;
|
|
63
|
+
const label = tabLabel(tab);
|
|
64
|
+
return html`
|
|
65
|
+
<div
|
|
66
|
+
class="tab-strip-tab ${isActive ? "active" : ""}"
|
|
67
|
+
@click=${() => activateTab(id)}
|
|
68
|
+
@auxclick=${(/** @type {MouseEvent} */ e) => {
|
|
69
|
+
if (e.button === 1) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
requestClose(id);
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
title=${tab.documentPath || "Untitled"}
|
|
75
|
+
>
|
|
76
|
+
<span class="tab-strip-label">${label}</span>
|
|
77
|
+
${isDirty ? html`<span class="tab-strip-dirty">●</span>` : nothing}
|
|
78
|
+
<button
|
|
79
|
+
class="tab-strip-close"
|
|
80
|
+
@click=${(/** @type {Event} */ e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
requestClose(id);
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
×
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
`,
|
|
92
|
+
_host,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive a short label from the tab's documentPath.
|
|
98
|
+
*
|
|
99
|
+
* @param {Tab} tab
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
function tabLabel(tab) {
|
|
103
|
+
const path = tab.documentPath;
|
|
104
|
+
if (!path) return "Untitled";
|
|
105
|
+
const parts = path.split("/");
|
|
106
|
+
return parts[parts.length - 1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Close a tab, prompting if dirty.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} id
|
|
113
|
+
*/
|
|
114
|
+
function requestClose(id) {
|
|
115
|
+
const tab = workspace.tabs.get(id);
|
|
116
|
+
if (!tab) return;
|
|
117
|
+
if (tab.doc.dirty) {
|
|
118
|
+
const confirmed = window.confirm(
|
|
119
|
+
`"${tabLabel(tab)}" has unsaved changes. Close without saving?`,
|
|
120
|
+
);
|
|
121
|
+
if (!confirmed) return;
|
|
122
|
+
}
|
|
123
|
+
closeTab(id);
|
|
124
|
+
}
|
package/src/panels/toolbar.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { html, render as litRender, nothing } from "lit-html";
|
|
7
|
-
import {
|
|
7
|
+
import { updateSession, updateUi } from "../store.js";
|
|
8
|
+
import { undo as tabUndo, redo as tabRedo } from "../tabs/transact.js";
|
|
9
|
+
import { effect, effectScope } from "../reactivity.js";
|
|
10
|
+
import { activeTab } from "../workspace/workspace.js";
|
|
8
11
|
import { getEffectiveMedia } from "../site-context.js";
|
|
9
12
|
import { mediaDisplayName } from "./shared.js";
|
|
10
13
|
import { view } from "../view.js";
|
|
@@ -15,8 +18,8 @@ let _rootEl = null;
|
|
|
15
18
|
/** @type {any} */
|
|
16
19
|
let _ctx = null;
|
|
17
20
|
|
|
18
|
-
/** @type {(
|
|
19
|
-
let
|
|
21
|
+
/** @type {import("@vue/reactivity").EffectScope | null} */
|
|
22
|
+
let _scope = null;
|
|
20
23
|
|
|
21
24
|
const toolbarIconMap = /** @type {Record<string, any>} */ ({
|
|
22
25
|
"sp-icon-folder-open": html`<sp-icon-folder-open slot="icon"></sp-icon-folder-open>`,
|
|
@@ -59,12 +62,28 @@ function tbBtnTpl(label, onClick, iconTag) {
|
|
|
59
62
|
export function mount(rootEl, ctx) {
|
|
60
63
|
_rootEl = rootEl;
|
|
61
64
|
_ctx = ctx;
|
|
62
|
-
|
|
65
|
+
_scope = effectScope();
|
|
66
|
+
_scope.run(() => {
|
|
67
|
+
effect(() => {
|
|
68
|
+
const tab = activeTab.value;
|
|
69
|
+
if (!tab) return;
|
|
70
|
+
// Read reactive properties to establish tracking
|
|
71
|
+
void tab.doc.document;
|
|
72
|
+
void tab.doc.dirty;
|
|
73
|
+
void tab.doc.mode;
|
|
74
|
+
void tab.session.selection;
|
|
75
|
+
void tab.session.ui.editingFunction;
|
|
76
|
+
void tab.session.ui.featureToggles;
|
|
77
|
+
void tab.session.ui.leftTab;
|
|
78
|
+
void tab.session.ui.rightTab;
|
|
79
|
+
render();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
export function unmount() {
|
|
66
|
-
|
|
67
|
-
|
|
85
|
+
_scope?.stop();
|
|
86
|
+
_scope = null;
|
|
68
87
|
_rootEl = null;
|
|
69
88
|
_ctx = null;
|
|
70
89
|
}
|
|
@@ -79,7 +98,18 @@ export function render() {
|
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
function toolbarTemplate() {
|
|
82
|
-
const
|
|
101
|
+
const tab = activeTab.value;
|
|
102
|
+
if (!tab) return html``;
|
|
103
|
+
const S = /** @type {any} */ ({
|
|
104
|
+
document: tab.doc.document,
|
|
105
|
+
ui: tab.session.ui,
|
|
106
|
+
mode: tab.doc.mode,
|
|
107
|
+
selection: tab.session.selection,
|
|
108
|
+
dirty: tab.doc.dirty,
|
|
109
|
+
documentPath: tab.documentPath,
|
|
110
|
+
fileHandle: tab.fileHandle,
|
|
111
|
+
documentStack: tab.session.documentStack,
|
|
112
|
+
});
|
|
83
113
|
const canvasMode = _ctx.getCanvasMode();
|
|
84
114
|
const hasStack = S.documentStack && S.documentStack.length > 0;
|
|
85
115
|
const hasFunc = !!S.ui.editingFunction;
|
|
@@ -203,8 +233,8 @@ function toolbarTemplate() {
|
|
|
203
233
|
${tbBtnTpl("Save", _ctx.saveFile, "sp-icon-save-floppy")}
|
|
204
234
|
</sp-action-group>
|
|
205
235
|
<sp-action-group compact size="s">
|
|
206
|
-
${tbBtnTpl("Undo", () =>
|
|
207
|
-
${tbBtnTpl("Redo", () =>
|
|
236
|
+
${tbBtnTpl("Undo", () => tabUndo(activeTab.value), "sp-icon-undo")}
|
|
237
|
+
${tbBtnTpl("Redo", () => tabRedo(activeTab.value), "sp-icon-redo")}
|
|
208
238
|
</sp-action-group>
|
|
209
239
|
<div class="tb-spacer"></div>
|
|
210
240
|
${S.documentPath
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports from @vue/reactivity. All studio code imports reactivity primitives from here (not
|
|
3
|
+
* directly from the package) for grep-ability and potential future wrapping.
|
|
4
|
+
*
|
|
5
|
+
* Version is pinned to match @jxsuite/runtime — reactive proxies from one version don't track in
|
|
6
|
+
* effects from another. See migrate-reactivity-workspace.md for rationale.
|
|
7
|
+
*
|
|
8
|
+
* Batching note: Vue's reactivity batches synchronous effect re-runs within a microtask. Use
|
|
9
|
+
* queueMicrotask() or Promise.resolve().then() to wait for the flush if needed (@vue/reactivity
|
|
10
|
+
* standalone doesn't ship nextTick).
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
reactive,
|
|
14
|
+
ref,
|
|
15
|
+
computed,
|
|
16
|
+
readonly,
|
|
17
|
+
shallowReactive,
|
|
18
|
+
shallowRef,
|
|
19
|
+
effect,
|
|
20
|
+
effectScope,
|
|
21
|
+
getCurrentScope,
|
|
22
|
+
onScopeDispose,
|
|
23
|
+
pauseTracking,
|
|
24
|
+
resetTracking,
|
|
25
|
+
toRaw,
|
|
26
|
+
isReactive,
|
|
27
|
+
isRef,
|
|
28
|
+
} from "@vue/reactivity";
|
|
@@ -8,14 +8,19 @@
|
|
|
8
8
|
import { html, render as litRender } from "lit-html";
|
|
9
9
|
import { getPlatform } from "../platform.js";
|
|
10
10
|
import { projectState } from "../store.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
fieldCardTpl,
|
|
13
|
+
addFieldFormTpl,
|
|
14
|
+
schemaForType,
|
|
15
|
+
detectFieldFormat,
|
|
16
|
+
} from "./schema-field-ui.js";
|
|
12
17
|
|
|
13
18
|
// ─── Module state ─────────────────────────────────────────────────────────────
|
|
14
19
|
|
|
15
20
|
/** @type {string | null} */
|
|
16
21
|
let selectedContentType = null;
|
|
17
22
|
let showAddField = false;
|
|
18
|
-
let newFieldState = { name: "", type: "string", required: false };
|
|
23
|
+
let newFieldState = { name: "", type: "string", format: "", required: false };
|
|
19
24
|
let showNewContentType = false;
|
|
20
25
|
let newContentTypeName = "";
|
|
21
26
|
|
|
@@ -76,7 +81,7 @@ function handleAddField(rerender) {
|
|
|
76
81
|
if (!schema) return;
|
|
77
82
|
|
|
78
83
|
if (!schema.properties) schema.properties = {};
|
|
79
|
-
schema.properties[name] = schemaForType(newFieldState.type);
|
|
84
|
+
schema.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
|
|
80
85
|
|
|
81
86
|
if (newFieldState.required) {
|
|
82
87
|
if (!schema.required) schema.required = [];
|
|
@@ -84,7 +89,7 @@ function handleAddField(rerender) {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
showAddField = false;
|
|
87
|
-
newFieldState = { name: "", type: "string", required: false };
|
|
92
|
+
newFieldState = { name: "", type: "string", format: "", required: false };
|
|
88
93
|
rerender();
|
|
89
94
|
saveProjectConfig();
|
|
90
95
|
}
|
|
@@ -158,7 +163,41 @@ function handleChangeType(fieldName, newType, rerender) {
|
|
|
158
163
|
const schema = getSelectedSchema();
|
|
159
164
|
if (!schema?.properties?.[fieldName]) return;
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
const oldFormat =
|
|
167
|
+
newType === "string" || newType === "array"
|
|
168
|
+
? detectFieldFormat(schema.properties[fieldName])
|
|
169
|
+
: undefined;
|
|
170
|
+
schema.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
|
|
171
|
+
rerender();
|
|
172
|
+
saveProjectConfig();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} fieldName
|
|
177
|
+
* @param {string} format
|
|
178
|
+
* @param {() => void} rerender
|
|
179
|
+
*/
|
|
180
|
+
function handleChangeFormat(fieldName, format, rerender) {
|
|
181
|
+
const schema = getSelectedSchema();
|
|
182
|
+
if (!schema?.properties?.[fieldName]) return;
|
|
183
|
+
|
|
184
|
+
const prop = schema.properties[fieldName];
|
|
185
|
+
const type = prop.type || "string";
|
|
186
|
+
schema.properties[fieldName] = schemaForType(type, format || undefined);
|
|
187
|
+
rerender();
|
|
188
|
+
saveProjectConfig();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {string} fieldName
|
|
193
|
+
* @param {string} target
|
|
194
|
+
* @param {() => void} rerender
|
|
195
|
+
*/
|
|
196
|
+
function handleChangeRefTarget(fieldName, target, rerender) {
|
|
197
|
+
const schema = getSelectedSchema();
|
|
198
|
+
if (!schema?.properties) return;
|
|
199
|
+
|
|
200
|
+
schema.properties[fieldName] = { $ref: `#/contentTypes/${target}` };
|
|
162
201
|
rerender();
|
|
163
202
|
saveProjectConfig();
|
|
164
203
|
}
|
|
@@ -264,7 +303,29 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
|
|
|
264
303
|
const parent = schema?.properties?.[parentName];
|
|
265
304
|
if (!parent?.properties?.[childName]) return;
|
|
266
305
|
|
|
267
|
-
|
|
306
|
+
const oldFormat =
|
|
307
|
+
newType === "string" || newType === "array"
|
|
308
|
+
? detectFieldFormat(parent.properties[childName])
|
|
309
|
+
: undefined;
|
|
310
|
+
parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
|
|
311
|
+
rerender();
|
|
312
|
+
saveProjectConfig();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} parentName
|
|
317
|
+
* @param {string} childName
|
|
318
|
+
* @param {string} format
|
|
319
|
+
* @param {() => void} rerender
|
|
320
|
+
*/
|
|
321
|
+
function handleChangeNestedFormat(parentName, childName, format, rerender) {
|
|
322
|
+
const schema = getSelectedSchema();
|
|
323
|
+
const parent = schema?.properties?.[parentName];
|
|
324
|
+
if (!parent?.properties?.[childName]) return;
|
|
325
|
+
|
|
326
|
+
const prop = parent.properties[childName];
|
|
327
|
+
const type = prop.type || "string";
|
|
328
|
+
parent.properties[childName] = schemaForType(type, format || undefined);
|
|
268
329
|
rerender();
|
|
269
330
|
saveProjectConfig();
|
|
270
331
|
}
|
|
@@ -367,15 +428,24 @@ export function renderContentTypesEditor(container) {
|
|
|
367
428
|
onToggleRequired: (n) => handleToggleRequired(n, rerender),
|
|
368
429
|
onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
|
|
369
430
|
onChangeType: (n, t) => handleChangeType(n, t, rerender),
|
|
431
|
+
onChangeFormat: (n, f) => handleChangeFormat(n, f, rerender),
|
|
432
|
+
onChangeRefTarget: (n, target) => handleChangeRefTarget(n, target, rerender),
|
|
370
433
|
onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
|
|
371
434
|
onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
|
|
372
435
|
onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
|
|
373
436
|
onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
|
|
374
437
|
onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
|
|
438
|
+
onChangeNestedFormat: (p, c, f) => handleChangeNestedFormat(p, c, f, rerender),
|
|
375
439
|
};
|
|
376
440
|
|
|
377
441
|
const fieldCards = Object.entries(properties).map(([name, def]) =>
|
|
378
|
-
fieldCardTpl(
|
|
442
|
+
fieldCardTpl(
|
|
443
|
+
name,
|
|
444
|
+
/** @type {any} */ (def),
|
|
445
|
+
required.includes(name),
|
|
446
|
+
handlers,
|
|
447
|
+
contentTypeNames,
|
|
448
|
+
),
|
|
379
449
|
);
|
|
380
450
|
|
|
381
451
|
editorTpl = html`
|
|
@@ -402,7 +472,7 @@ export function renderContentTypesEditor(container) {
|
|
|
402
472
|
onConfirm: () => handleAddField(rerender),
|
|
403
473
|
onCancel: () => {
|
|
404
474
|
showAddField = false;
|
|
405
|
-
newFieldState = { name: "", type: "string", required: false };
|
|
475
|
+
newFieldState = { name: "", type: "string", format: "", required: false };
|
|
406
476
|
rerender();
|
|
407
477
|
},
|
|
408
478
|
})
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
import { html, render as litRender } from "lit-html";
|
|
10
10
|
import { getPlatform } from "../platform.js";
|
|
11
11
|
import { projectState } from "../store.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
fieldCardTpl,
|
|
14
|
+
addFieldFormTpl,
|
|
15
|
+
schemaForType,
|
|
16
|
+
detectFieldFormat,
|
|
17
|
+
} from "./schema-field-ui.js";
|
|
13
18
|
|
|
14
19
|
// ─── Module state ─────────────────────────────────────────────────────────────
|
|
15
20
|
|
|
16
21
|
/** @type {string | null} */
|
|
17
22
|
let selectedDef = null;
|
|
18
23
|
let showAddField = false;
|
|
19
|
-
let newFieldState = { name: "", type: "string", required: false };
|
|
24
|
+
let newFieldState = { name: "", type: "string", format: "", required: false };
|
|
20
25
|
let showNewDef = false;
|
|
21
26
|
let newDefName = "";
|
|
22
27
|
|
|
@@ -70,7 +75,7 @@ function handleAddField(rerender) {
|
|
|
70
75
|
if (!def) return;
|
|
71
76
|
|
|
72
77
|
if (!def.properties) def.properties = {};
|
|
73
|
-
def.properties[name] = schemaForType(newFieldState.type);
|
|
78
|
+
def.properties[name] = schemaForType(newFieldState.type, newFieldState.format || undefined);
|
|
74
79
|
|
|
75
80
|
if (newFieldState.required) {
|
|
76
81
|
if (!def.required) def.required = [];
|
|
@@ -78,7 +83,7 @@ function handleAddField(rerender) {
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
showAddField = false;
|
|
81
|
-
newFieldState = { name: "", type: "string", required: false };
|
|
86
|
+
newFieldState = { name: "", type: "string", format: "", required: false };
|
|
82
87
|
rerender();
|
|
83
88
|
saveProjectConfig();
|
|
84
89
|
}
|
|
@@ -150,7 +155,27 @@ function handleChangeType(fieldName, newType, rerender) {
|
|
|
150
155
|
const def = getSelectedDef();
|
|
151
156
|
if (!def?.properties?.[fieldName]) return;
|
|
152
157
|
|
|
153
|
-
|
|
158
|
+
const oldFormat =
|
|
159
|
+
newType === "string" || newType === "array"
|
|
160
|
+
? detectFieldFormat(def.properties[fieldName])
|
|
161
|
+
: undefined;
|
|
162
|
+
def.properties[fieldName] = schemaForType(newType, oldFormat || undefined);
|
|
163
|
+
rerender();
|
|
164
|
+
saveProjectConfig();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} fieldName
|
|
169
|
+
* @param {string} format
|
|
170
|
+
* @param {() => void} rerender
|
|
171
|
+
*/
|
|
172
|
+
function handleChangeFormat(fieldName, format, rerender) {
|
|
173
|
+
const def = getSelectedDef();
|
|
174
|
+
if (!def?.properties?.[fieldName]) return;
|
|
175
|
+
|
|
176
|
+
const prop = def.properties[fieldName];
|
|
177
|
+
const type = prop.type || "string";
|
|
178
|
+
def.properties[fieldName] = schemaForType(type, format || undefined);
|
|
154
179
|
rerender();
|
|
155
180
|
saveProjectConfig();
|
|
156
181
|
}
|
|
@@ -256,7 +281,29 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
|
|
|
256
281
|
const parent = def?.properties?.[parentName];
|
|
257
282
|
if (!parent?.properties?.[childName]) return;
|
|
258
283
|
|
|
259
|
-
|
|
284
|
+
const oldFormat =
|
|
285
|
+
newType === "string" || newType === "array"
|
|
286
|
+
? detectFieldFormat(parent.properties[childName])
|
|
287
|
+
: undefined;
|
|
288
|
+
parent.properties[childName] = schemaForType(newType, oldFormat || undefined);
|
|
289
|
+
rerender();
|
|
290
|
+
saveProjectConfig();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {string} parentName
|
|
295
|
+
* @param {string} childName
|
|
296
|
+
* @param {string} format
|
|
297
|
+
* @param {() => void} rerender
|
|
298
|
+
*/
|
|
299
|
+
function handleChangeNestedFormat(parentName, childName, format, rerender) {
|
|
300
|
+
const def = getSelectedDef();
|
|
301
|
+
const parent = def?.properties?.[parentName];
|
|
302
|
+
if (!parent?.properties?.[childName]) return;
|
|
303
|
+
|
|
304
|
+
const prop = parent.properties[childName];
|
|
305
|
+
const type = prop.type || "string";
|
|
306
|
+
parent.properties[childName] = schemaForType(type, format || undefined);
|
|
260
307
|
rerender();
|
|
261
308
|
saveProjectConfig();
|
|
262
309
|
}
|
|
@@ -358,11 +405,13 @@ export function renderDefsEditor(container) {
|
|
|
358
405
|
onToggleRequired: (n) => handleToggleRequired(n, rerender),
|
|
359
406
|
onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
|
|
360
407
|
onChangeType: (n, t) => handleChangeType(n, t, rerender),
|
|
408
|
+
onChangeFormat: (n, f) => handleChangeFormat(n, f, rerender),
|
|
361
409
|
onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
|
|
362
410
|
onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
|
|
363
411
|
onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
|
|
364
412
|
onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
|
|
365
413
|
onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
|
|
414
|
+
onChangeNestedFormat: (p, c, f) => handleChangeNestedFormat(p, c, f, rerender),
|
|
366
415
|
};
|
|
367
416
|
|
|
368
417
|
const fieldCards = Object.entries(properties).map(([name, fieldDef]) =>
|
|
@@ -392,7 +441,7 @@ export function renderDefsEditor(container) {
|
|
|
392
441
|
onConfirm: () => handleAddField(rerender),
|
|
393
442
|
onCancel: () => {
|
|
394
443
|
showAddField = false;
|
|
395
|
-
newFieldState = { name: "", type: "string", required: false };
|
|
444
|
+
newFieldState = { name: "", type: "string", format: "", required: false };
|
|
396
445
|
rerender();
|
|
397
446
|
},
|
|
398
447
|
})
|