@jxsuite/studio 0.11.0 → 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/dist/studio.js +4248 -3412
- package/dist/studio.js.map +49 -43
- package/package.json +5 -3
- package/src/canvas/canvas-diff.js +184 -0
- package/src/canvas/canvas-helpers.js +10 -14
- package/src/canvas/canvas-live-render.js +3 -2
- package/src/canvas/canvas-render.js +152 -20
- 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/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 +91 -41
- package/src/panels/panel-events.js +9 -12
- package/src/panels/properties-panel.js +179 -104
- 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 +11 -15
- 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 +56 -7
- package/src/settings/defs-editor.js +56 -7
- package/src/settings/schema-field-ui.js +60 -25
- package/src/state.js +0 -456
- package/src/store.js +97 -124
- package/src/studio.js +112 -121
- package/src/tabs/tab.js +153 -0
- package/src/tabs/transact.js +406 -0
- package/src/workspace/workspace.js +89 -0
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,27 @@ 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);
|
|
162
187
|
rerender();
|
|
163
188
|
saveProjectConfig();
|
|
164
189
|
}
|
|
@@ -278,7 +303,29 @@ function handleChangeNestedType(parentName, childName, newType, rerender) {
|
|
|
278
303
|
const parent = schema?.properties?.[parentName];
|
|
279
304
|
if (!parent?.properties?.[childName]) return;
|
|
280
305
|
|
|
281
|
-
|
|
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);
|
|
282
329
|
rerender();
|
|
283
330
|
saveProjectConfig();
|
|
284
331
|
}
|
|
@@ -381,12 +428,14 @@ export function renderContentTypesEditor(container) {
|
|
|
381
428
|
onToggleRequired: (n) => handleToggleRequired(n, rerender),
|
|
382
429
|
onRename: (oldN, newN) => handleRenameField(oldN, newN, rerender),
|
|
383
430
|
onChangeType: (n, t) => handleChangeType(n, t, rerender),
|
|
431
|
+
onChangeFormat: (n, f) => handleChangeFormat(n, f, rerender),
|
|
384
432
|
onChangeRefTarget: (n, target) => handleChangeRefTarget(n, target, rerender),
|
|
385
433
|
onAddNestedField: (p, s) => handleAddNestedField(p, s, rerender),
|
|
386
434
|
onDeleteNested: (p, c) => handleDeleteNested(p, c, rerender),
|
|
387
435
|
onToggleNestedRequired: (p, c) => handleToggleNestedRequired(p, c, rerender),
|
|
388
436
|
onRenameNested: (p, o, n) => handleRenameNested(p, o, n, rerender),
|
|
389
437
|
onChangeNestedType: (p, c, t) => handleChangeNestedType(p, c, t, rerender),
|
|
438
|
+
onChangeNestedFormat: (p, c, f) => handleChangeNestedFormat(p, c, f, rerender),
|
|
390
439
|
};
|
|
391
440
|
|
|
392
441
|
const fieldCards = Object.entries(properties).map(([name, def]) =>
|
|
@@ -423,7 +472,7 @@ export function renderContentTypesEditor(container) {
|
|
|
423
472
|
onConfirm: () => handleAddField(rerender),
|
|
424
473
|
onCancel: () => {
|
|
425
474
|
showAddField = false;
|
|
426
|
-
newFieldState = { name: "", type: "string", required: false };
|
|
475
|
+
newFieldState = { name: "", type: "string", format: "", required: false };
|
|
427
476
|
rerender();
|
|
428
477
|
},
|
|
429
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
|
})
|
|
@@ -5,17 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { html, nothing } from "lit-html";
|
|
7
7
|
|
|
8
|
-
export const FIELD_TYPES = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"boolean",
|
|
12
|
-
"array",
|
|
13
|
-
"object",
|
|
14
|
-
"date",
|
|
15
|
-
"image",
|
|
16
|
-
"gallery",
|
|
17
|
-
"reference",
|
|
18
|
-
];
|
|
8
|
+
export const FIELD_TYPES = ["string", "number", "boolean", "array", "object", "reference"];
|
|
9
|
+
|
|
10
|
+
export const FORMAT_OPTIONS = ["", "image", "date", "color"];
|
|
19
11
|
|
|
20
12
|
/**
|
|
21
13
|
* @typedef {{
|
|
@@ -34,6 +26,7 @@ export const FIELD_TYPES = [
|
|
|
34
26
|
* onToggleRequired: (name: string) => void;
|
|
35
27
|
* onRename: (oldName: string, newName: string) => void;
|
|
36
28
|
* onChangeType: (name: string, newType: string) => void;
|
|
29
|
+
* onChangeFormat?: (name: string, format: string) => void;
|
|
37
30
|
* onChangeRefTarget?: (name: string, target: string) => void;
|
|
38
31
|
* onAddNestedField?: (
|
|
39
32
|
* parentName: string,
|
|
@@ -43,6 +36,7 @@ export const FIELD_TYPES = [
|
|
|
43
36
|
* onToggleNestedRequired?: (parentName: string, childName: string) => void;
|
|
44
37
|
* onRenameNested?: (parentName: string, oldChild: string, newChild: string) => void;
|
|
45
38
|
* onChangeNestedType?: (parentName: string, childName: string, newType: string) => void;
|
|
39
|
+
* onChangeNestedFormat?: (parentName: string, childName: string, format: string) => void;
|
|
46
40
|
* }} FieldHandlers
|
|
47
41
|
*/
|
|
48
42
|
|
|
@@ -54,12 +48,20 @@ export const FIELD_TYPES = [
|
|
|
54
48
|
*/
|
|
55
49
|
export function detectFieldType(schema) {
|
|
56
50
|
if (schema.$ref) return "reference";
|
|
57
|
-
if (schema.format === "image") return "image";
|
|
58
|
-
if (schema.format === "date") return "date";
|
|
59
|
-
if (schema.type === "array" && schema.items?.format === "image") return "gallery";
|
|
60
51
|
return schema.type || "string";
|
|
61
52
|
}
|
|
62
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Detect the format from a JSON Schema property definition.
|
|
56
|
+
*
|
|
57
|
+
* @param {SchemaProperty} schema
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function detectFieldFormat(schema) {
|
|
61
|
+
if (schema.type === "array" && schema.items?.format) return schema.items.format;
|
|
62
|
+
return schema.format || "";
|
|
63
|
+
}
|
|
64
|
+
|
|
63
65
|
/**
|
|
64
66
|
* Render a single schema field as an inline-editable form row.
|
|
65
67
|
*
|
|
@@ -72,6 +74,7 @@ export function detectFieldType(schema) {
|
|
|
72
74
|
*/
|
|
73
75
|
export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, contentTypeNames = []) {
|
|
74
76
|
const type = detectFieldType(fieldSchema);
|
|
77
|
+
const format = detectFieldFormat(fieldSchema);
|
|
75
78
|
const isNested = type === "object";
|
|
76
79
|
const isRef = type === "reference";
|
|
77
80
|
const nestedProps = fieldSchema.properties || {};
|
|
@@ -100,6 +103,11 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
|
|
|
100
103
|
}}
|
|
101
104
|
></sp-textfield>
|
|
102
105
|
${typePickerTpl(type, (newType) => handlers.onChangeType(fieldName, newType))}
|
|
106
|
+
${type === "string" || type === "array"
|
|
107
|
+
? formatPickerTpl(format, (f) => {
|
|
108
|
+
if (handlers.onChangeFormat) handlers.onChangeFormat(fieldName, f);
|
|
109
|
+
})
|
|
110
|
+
: nothing}
|
|
103
111
|
<sp-switch
|
|
104
112
|
size="s"
|
|
105
113
|
?checked=${isRequired}
|
|
@@ -166,6 +174,7 @@ export function fieldCardTpl(fieldName, fieldSchema, isRequired, handlers, conte
|
|
|
166
174
|
*/
|
|
167
175
|
function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, handlers) {
|
|
168
176
|
const type = detectFieldType(childSchema);
|
|
177
|
+
const format = detectFieldFormat(childSchema);
|
|
169
178
|
|
|
170
179
|
return html`
|
|
171
180
|
<div class="schema-field-card schema-field-card--nested">
|
|
@@ -195,6 +204,12 @@ function nestedFieldCardTpl(parentName, childName, childSchema, isRequired, hand
|
|
|
195
204
|
if (handlers.onChangeNestedType)
|
|
196
205
|
handlers.onChangeNestedType(parentName, childName, newType);
|
|
197
206
|
})}
|
|
207
|
+
${type === "string" || type === "array"
|
|
208
|
+
? formatPickerTpl(format, (f) => {
|
|
209
|
+
if (handlers.onChangeNestedFormat)
|
|
210
|
+
handlers.onChangeNestedFormat(parentName, childName, f);
|
|
211
|
+
})
|
|
212
|
+
: nothing}
|
|
198
213
|
<sp-switch
|
|
199
214
|
size="s"
|
|
200
215
|
?checked=${isRequired}
|
|
@@ -290,10 +305,30 @@ export function typePickerTpl(value, onChange) {
|
|
|
290
305
|
`;
|
|
291
306
|
}
|
|
292
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Render the format picker as an sp-picker dropdown.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} value
|
|
312
|
+
* @param {(format: string) => void} onChange
|
|
313
|
+
* @returns {any}
|
|
314
|
+
*/
|
|
315
|
+
export function formatPickerTpl(value, onChange) {
|
|
316
|
+
return html`
|
|
317
|
+
<sp-picker
|
|
318
|
+
size="s"
|
|
319
|
+
label="Format"
|
|
320
|
+
value=${value}
|
|
321
|
+
@change=${(/** @type {any} */ e) => onChange(e.target.value)}
|
|
322
|
+
>
|
|
323
|
+
${FORMAT_OPTIONS.map((f) => html`<sp-menu-item value=${f}>${f || "(none)"}</sp-menu-item>`)}
|
|
324
|
+
</sp-picker>
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
293
328
|
/**
|
|
294
329
|
* Render the add-field form (inline, not a dialog).
|
|
295
330
|
*
|
|
296
|
-
* @param {{ name: string; type: string; required: boolean }} state
|
|
331
|
+
* @param {{ name: string; type: string; format: string; required: boolean }} state
|
|
297
332
|
* @param {{
|
|
298
333
|
* onInput: (field: string, value: any) => void;
|
|
299
334
|
* onConfirm: () => void;
|
|
@@ -315,6 +350,9 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
315
350
|
}}
|
|
316
351
|
></sp-textfield>
|
|
317
352
|
${typePickerTpl(state.type, (t) => handlers.onInput("type", t))}
|
|
353
|
+
${state.type === "string" || state.type === "array"
|
|
354
|
+
? formatPickerTpl(state.format || "", (f) => handlers.onInput("format", f))
|
|
355
|
+
: nothing}
|
|
318
356
|
<sp-switch
|
|
319
357
|
size="s"
|
|
320
358
|
?checked=${state.required}
|
|
@@ -329,31 +367,28 @@ export function addFieldFormTpl(state, handlers) {
|
|
|
329
367
|
}
|
|
330
368
|
|
|
331
369
|
/**
|
|
332
|
-
* Build a JSON Schema property definition from a type
|
|
370
|
+
* Build a JSON Schema property definition from a type and optional format.
|
|
333
371
|
*
|
|
334
372
|
* @param {string} type
|
|
373
|
+
* @param {string} [format]
|
|
335
374
|
* @returns {object}
|
|
336
375
|
*/
|
|
337
|
-
export function schemaForType(type) {
|
|
376
|
+
export function schemaForType(type, format) {
|
|
338
377
|
switch (type) {
|
|
339
378
|
case "number":
|
|
340
379
|
return { type: "number" };
|
|
341
380
|
case "boolean":
|
|
342
381
|
return { type: "boolean" };
|
|
343
382
|
case "array":
|
|
344
|
-
return
|
|
383
|
+
return format
|
|
384
|
+
? { type: "array", items: { type: "string", format } }
|
|
385
|
+
: { type: "array", items: { type: "string" } };
|
|
345
386
|
case "object":
|
|
346
387
|
return { type: "object", properties: {}, required: [] };
|
|
347
|
-
case "date":
|
|
348
|
-
return { type: "string", format: "date" };
|
|
349
|
-
case "image":
|
|
350
|
-
return { type: "string", format: "image" };
|
|
351
|
-
case "gallery":
|
|
352
|
-
return { type: "array", items: { type: "string", format: "image" } };
|
|
353
388
|
case "reference":
|
|
354
389
|
return { $ref: "#/contentTypes/" };
|
|
355
390
|
default:
|
|
356
|
-
return { type: "string" };
|
|
391
|
+
return format ? { type: "string", format } : { type: "string" };
|
|
357
392
|
}
|
|
358
393
|
}
|
|
359
394
|
|