UI POG
parent
80ce5bda66
commit
9bff98525a
@ -0,0 +1,92 @@
|
||||
import { swel } from "../deps/swel.ts";
|
||||
import { Callback } from "../utils.ts";
|
||||
|
||||
export class TextInputWithValidation {
|
||||
private input = swel("input");
|
||||
private errorText = swel("div", { className: "error-message" }, " ");
|
||||
public el = swel("div", { className: "validated-input" }, [
|
||||
this.input,
|
||||
this.errorText,
|
||||
]);
|
||||
|
||||
set placeholder(s: string) {
|
||||
this.input.placeholder = s;
|
||||
}
|
||||
|
||||
set onInput(cb: Callback<[string]>) {
|
||||
this.input.oninput = () => cb(this.input.value);
|
||||
}
|
||||
|
||||
set error(msg: string | undefined) {
|
||||
if (msg) {
|
||||
this.el.classList.add("error");
|
||||
this.errorText.replaceChildren(msg);
|
||||
} else {
|
||||
this.el.classList.remove("error");
|
||||
this.errorText.replaceChildren(" ");
|
||||
}
|
||||
}
|
||||
|
||||
set value(v: string) {
|
||||
this.input.value = v;
|
||||
}
|
||||
}
|
||||
|
||||
export class LabeledCheckbox {
|
||||
private checkbox = swel("input", { type: "checkbox" });
|
||||
private labelText: ReturnType<typeof document.createTextNode>;
|
||||
private label: HTMLLabelElement;
|
||||
|
||||
constructor(p: { labelText: string; checked?: boolean }) {
|
||||
this.checkbox.checked = p.checked ?? false;
|
||||
this.labelText = document.createTextNode(p.labelText);
|
||||
this.label = swel("label", [this.labelText, this.checkbox]);
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
set checked(c: boolean) {
|
||||
this.checkbox.checked = c;
|
||||
}
|
||||
|
||||
set onChange(cb: Callback<[boolean]>) {
|
||||
this.checkbox.onchange = () => cb(this.checkbox.checked);
|
||||
}
|
||||
}
|
||||
|
||||
export type Choice<V> = {
|
||||
value: V;
|
||||
display: string;
|
||||
};
|
||||
|
||||
export class LabeledChoices<V> {
|
||||
private label: HTMLLabelElement;
|
||||
private select: HTMLSelectElement;
|
||||
private _choices: Choice<V>[] = [];
|
||||
|
||||
constructor(labelText: string) {
|
||||
this.select = swel("select");
|
||||
this.label = swel("label", [labelText, this.select]);
|
||||
}
|
||||
|
||||
set choices(choices: typeof this._choices) {
|
||||
this._choices = choices;
|
||||
this.select.replaceChildren(
|
||||
...this._choices.map((c, i) =>
|
||||
swel("option", { value: `${i}` }, c.display),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
set onChange(cb: Callback<[V]>) {
|
||||
this.select.onchange = () => {
|
||||
cb(this._choices[Number(this.select.value)].value);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { swel } from "../deps/swel.ts";
|
||||
|
||||
export const Loader = (p: Promise<HTMLElement>) => {
|
||||
const el = swel("div", { className: "loader" }, "Loading...");
|
||||
p.then((e) => el.replaceWith(e));
|
||||
|
||||
return el;
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
import { Opts, swel } from "../deps/swel.ts";
|
||||
|
||||
export const WithValidationError = (p: { c: HTMLElement }) => {
|
||||
const errorMsg = swel("div", { className: "error-message" }, "Space");
|
||||
const wrapper = swel("div", { className: "with-validation-error" }, [
|
||||
p.c,
|
||||
errorMsg,
|
||||
]);
|
||||
|
||||
return {
|
||||
el: wrapper,
|
||||
updateError: (e?: string) => {
|
||||
if (e) {
|
||||
errorMsg.innerText = e;
|
||||
wrapper.classList.add("error");
|
||||
} else {
|
||||
wrapper.classList.remove("error");
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const InputWithValidationError = (opts: Opts<"input">) => {
|
||||
const input = swel("input", opts);
|
||||
const withError = WithValidationError({ c: input });
|
||||
|
||||
return {
|
||||
...withError,
|
||||
input,
|
||||
};
|
||||
};
|
||||
|
||||
export const LabeledCheckbox = (p: {
|
||||
labelText: string;
|
||||
labelOpts?: Opts<"label">;
|
||||
inputOpts?: Omit<Opts<"input">, "type">;
|
||||
}) => {
|
||||
const input = swel("input", { ...p.inputOpts, type: "checkbox" });
|
||||
const label = swel("label", p.labelOpts, [input, p.labelText]);
|
||||
|
||||
return { label, input };
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
import { swel } from "../deps/swel.ts";
|
||||
|
||||
export class RecyclerList<View, Model> {
|
||||
private activeViews = 0;
|
||||
private _views: View[] = [];
|
||||
private container: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private createView: () => View,
|
||||
private getViewDOMElement: (view: View) => HTMLElement,
|
||||
private bindModel: (view: View, model: Model, index: number) => void,
|
||||
container?: HTMLElement,
|
||||
) {
|
||||
this.container = container ?? swel("div", { className: "recycler-list" });
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
changed(models: Model[]) {
|
||||
if (this.activeViews < models.length) {
|
||||
let i = 0;
|
||||
for (; i < this.activeViews; ++i) {
|
||||
const view = this._views[i];
|
||||
this.bindModel(view, models[i], i);
|
||||
}
|
||||
|
||||
for (; i < models.length; ++i) {
|
||||
const view = this.createView();
|
||||
this._views.push(view);
|
||||
this.bindModel(view, models[i], i);
|
||||
|
||||
this.container.appendChild(this.getViewDOMElement(view));
|
||||
}
|
||||
} else {
|
||||
let i = 0;
|
||||
for (; i < models.length; ++i) {
|
||||
const view = this._views[i];
|
||||
this.bindModel(view, models[i], i);
|
||||
}
|
||||
|
||||
for (; i < this.activeViews; ++i) {
|
||||
this.container.removeChild(this.container.childNodes.item(i));
|
||||
}
|
||||
}
|
||||
|
||||
this.activeViews = models.length;
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
import {
|
||||
Field,
|
||||
FieldKind,
|
||||
FieldKinds,
|
||||
FieldOptions,
|
||||
MaybePersistedSchema,
|
||||
ValidateSchemaResult,
|
||||
defaultOptionsForFieldKind,
|
||||
validateSchema,
|
||||
} from "../../common/schema.ts";
|
||||
import { swel } from "../deps/swel.ts";
|
||||
import { Callback, Update } from "../utils.ts";
|
||||
import {
|
||||
LabeledChoices,
|
||||
LabeledCheckbox,
|
||||
TextInputWithValidation,
|
||||
} from "./input.ts";
|
||||
import { RecyclerList } from "./recycler_list.ts";
|
||||
|
||||
class StringOptionsEditor {
|
||||
private displayAs = new LabeledChoices<FieldOptions["string"]["displayAs"]>(
|
||||
"Display as",
|
||||
);
|
||||
private container = swel("div", { className: "string-options" }, [
|
||||
this.displayAs.el,
|
||||
]);
|
||||
|
||||
private _updateOptions: Update<FieldOptions["string"]> | null = null;
|
||||
|
||||
constructor() {
|
||||
this.displayAs.choices = [
|
||||
{
|
||||
value: "input",
|
||||
display: "input",
|
||||
},
|
||||
{
|
||||
value: "textarea",
|
||||
display: "textarea",
|
||||
},
|
||||
];
|
||||
|
||||
this.displayAs.onChange = (s) =>
|
||||
this._updateOptions?.((o) => (o.displayAs = s));
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
set updateOptions(u: typeof this._updateOptions) {
|
||||
this._updateOptions = u;
|
||||
}
|
||||
|
||||
optionsChanged(options: FieldOptions["string"]) {}
|
||||
}
|
||||
class NumberOptionsEditor {
|
||||
private container = swel("div", { className: "number-options" });
|
||||
|
||||
private _updateOptions: Update<FieldOptions["string"]> | null = null;
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
set updateOptions(u: typeof this._updateOptions) {
|
||||
this._updateOptions = u;
|
||||
}
|
||||
|
||||
optionsChanged(options: FieldOptions["number"]) {}
|
||||
}
|
||||
|
||||
class FieldOptionsEditor {
|
||||
private fieldKindToView = {
|
||||
string: new StringOptionsEditor(),
|
||||
number: new NumberOptionsEditor(),
|
||||
};
|
||||
|
||||
private container = swel("div", { className: "options-editor" });
|
||||
|
||||
private _updateField: Update<Field> | null = null;
|
||||
set updateField(update: Update<Field>) {
|
||||
this._updateField = update;
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
Object.values(this.fieldKindToView).forEach((v) => {
|
||||
v.updateOptions = (cb) =>
|
||||
this._updateField?.((v) => cb(v.options as any));
|
||||
});
|
||||
}
|
||||
|
||||
fieldChanged(field: Field) {
|
||||
const view = this.fieldKindToView[field.kind];
|
||||
view.optionsChanged(field.options as any);
|
||||
|
||||
this.container.replaceChildren(view.el);
|
||||
}
|
||||
}
|
||||
|
||||
class FieldEditor {
|
||||
private nameInput = new TextInputWithValidation();
|
||||
private kind = new LabeledChoices<FieldKind>("Kind");
|
||||
private required = new LabeledCheckbox({ labelText: "Required" });
|
||||
private optionsEditor = new FieldOptionsEditor();
|
||||
|
||||
private container = swel("div", { className: "field" }, [
|
||||
this.nameInput.el,
|
||||
this.kind.el,
|
||||
this.optionsEditor.el,
|
||||
this.required.el,
|
||||
]);
|
||||
private _updateField: Update<Field> | null = null;
|
||||
set updateField(update: Update<Field>) {
|
||||
this._updateField = update;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.nameInput.placeholder = "Field name";
|
||||
|
||||
this.kind.choices = FieldKinds.map((k) => ({
|
||||
display: k,
|
||||
value: k,
|
||||
}));
|
||||
|
||||
this.nameInput.onInput = (s) => this._updateField?.((f) => (f.name = s));
|
||||
this.required.onChange = (s) =>
|
||||
this._updateField?.((f) => (f.required = s));
|
||||
this.kind.onChange = (k) =>
|
||||
this._updateField?.((f) => {
|
||||
f.kind = k;
|
||||
f.options = defaultOptionsForFieldKind(k);
|
||||
});
|
||||
|
||||
this.optionsEditor.updateField = (cb) => this._updateField?.((f) => cb(f));
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
set checked(c: boolean) {
|
||||
this.required.checked = c;
|
||||
}
|
||||
|
||||
fieldChanged(field: Field, error: string | undefined) {
|
||||
this.nameInput.value = field.name;
|
||||
this.nameInput.error = error;
|
||||
|
||||
this.optionsEditor.fieldChanged(field);
|
||||
}
|
||||
}
|
||||
|
||||
class FieldEditors {
|
||||
private _updateField: ((index: number) => Update<Field>) | null = null;
|
||||
|
||||
private list = new RecyclerList(
|
||||
() => new FieldEditor(),
|
||||
(view) => view.el,
|
||||
(view, model: [Field, string | undefined], index) => {
|
||||
const [field, error] = model;
|
||||
|
||||
view.fieldChanged(field, error);
|
||||
view.updateField = (field) => this._updateField?.(index)(field);
|
||||
},
|
||||
);
|
||||
|
||||
private addFieldButton = swel(
|
||||
"button",
|
||||
{
|
||||
className: "add",
|
||||
},
|
||||
"Add field",
|
||||
);
|
||||
private container = swel("div", { className: "fields" }, [
|
||||
this.list.el,
|
||||
this.addFieldButton,
|
||||
]);
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
set updateField(fn: Exclude<typeof this._updateField, null>) {
|
||||
this._updateField = fn;
|
||||
}
|
||||
|
||||
set onAdd(cb: Callback<[Field]>) {
|
||||
this.addFieldButton.onclick = () =>
|
||||
cb({
|
||||
kind: "string",
|
||||
options: defaultOptionsForFieldKind("string"),
|
||||
name: "",
|
||||
required: false,
|
||||
});
|
||||
}
|
||||
|
||||
fieldsChanged(fields: Field[], errors: ValidateSchemaResult) {
|
||||
this.list.changed(
|
||||
fields.map((f, i) => [f, errors[`fields.${i}.name`]?.[0]]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Editor {
|
||||
private nameInput = new TextInputWithValidation();
|
||||
private fields = new FieldEditors();
|
||||
private container = swel("div", { className: "schema-editor" }, [
|
||||
this.nameInput.el,
|
||||
this.fields.el,
|
||||
]);
|
||||
private _updateSchema: Update<MaybePersistedSchema> | null = null;
|
||||
|
||||
constructor() {
|
||||
this.nameInput.placeholder = "Schema name";
|
||||
|
||||
this.nameInput.onInput = (s) => {
|
||||
this._updateSchema?.((sc) => (sc.name = s));
|
||||
};
|
||||
|
||||
this.fields.onAdd = (f) => {
|
||||
this._updateSchema?.((sc) => sc.fields.push(f));
|
||||
};
|
||||
|
||||
this.fields.updateField = (idx) => (cb) => {
|
||||
this._updateSchema?.((sc) => cb(sc.fields[idx]));
|
||||
};
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
set updateSchema(cb: Update<MaybePersistedSchema>) {
|
||||
this._updateSchema = cb;
|
||||
}
|
||||
|
||||
schemaChanged(schema: MaybePersistedSchema, errors: ValidateSchemaResult) {
|
||||
this.nameInput.value = schema.name;
|
||||
this.nameInput.error = errors.name?.[0];
|
||||
|
||||
this.fields.fieldsChanged(schema.fields, errors);
|
||||
}
|
||||
}
|
||||
|
||||
export class SchemaEditor {
|
||||
private editor = new Editor();
|
||||
|
||||
get el() {
|
||||
return this.editor.el;
|
||||
}
|
||||
|
||||
editSchema(schema: MaybePersistedSchema) {
|
||||
const schemaChanged = () => {
|
||||
const errors = validateSchema(schema);
|
||||
|
||||
this.editor.schemaChanged(schema, errors);
|
||||
};
|
||||
|
||||
const updateSchema = (cb: Callback<[typeof schema]>) => {
|
||||
cb(schema);
|
||||
console.debug({ schema });
|
||||
schemaChanged();
|
||||
};
|
||||
|
||||
this.editor.updateSchema = updateSchema;
|
||||
schemaChanged();
|
||||
}
|
||||
}
|
@ -1,72 +1,50 @@
|
||||
import { makeClient, assert } from "../common/deps/yaypi.ts";
|
||||
import { Api, replacer, reviver } from "../common/api.ts";
|
||||
import { makeClient } from "../../yaypi/mod.ts";
|
||||
import { reviver } from "../common/api.ts";
|
||||
import { replacer } from "../common/api.ts";
|
||||
import { Api } from "../common/api.ts";
|
||||
import { MaybePersistedSchema } from "../common/schema.ts";
|
||||
import { SchemaEditor } from "./components/schema_editor.ts";
|
||||
import { swel } from "./deps/swel.ts";
|
||||
import { SchemaEditorView } from "./schema_editor.ts";
|
||||
|
||||
const client = makeClient(Api, "/api", replacer, reviver);
|
||||
export type Client = typeof client;
|
||||
|
||||
type ShowView = (node: HTMLElement) => void;
|
||||
|
||||
const Home = async (p: { showView: ShowView; client: Client }) => {
|
||||
const schemas = assert(await client.v1.listSchemas(null));
|
||||
|
||||
const container = swel("div", { className: "home" }, [
|
||||
swel("header", "Schemas"),
|
||||
swel(
|
||||
"ul",
|
||||
schemas.map((s) => swel("li", `${s.name}`)),
|
||||
),
|
||||
swel(
|
||||
"button",
|
||||
{
|
||||
on: { click: () => p.showView(SchemaEditorView(p)) },
|
||||
const Home = (showSchemaEditor: () => void) => {
|
||||
const newSchemaButton = swel(
|
||||
"button",
|
||||
{
|
||||
className: "new-schema",
|
||||
on: {
|
||||
click: showSchemaEditor,
|
||||
},
|
||||
"New Schema",
|
||||
),
|
||||
]);
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
let viewIdx = -1;
|
||||
const viewStack: HTMLElement[] = [];
|
||||
},
|
||||
"New schema",
|
||||
);
|
||||
|
||||
self.addEventListener("popstate", (ev) => {
|
||||
const state = ev.state as typeof viewIdx;
|
||||
viewIdx = state;
|
||||
const el = swel("div", { className: "home" }, newSchemaButton);
|
||||
|
||||
const view = viewStack[viewIdx];
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
main.replaceChildren(view);
|
||||
});
|
||||
const App = () => {
|
||||
const showSchemaEditor = () => {
|
||||
editor.editSchema({
|
||||
fields: [],
|
||||
name: "New schema",
|
||||
});
|
||||
el.replaceChildren(editor.el);
|
||||
};
|
||||
const home = Home(showSchemaEditor);
|
||||
const editor = new SchemaEditor();
|
||||
|
||||
const showView = (view: HTMLElement) => {
|
||||
if (viewIdx == -1) {
|
||||
history.replaceState(++viewIdx, "", null);
|
||||
} else {
|
||||
history.pushState(++viewIdx, "", null);
|
||||
}
|
||||
viewStack[viewIdx] = view;
|
||||
const el = swel("div", { className: "app" }, home);
|
||||
|
||||
main.replaceChildren(view);
|
||||
};
|
||||
return el;
|
||||
};
|
||||
|
||||
const chrome = swel("nav", { className: "nav-chrome" }, [
|
||||
swel(
|
||||
"a",
|
||||
{ on: { click: async () => showView(await Home({ showView, client })) } },
|
||||
"Home",
|
||||
),
|
||||
]);
|
||||
const main = swel("div", { className: "main" });
|
||||
const init = () => {
|
||||
const app = App();
|
||||
|
||||
showView(await Home({ showView, client }));
|
||||
document.body.replaceChildren(chrome, main);
|
||||
document.body.replaceChildren(app);
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
@ -1,222 +0,0 @@
|
||||
import { swel } from "./deps/swel.ts";
|
||||
import {
|
||||
type MaybePersistedSchema,
|
||||
validateSchema,
|
||||
FieldShape,
|
||||
} from "../common/schema.ts";
|
||||
import { Client } from "./main.ts";
|
||||
import { Const } from "../common/utils.ts";
|
||||
import { Update } from "./utils.ts";
|
||||
|
||||
const WithValidationError = (p: { c: HTMLElement }) => {
|
||||
const errorMsg = swel("div", { className: "error-message" }, "Space");
|
||||
const wrapper = swel("div", { className: "with-validation-error" }, [
|
||||
p.c,
|
||||
errorMsg,
|
||||
]);
|
||||
|
||||
return {
|
||||
el: wrapper,
|
||||
paint: (e?: string) => {
|
||||
if (e) {
|
||||
errorMsg.innerText = e;
|
||||
wrapper.classList.add("error");
|
||||
} else {
|
||||
wrapper.classList.remove("error");
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const FieldElement = () => {
|
||||
const nameInput = swel("input", {
|
||||
className: "name",
|
||||
});
|
||||
const nameInputWithValidationError = WithValidationError({ c: nameInput });
|
||||
|
||||
const deleteButton = swel("button", { className: "delete" }, "Delete");
|
||||
|
||||
const el = swel("div", { className: "field-element" }, [
|
||||
nameInputWithValidationError.el,
|
||||
deleteButton,
|
||||
]);
|
||||
|
||||
const paint = (p: {
|
||||
field: Const<FieldShape>;
|
||||
errors: Const<{ [K in keyof FieldShape]?: string[] }>;
|
||||
updateField: Update<FieldShape>;
|
||||
deleteField: () => void;
|
||||
}) => {
|
||||
nameInput.value = p.field.name;
|
||||
nameInput.oninput = () => {
|
||||
p.updateField((f) => (f.name = nameInput.value));
|
||||
};
|
||||
deleteButton.onclick = p.deleteField;
|
||||
nameInputWithValidationError.paint(p.errors.name?.[0]);
|
||||
};
|
||||
|
||||
return {
|
||||
el,
|
||||
paint,
|
||||
};
|
||||
};
|
||||
|
||||
const FieldElements = (p: { updateFields: Update<FieldShape[]> }) => {
|
||||
const elements: ReturnType<typeof FieldElement>[] = [];
|
||||
|
||||
const { updateFields } = p;
|
||||
|
||||
let prevLength = 0;
|
||||
const paint = (p: {
|
||||
fields: Const<FieldShape[]>;
|
||||
errors: Const<ReturnType<typeof validateSchema>>;
|
||||
}) => {
|
||||
const lenDiff = p.fields.length - prevLength;
|
||||
|
||||
if (lenDiff > 0) {
|
||||
for (let i = prevLength; i < p.fields.length; ++i) {
|
||||
const fe = FieldElement();
|
||||
elements[i] = fe;
|
||||
el.appendChild(fe.el);
|
||||
}
|
||||
} else if (lenDiff < 0) {
|
||||
for (let i = prevLength - 1; i >= p.fields.length; --i) {
|
||||
el.removeChild(elements[i].el);
|
||||
delete elements[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < p.fields.length; ++i) {
|
||||
const field = p.fields[i];
|
||||
const element = elements[i];
|
||||
const updateField: Update<FieldShape> = (fn) => {
|
||||
updateFields((fs) => {
|
||||
fn?.(fs[i]);
|
||||
});
|
||||
};
|
||||
const deleteField = () => {
|
||||
updateFields((fs) => {
|
||||
fs.splice(i, 1);
|
||||
});
|
||||
};
|
||||
|
||||
element.paint({
|
||||
field,
|
||||
updateField,
|
||||
deleteField,
|
||||
errors: {
|
||||
name: p.errors[`fields.${i}.name`],
|
||||
kind: p.errors[`fields.${i}.kind`],
|
||||
required: p.errors[`fields.${i}.required`],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
prevLength = p.fields.length;
|
||||
};
|
||||
|
||||
const el = swel("div", { className: "field-elements" });
|
||||
|
||||
return {
|
||||
el,
|
||||
paint,
|
||||
};
|
||||
};
|
||||
|
||||
export const SchemaEditor = (p: {
|
||||
updateSchema: Update<MaybePersistedSchema>;
|
||||
}) => {
|
||||
const nameInput = swel("input", {
|
||||
className: "name",
|
||||
on: {
|
||||
input: () => {
|
||||
p.updateSchema((sc) => {
|
||||
sc.name = nameInput.value;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const nameInputWithValidationError = WithValidationError({ c: nameInput });
|
||||
|
||||
const fieldElements = FieldElements({
|
||||
updateFields: (fn) => p.updateSchema((sc) => fn?.(sc.fields)),
|
||||
});
|
||||
const addFieldButton = swel(
|
||||
"button",
|
||||
{
|
||||
className: "add-field",
|
||||
on: {
|
||||
click: () => {
|
||||
p.updateSchema((sc) => {
|
||||
sc.fields.push({
|
||||
kind: "string",
|
||||
name: "",
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
"Add field",
|
||||
);
|
||||
|
||||
const saveButton = swel(
|
||||
"button",
|
||||
{
|
||||
className: "save",
|
||||
on: { click: () => console.log("save clicked") },
|
||||
},
|
||||
"Save",
|
||||
);
|
||||
|
||||
const controls = swel("div", { className: "controls" }, [
|
||||
addFieldButton,
|
||||
saveButton,
|
||||
]);
|
||||
|
||||
const el = swel("div", { className: "schema-editor" }, [
|
||||
nameInputWithValidationError.el,
|
||||
fieldElements.el,
|
||||
controls,
|
||||
]);
|
||||
|
||||
const paint = (p: {
|
||||
schema: Const<MaybePersistedSchema>;
|
||||
errors: Const<ReturnType<typeof validateSchema>>;
|
||||
}) => {
|
||||
nameInput.value = p.schema.name;
|
||||
saveButton.disabled = Object.keys(p.errors).length > 0;
|
||||
nameInputWithValidationError.paint(p.errors.name?.[0]);
|
||||
fieldElements.paint({ fields: p.schema.fields, errors: p.errors });
|
||||
};
|
||||
|
||||
return { paint, el };
|
||||
};
|
||||
|
||||
export const SchemaEditorView = (p: {
|
||||
maybeSchema?: MaybePersistedSchema;
|
||||
client: Client;
|
||||
}) => {
|
||||
const schema =
|
||||
p.maybeSchema ??
|
||||
({
|
||||
fields: [],
|
||||
name: "New Schema",
|
||||
} satisfies MaybePersistedSchema);
|
||||
|
||||
const errors = validateSchema(schema);
|
||||
|
||||
const updateSchema: Update<MaybePersistedSchema> = (fn) => {
|
||||
fn?.(schema);
|
||||
const errors = validateSchema(schema);
|
||||
|
||||
paint({ schema, errors });
|
||||
};
|
||||
|
||||
const { paint, el } = SchemaEditor({
|
||||
updateSchema,
|
||||
});
|
||||
paint({ schema, errors });
|
||||
|
||||
return el;
|
||||
};
|
@ -1 +1,4 @@
|
||||
export type Update<T> = (fn?: (arg: T) => void) => void;
|
||||
import { Const } from "../common/utils.ts";
|
||||
|
||||
export type Callback<T extends unknown[]> = (...args: T) => unknown;
|
||||
export type Update<T> = (cb: (value: T) => void) => void;
|
||||
|
Loading…
Reference in New Issue