@typeonce/effect-machine-devtools 0.24.0 → 0.26.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/NOTICE +4 -0
- package/README.md +13 -13
- package/dist/DevToolsProtocol.d.ts +66 -630
- package/dist/DevToolsProtocol.d.ts.map +1 -1
- package/dist/DevToolsProtocol.js +0 -190
- package/dist/DevToolsProtocol.js.map +1 -1
- package/dist/MachineDocument.d.ts +38 -18
- package/dist/MachineDocument.d.ts.map +1 -1
- package/dist/MachineDocument.js +14 -12
- package/dist/MachineDocument.js.map +1 -1
- package/dist/MachineRegistry.d.ts +22 -2
- package/dist/MachineRegistry.d.ts.map +1 -1
- package/dist/MachineWalkthrough.d.ts +171 -0
- package/dist/MachineWalkthrough.d.ts.map +1 -0
- package/dist/MachineWalkthrough.js +98 -0
- package/dist/MachineWalkthrough.js.map +1 -0
- package/dist/ProjectInspector.d.ts +0 -2
- package/dist/ProjectInspector.d.ts.map +1 -1
- package/dist/ProjectInspector.js.map +1 -1
- package/dist/client/assets/index-BsorPSDS.css +1 -0
- package/dist/client/assets/index-eiptehRd.js +37 -0
- package/dist/client/index.html +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/devServer.d.ts.map +1 -1
- package/dist/internal/devServer.js +4 -64
- package/dist/internal/devServer.js.map +1 -1
- package/dist/internal/evaluationWorker.d.ts.map +1 -1
- package/dist/internal/evaluationWorker.js +7 -263
- package/dist/internal/evaluationWorker.js.map +1 -1
- package/dist/internal/machineDocument.d.ts.map +1 -1
- package/dist/internal/machineDocument.js +3 -1
- package/dist/internal/machineDocument.js.map +1 -1
- package/dist/internal/machineWalkthrough.d.ts +26 -0
- package/dist/internal/machineWalkthrough.d.ts.map +1 -0
- package/dist/internal/machineWalkthrough.js +256 -0
- package/dist/internal/machineWalkthrough.js.map +1 -0
- package/dist/internal/projectInspector.d.ts.map +1 -1
- package/dist/internal/projectInspector.js +1 -11
- package/dist/internal/projectInspector.js.map +1 -1
- package/index.html +2 -2
- package/package.json +8 -7
- package/src/DevToolsProtocol.ts +0 -285
- package/src/MachineDocument.ts +21 -19
- package/src/MachineWalkthrough.ts +199 -0
- package/src/ProjectInspector.ts +0 -5
- package/src/index.ts +2 -2
- package/src/internal/browser/chart-layout-policy.ts +206 -0
- package/src/internal/browser/chart-layout.ts +686 -0
- package/src/internal/browser/chart-model.ts +244 -0
- package/src/internal/browser/chart-renderer.ts +774 -0
- package/src/internal/browser/example-machine.ts +15 -6
- package/src/internal/browser/input-form.ts +1 -418
- package/src/internal/browser/invoke-outcomes-example.ts +231 -0
- package/src/internal/browser/parallel-completion-example.ts +199 -0
- package/src/internal/browser/planner-example.ts +2 -0
- package/src/internal/browser/protocol-events-example.ts +200 -0
- package/src/internal/browser/styles.css +974 -342
- package/src/internal/browser/transition-semantics-example.ts +245 -0
- package/src/internal/browser/visualizer-analysis.ts +52 -0
- package/src/internal/browser/visualizer-app.ts +874 -706
- package/src/internal/browser/visualizer-model.ts +104 -0
- package/src/internal/devServer.ts +4 -82
- package/src/internal/evaluationWorker.ts +7 -371
- package/src/internal/machineDocument.ts +3 -1
- package/src/internal/machineWalkthrough.ts +355 -0
- package/src/internal/projectInspector.ts +3 -31
- package/dist/MachineSimulator.d.ts +0 -169
- package/dist/MachineSimulator.d.ts.map +0 -1
- package/dist/MachineSimulator.js +0 -98
- package/dist/MachineSimulator.js.map +0 -1
- package/dist/client/assets/index-B49Tjawc.js +0 -14
- package/dist/client/assets/index-BKH0cOG2.css +0 -1
- package/dist/internal/machineSimulator.d.ts +0 -5
- package/dist/internal/machineSimulator.d.ts.map +0 -1
- package/dist/internal/machineSimulator.js +0 -156
- package/dist/internal/machineSimulator.js.map +0 -1
- package/src/MachineSimulator.ts +0 -154
- package/src/internal/browser/simulation-client.ts +0 -20
- package/src/internal/machineSimulator.ts +0 -199
|
@@ -3,8 +3,14 @@ import { Schema } from "effect"
|
|
|
3
3
|
|
|
4
4
|
// Shared by the live browser UI and its project-inspection fixture.
|
|
5
5
|
|
|
6
|
-
class Application extends Schema.TaggedClass<Application>("Application")("Application", {
|
|
7
|
-
|
|
6
|
+
class Application extends Schema.TaggedClass<Application>("Application")("Application", {
|
|
7
|
+
workspace: Schema.String,
|
|
8
|
+
revision: Schema.Number
|
|
9
|
+
}) {}
|
|
10
|
+
class Workflow extends Schema.TaggedClass<Workflow>("Workflow")("Workflow", {
|
|
11
|
+
document: Schema.String,
|
|
12
|
+
unsavedChanges: Schema.Number
|
|
13
|
+
}) {}
|
|
8
14
|
class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
|
|
9
15
|
class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {}
|
|
10
16
|
class Editing extends Schema.TaggedClass<Editing>("Editing")("Editing", {}) {}
|
|
@@ -59,11 +65,11 @@ const States = Machine.states({
|
|
|
59
65
|
|
|
60
66
|
export const snapshot = {
|
|
61
67
|
path: "application" as const,
|
|
62
|
-
value: new Application({}),
|
|
68
|
+
value: new Application({ workspace: "effect-machine", revision: 7 }),
|
|
63
69
|
states: {
|
|
64
70
|
workflow: {
|
|
65
71
|
path: "application.workflow" as const,
|
|
66
|
-
value: new Workflow({}),
|
|
72
|
+
value: new Workflow({ document: "Machine.ts", unsavedChanges: 2 }),
|
|
67
73
|
state: { path: "application.workflow.idle" as const, value: new Idle({}) }
|
|
68
74
|
},
|
|
69
75
|
connection: {
|
|
@@ -103,9 +109,12 @@ export const machine = Machine.make({
|
|
|
103
109
|
target.decoded(
|
|
104
110
|
new Running({}),
|
|
105
111
|
(running) => running.editing.decoded(new Editing({}))
|
|
106
|
-
).update(owner.decoded(new Workflow({})))
|
|
112
|
+
).update(owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 3 })))
|
|
107
113
|
),
|
|
108
|
-
Refresh: (to) =>
|
|
114
|
+
Refresh: (to) =>
|
|
115
|
+
to.local.update(({ owner }) =>
|
|
116
|
+
owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
|
|
117
|
+
)
|
|
109
118
|
}
|
|
110
119
|
},
|
|
111
120
|
running: {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { InputIssue } from "../../DevToolsProtocol.js"
|
|
2
1
|
import type { InputSchema } from "../../MachineDocument.js"
|
|
3
2
|
|
|
4
3
|
type JsonPrimitive = string | number | boolean | null
|
|
@@ -217,9 +216,7 @@ const project = (
|
|
|
217
216
|
case "null":
|
|
218
217
|
return { _tag: "Literal", ...common, value: null }
|
|
219
218
|
case "object": {
|
|
220
|
-
if (!isRecord(resolved.properties)) {
|
|
221
|
-
return { _tag: "Object", ...common, fields: [] }
|
|
222
|
-
}
|
|
219
|
+
if (!isRecord(resolved.properties)) return { _tag: "Object", ...common, fields: [] }
|
|
223
220
|
const required = new Set(
|
|
224
221
|
Array.isArray(resolved.required)
|
|
225
222
|
? resolved.required.filter((item): item is string => typeof item === "string")
|
|
@@ -253,417 +250,3 @@ const project = (
|
|
|
253
250
|
}
|
|
254
251
|
|
|
255
252
|
export const projectInputSchema = (document: InputSchema): InputField => project(document.schema, document.definitions)
|
|
256
|
-
|
|
257
|
-
export interface InputFormResult {
|
|
258
|
-
readonly ok: boolean
|
|
259
|
-
readonly value?: unknown
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export interface InputForm {
|
|
263
|
-
readonly element: HTMLFormElement
|
|
264
|
-
readonly hasFields: boolean
|
|
265
|
-
readonly supported: boolean
|
|
266
|
-
readonly read: () => InputFormResult
|
|
267
|
-
readonly clearIssues: () => void
|
|
268
|
-
readonly setIssues: (issues: ReadonlyArray<InputIssue>) => void
|
|
269
|
-
readonly setPending: (pending: boolean) => void
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
interface Control {
|
|
273
|
-
readonly element: HTMLElement
|
|
274
|
-
readonly interactive: boolean
|
|
275
|
-
readonly supported: boolean
|
|
276
|
-
readonly read: () => unknown
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
interface RenderContext {
|
|
280
|
-
readonly issueTargets: Map<string, HTMLElement>
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let nextControlId = 0
|
|
284
|
-
|
|
285
|
-
const element = <Tag extends keyof HTMLElementTagNameMap>(
|
|
286
|
-
tag: Tag,
|
|
287
|
-
className?: string,
|
|
288
|
-
text?: string
|
|
289
|
-
): HTMLElementTagNameMap[Tag] => {
|
|
290
|
-
const node = document.createElement(tag)
|
|
291
|
-
if (className !== undefined) node.className = className
|
|
292
|
-
if (text !== undefined) node.textContent = text
|
|
293
|
-
return node
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const valueKey = (value: JsonPrimitive): string => JSON.stringify(value)
|
|
297
|
-
|
|
298
|
-
const labelText = (field: InputField, fallback: string): string => field.title ?? fallback
|
|
299
|
-
|
|
300
|
-
const description = (field: InputField): HTMLElement | undefined =>
|
|
301
|
-
field.description === undefined ? undefined : element("p", "input-description", field.description)
|
|
302
|
-
|
|
303
|
-
const fieldType = (field: InputField): string => {
|
|
304
|
-
switch (field._tag) {
|
|
305
|
-
case "String":
|
|
306
|
-
return field.format ?? "string"
|
|
307
|
-
case "Number":
|
|
308
|
-
return field.integer ? "integer" : "number"
|
|
309
|
-
case "Boolean":
|
|
310
|
-
return "boolean"
|
|
311
|
-
case "Enum":
|
|
312
|
-
return "enum"
|
|
313
|
-
case "Literal":
|
|
314
|
-
return "literal"
|
|
315
|
-
case "Object":
|
|
316
|
-
return "object"
|
|
317
|
-
case "Array":
|
|
318
|
-
return `${fieldType(field.item)}[]`
|
|
319
|
-
case "Union":
|
|
320
|
-
return "union"
|
|
321
|
-
case "Unsupported":
|
|
322
|
-
return "unsupported"
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const fieldConstraints = (field: InputField): ReadonlyArray<string> => {
|
|
327
|
-
switch (field._tag) {
|
|
328
|
-
case "String":
|
|
329
|
-
return [
|
|
330
|
-
field.minLength === undefined ? undefined : `min ${field.minLength} characters`,
|
|
331
|
-
field.maxLength === undefined ? undefined : `max ${field.maxLength} characters`,
|
|
332
|
-
field.pattern === undefined ? undefined : `pattern ${field.pattern}`,
|
|
333
|
-
field.defaultValue === undefined ? undefined : `default ${field.defaultValue}`
|
|
334
|
-
].filter((value): value is string => value !== undefined)
|
|
335
|
-
case "Number":
|
|
336
|
-
return [
|
|
337
|
-
field.minimum === undefined ? undefined : `min ${field.minimum}`,
|
|
338
|
-
field.maximum === undefined ? undefined : `max ${field.maximum}`,
|
|
339
|
-
field.defaultValue === undefined ? undefined : `default ${field.defaultValue}`
|
|
340
|
-
].filter((value): value is string => value !== undefined)
|
|
341
|
-
case "Enum":
|
|
342
|
-
return [
|
|
343
|
-
`${field.values.length} options`,
|
|
344
|
-
field.defaultValue === undefined ? undefined : `default ${String(field.defaultValue)}`
|
|
345
|
-
].filter((value): value is string => value !== undefined)
|
|
346
|
-
case "Literal":
|
|
347
|
-
return [`fixed ${String(field.value)}`]
|
|
348
|
-
case "Object":
|
|
349
|
-
return [`${field.fields.length} fields`]
|
|
350
|
-
case "Array":
|
|
351
|
-
return [
|
|
352
|
-
field.minItems === 0 ? undefined : `min ${field.minItems} items`,
|
|
353
|
-
field.maxItems === undefined ? undefined : `max ${field.maxItems} items`
|
|
354
|
-
].filter((value): value is string => value !== undefined)
|
|
355
|
-
case "Union":
|
|
356
|
-
return [`${field.alternatives.length} variants`]
|
|
357
|
-
case "Boolean":
|
|
358
|
-
case "Unsupported":
|
|
359
|
-
return []
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const renderControl = (
|
|
364
|
-
field: InputField,
|
|
365
|
-
name: string,
|
|
366
|
-
context: RenderContext,
|
|
367
|
-
path: ReadonlyArray<string | number>
|
|
368
|
-
): Control => {
|
|
369
|
-
switch (field._tag) {
|
|
370
|
-
case "String": {
|
|
371
|
-
const input = element("input", "input-control")
|
|
372
|
-
input.type = field.format === "date-time"
|
|
373
|
-
? "datetime-local"
|
|
374
|
-
: field.format === "date"
|
|
375
|
-
? "date"
|
|
376
|
-
: field.format === "email"
|
|
377
|
-
? "email"
|
|
378
|
-
: field.format === "uri"
|
|
379
|
-
? "url"
|
|
380
|
-
: "text"
|
|
381
|
-
input.name = name
|
|
382
|
-
input.value = field.defaultValue ?? ""
|
|
383
|
-
if (field.minLength !== undefined) input.minLength = field.minLength
|
|
384
|
-
if (field.maxLength !== undefined) input.maxLength = field.maxLength
|
|
385
|
-
if (field.pattern !== undefined) input.pattern = field.pattern
|
|
386
|
-
return { element: input, interactive: true, supported: true, read: () => input.value }
|
|
387
|
-
}
|
|
388
|
-
case "Number": {
|
|
389
|
-
const input = element("input", "input-control")
|
|
390
|
-
input.type = "number"
|
|
391
|
-
input.name = name
|
|
392
|
-
input.step = field.integer ? "1" : "any"
|
|
393
|
-
if (field.defaultValue !== undefined) input.value = String(field.defaultValue)
|
|
394
|
-
if (field.minimum !== undefined) input.min = String(field.minimum)
|
|
395
|
-
if (field.maximum !== undefined) input.max = String(field.maximum)
|
|
396
|
-
return {
|
|
397
|
-
element: input,
|
|
398
|
-
interactive: true,
|
|
399
|
-
supported: true,
|
|
400
|
-
read: () => input.value === "" ? undefined : Number(input.value)
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
case "Boolean": {
|
|
404
|
-
const wrapper = element("label", "boolean-control")
|
|
405
|
-
const input = element("input")
|
|
406
|
-
input.type = "checkbox"
|
|
407
|
-
input.name = name
|
|
408
|
-
input.checked = field.defaultValue ?? false
|
|
409
|
-
wrapper.append(input, element("span", undefined, "Enabled"))
|
|
410
|
-
return { element: wrapper, interactive: true, supported: true, read: () => input.checked }
|
|
411
|
-
}
|
|
412
|
-
case "Enum": {
|
|
413
|
-
const select = element("select", "input-control")
|
|
414
|
-
select.name = name
|
|
415
|
-
field.values.forEach((value) => {
|
|
416
|
-
const option = element("option", undefined, String(value))
|
|
417
|
-
option.value = valueKey(value)
|
|
418
|
-
option.selected = Object.is(value, field.defaultValue)
|
|
419
|
-
select.append(option)
|
|
420
|
-
})
|
|
421
|
-
return {
|
|
422
|
-
element: select,
|
|
423
|
-
interactive: true,
|
|
424
|
-
supported: true,
|
|
425
|
-
read: () => JSON.parse(select.value) as JsonPrimitive
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
case "Literal": {
|
|
429
|
-
const output = element("output", "literal-control", String(field.value))
|
|
430
|
-
return { element: output, interactive: false, supported: true, read: () => field.value }
|
|
431
|
-
}
|
|
432
|
-
case "Object": {
|
|
433
|
-
const group = element("fieldset", "input-object")
|
|
434
|
-
const legend = element("legend", undefined, labelText(field, name))
|
|
435
|
-
group.append(legend)
|
|
436
|
-
const controls: Array<{
|
|
437
|
-
readonly key: string
|
|
438
|
-
readonly included: HTMLInputElement | undefined
|
|
439
|
-
readonly control: Control
|
|
440
|
-
}> = []
|
|
441
|
-
for (const property of field.fields) {
|
|
442
|
-
const row = element("div", "input-field")
|
|
443
|
-
const heading = element("div", "input-field-heading")
|
|
444
|
-
const label = element("label", "input-label", labelText(property.field, property.key))
|
|
445
|
-
const identity = element("div", "input-field-identity")
|
|
446
|
-
identity.append(label, element("span", "input-field-type", fieldType(property.field)))
|
|
447
|
-
const required = property.required ? element("span", "input-required", "required") : undefined
|
|
448
|
-
let included: HTMLInputElement | undefined
|
|
449
|
-
const propertyPath = [...path, property.key]
|
|
450
|
-
const control = renderControl(property.field, `${name}.${property.key}`, context, propertyPath)
|
|
451
|
-
const labelled = control.element instanceof HTMLInputElement || control.element instanceof HTMLSelectElement
|
|
452
|
-
? control.element
|
|
453
|
-
: control.element.querySelector<HTMLInputElement | HTMLSelectElement>(":scope > input, :scope > select")
|
|
454
|
-
if (labelled !== null) {
|
|
455
|
-
const id = `machine-input-${nextControlId++}`
|
|
456
|
-
labelled.id = id
|
|
457
|
-
label.htmlFor = id
|
|
458
|
-
}
|
|
459
|
-
if (property.required) {
|
|
460
|
-
if (control.element instanceof HTMLSelectElement) {
|
|
461
|
-
control.element.required = true
|
|
462
|
-
} else if (control.element instanceof HTMLInputElement) {
|
|
463
|
-
control.element.required = property.field._tag === "Number" ||
|
|
464
|
-
(property.field._tag === "String" &&
|
|
465
|
-
(property.field.format !== undefined || (property.field.minLength ?? 0) > 0))
|
|
466
|
-
}
|
|
467
|
-
heading.append(identity, required!)
|
|
468
|
-
} else {
|
|
469
|
-
const optional = element("label", "input-optional")
|
|
470
|
-
included = element("input")
|
|
471
|
-
included.type = "checkbox"
|
|
472
|
-
optional.append(included, element("span", undefined, "include"))
|
|
473
|
-
heading.append(identity, optional)
|
|
474
|
-
control.element.toggleAttribute("inert", true)
|
|
475
|
-
control.element.classList.add("is-disabled")
|
|
476
|
-
control.element.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
|
|
477
|
-
"input, select, button"
|
|
478
|
-
).forEach((item) => item.disabled = true)
|
|
479
|
-
if (
|
|
480
|
-
control.element instanceof HTMLInputElement ||
|
|
481
|
-
control.element instanceof HTMLSelectElement ||
|
|
482
|
-
control.element instanceof HTMLButtonElement
|
|
483
|
-
) control.element.disabled = true
|
|
484
|
-
included.addEventListener("change", () => {
|
|
485
|
-
control.element.toggleAttribute("inert", !included!.checked)
|
|
486
|
-
control.element.classList.toggle("is-disabled", !included!.checked)
|
|
487
|
-
control.element.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
|
|
488
|
-
"input, select, button"
|
|
489
|
-
).forEach((item) => item.disabled = !included!.checked)
|
|
490
|
-
if (
|
|
491
|
-
control.element instanceof HTMLInputElement ||
|
|
492
|
-
control.element instanceof HTMLSelectElement ||
|
|
493
|
-
control.element instanceof HTMLButtonElement
|
|
494
|
-
) control.element.disabled = !included!.checked
|
|
495
|
-
})
|
|
496
|
-
}
|
|
497
|
-
const issues = element("div", "input-errors")
|
|
498
|
-
issues.hidden = true
|
|
499
|
-
context.issueTargets.set(JSON.stringify(propertyPath), issues)
|
|
500
|
-
row.append(heading, control.element)
|
|
501
|
-
const constraints = fieldConstraints(property.field)
|
|
502
|
-
if (constraints.length > 0) {
|
|
503
|
-
const metadata = element("div", "input-constraints")
|
|
504
|
-
constraints.forEach((constraint) => metadata.append(element("span", undefined, constraint)))
|
|
505
|
-
row.append(metadata)
|
|
506
|
-
}
|
|
507
|
-
const details = description(property.field)
|
|
508
|
-
if (details !== undefined) row.append(details)
|
|
509
|
-
row.append(issues)
|
|
510
|
-
group.append(row)
|
|
511
|
-
controls.push({ key: property.key, included, control })
|
|
512
|
-
}
|
|
513
|
-
return {
|
|
514
|
-
element: group,
|
|
515
|
-
interactive: controls.some(({ control }) => control.interactive),
|
|
516
|
-
supported: controls.every(({ control }) => control.supported),
|
|
517
|
-
read: () =>
|
|
518
|
-
Object.fromEntries(
|
|
519
|
-
controls
|
|
520
|
-
.filter(({ included }) => included === undefined || included.checked)
|
|
521
|
-
.map(({ key, control }) => [key, control.read()])
|
|
522
|
-
.filter((entry) => entry[1] !== undefined)
|
|
523
|
-
)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
case "Array": {
|
|
527
|
-
const group = element("fieldset", "input-array")
|
|
528
|
-
group.append(element("legend", undefined, labelText(field, name)))
|
|
529
|
-
const items = element("div", "input-array-items")
|
|
530
|
-
const controls: Array<{
|
|
531
|
-
readonly row: HTMLElement
|
|
532
|
-
readonly control: Control
|
|
533
|
-
readonly remove: HTMLButtonElement
|
|
534
|
-
}> = []
|
|
535
|
-
const add = element("button", "input-array-add", "Add item")
|
|
536
|
-
add.type = "button"
|
|
537
|
-
const refreshActions = (): void => {
|
|
538
|
-
add.disabled = field.maxItems !== undefined && controls.length >= field.maxItems
|
|
539
|
-
controls.forEach(({ remove }) => remove.disabled = controls.length <= field.minItems)
|
|
540
|
-
}
|
|
541
|
-
const addItem = (): void => {
|
|
542
|
-
if (field.maxItems !== undefined && controls.length >= field.maxItems) return
|
|
543
|
-
const row = element("div", "input-array-item")
|
|
544
|
-
const control = renderControl(field.item, `${name}.${controls.length}`, context, [...path, controls.length])
|
|
545
|
-
const remove = element("button", "input-array-remove", "Remove")
|
|
546
|
-
remove.type = "button"
|
|
547
|
-
remove.addEventListener("click", () => {
|
|
548
|
-
row.remove()
|
|
549
|
-
const index = controls.findIndex((item) => item.row === row)
|
|
550
|
-
if (index >= 0) controls.splice(index, 1)
|
|
551
|
-
refreshActions()
|
|
552
|
-
})
|
|
553
|
-
row.append(control.element, remove)
|
|
554
|
-
items.append(row)
|
|
555
|
-
controls.push({ row, control, remove })
|
|
556
|
-
refreshActions()
|
|
557
|
-
}
|
|
558
|
-
for (let index = 0; index < field.minItems; index++) addItem()
|
|
559
|
-
add.addEventListener("click", addItem)
|
|
560
|
-
group.append(items, add)
|
|
561
|
-
return {
|
|
562
|
-
element: group,
|
|
563
|
-
interactive: true,
|
|
564
|
-
supported: controls.every(({ control }) => control.supported) && field.item._tag !== "Unsupported",
|
|
565
|
-
read: () => controls.map(({ control }) => control.read())
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
case "Union": {
|
|
569
|
-
const group = element("fieldset", "input-union")
|
|
570
|
-
group.append(element("legend", undefined, labelText(field, name)))
|
|
571
|
-
const select = element("select", "input-control")
|
|
572
|
-
const body = element("div", "input-union-body")
|
|
573
|
-
let selected = 0
|
|
574
|
-
const controls = field.alternatives.map((alternative, index) => {
|
|
575
|
-
const control = renderControl(alternative, `${name}.${index}`, context, path)
|
|
576
|
-
const option = element("option", undefined, labelText(alternative, `Option ${index + 1}`))
|
|
577
|
-
option.value = String(index)
|
|
578
|
-
select.append(option)
|
|
579
|
-
return control
|
|
580
|
-
})
|
|
581
|
-
const update = (): void => {
|
|
582
|
-
selected = Number(select.value)
|
|
583
|
-
body.replaceChildren(controls[selected]?.element ?? element("div"))
|
|
584
|
-
}
|
|
585
|
-
select.addEventListener("change", update)
|
|
586
|
-
group.append(select, body)
|
|
587
|
-
update()
|
|
588
|
-
return {
|
|
589
|
-
element: group,
|
|
590
|
-
interactive: true,
|
|
591
|
-
supported: controls.every(({ supported }) => supported),
|
|
592
|
-
read: () => controls[selected]?.read()
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
case "Unsupported": {
|
|
596
|
-
const message = element("p", "input-unsupported", field.reason)
|
|
597
|
-
return { element: message, interactive: false, supported: false, read: () => undefined }
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
export const renderInputForm = (
|
|
603
|
-
schema: InputSchema,
|
|
604
|
-
options: {
|
|
605
|
-
readonly name: string
|
|
606
|
-
readonly fixed?: Readonly<Record<string, JsonPrimitive>>
|
|
607
|
-
readonly omit?: ReadonlyArray<string>
|
|
608
|
-
}
|
|
609
|
-
): InputForm => {
|
|
610
|
-
const form = element("form", "schema-form")
|
|
611
|
-
const formIssues = element("div", "input-errors input-errors-form")
|
|
612
|
-
formIssues.hidden = true
|
|
613
|
-
const context: RenderContext = { issueTargets: new Map() }
|
|
614
|
-
const projected = projectInputSchema(schema)
|
|
615
|
-
const visible = projected._tag === "Object" && options.omit !== undefined
|
|
616
|
-
? { ...projected, fields: projected.fields.filter(({ key }) => !options.omit!.includes(key)) }
|
|
617
|
-
: projected
|
|
618
|
-
const control = renderControl(visible, options.name, context, [])
|
|
619
|
-
form.append(formIssues, control.element)
|
|
620
|
-
const clearIssues = (): void => {
|
|
621
|
-
formIssues.replaceChildren()
|
|
622
|
-
formIssues.hidden = true
|
|
623
|
-
context.issueTargets.forEach((target) => {
|
|
624
|
-
target.replaceChildren()
|
|
625
|
-
target.hidden = true
|
|
626
|
-
})
|
|
627
|
-
}
|
|
628
|
-
const setIssues = (issues: ReadonlyArray<InputIssue>): void => {
|
|
629
|
-
clearIssues()
|
|
630
|
-
const grouped = new Map<HTMLElement, Array<string>>()
|
|
631
|
-
for (const issue of issues) {
|
|
632
|
-
let target: HTMLElement = formIssues
|
|
633
|
-
for (let length = issue.path.length; length > 0; length--) {
|
|
634
|
-
const candidate = context.issueTargets.get(JSON.stringify(issue.path.slice(0, length)))
|
|
635
|
-
if (candidate !== undefined) {
|
|
636
|
-
target = candidate
|
|
637
|
-
break
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
const messages = grouped.get(target) ?? []
|
|
641
|
-
if (!messages.includes(issue.message)) messages.push(issue.message)
|
|
642
|
-
grouped.set(target, messages)
|
|
643
|
-
}
|
|
644
|
-
grouped.forEach((messages, target) => {
|
|
645
|
-
messages.forEach((message) => target.append(element("div", undefined, message)))
|
|
646
|
-
target.hidden = false
|
|
647
|
-
})
|
|
648
|
-
}
|
|
649
|
-
return {
|
|
650
|
-
element: form,
|
|
651
|
-
hasFields: control.interactive,
|
|
652
|
-
supported: control.supported,
|
|
653
|
-
read: () => {
|
|
654
|
-
clearIssues()
|
|
655
|
-
if (!form.reportValidity() || !control.supported) return { ok: false }
|
|
656
|
-
const value = control.read()
|
|
657
|
-
return {
|
|
658
|
-
ok: true,
|
|
659
|
-
value: isRecord(value) && options.fixed !== undefined ? { ...value, ...options.fixed } : value
|
|
660
|
-
}
|
|
661
|
-
},
|
|
662
|
-
clearIssues,
|
|
663
|
-
setIssues,
|
|
664
|
-
setPending: (pending) => {
|
|
665
|
-
form.toggleAttribute("inert", pending)
|
|
666
|
-
form.setAttribute("aria-busy", String(pending))
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|