Preactify
parent
8079a1bd79
commit
4cd9a47dc3
@ -1,23 +1,24 @@
|
||||
import { Const } from "../utils.ts";
|
||||
|
||||
export const setIdx = <T>(
|
||||
arr: Const<T[]>,
|
||||
index: number,
|
||||
newValue: Const<T>,
|
||||
): Const<T[]> => {
|
||||
const newArr = [...arr] as Const<T>[];
|
||||
export const setIdx = <T>(arr: T[], index: number, newValue: T): T[] => {
|
||||
const newArr = [...arr];
|
||||
newArr[index] = newValue;
|
||||
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export const push = <T>(arr: Const<T[]>, newValue: T) => [...arr, newValue];
|
||||
export const push = <T>(arr: T[], newValue: T) => [...arr, newValue];
|
||||
|
||||
export const deleteIdx = <T>(arr: T[], index: number): T[] => {
|
||||
const newArr = [...arr];
|
||||
newArr.splice(index, 1);
|
||||
|
||||
return newArr;
|
||||
};
|
||||
|
||||
export const setProp = <T, K extends keyof T>(
|
||||
obj: Const<T>,
|
||||
obj: T,
|
||||
key: K,
|
||||
newValue: Const<T[K]>,
|
||||
): Const<T> => ({
|
||||
newValue: T[K],
|
||||
): T => ({
|
||||
...obj,
|
||||
[key]: newValue,
|
||||
});
|
||||
|
@ -1,2 +1,9 @@
|
||||
{
|
||||
"imports": {
|
||||
"frontend": "./frontend",
|
||||
"swel": "../swel/mod.ts",
|
||||
"preact": "https://esm.sh/preact@10.13.2",
|
||||
"preact/debug": "https://esm.sh/preact@10.13.2/debug",
|
||||
"preact/hooks": "https://esm.sh/preact@10.13.2/hooks"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
import { ComponentChild, Fragment, VNode, createContext, h } from "preact";
|
||||
import { useContext, useEffect, useMemo, useReducer } from "preact/hooks";
|
||||
import { Home } from "../Home/Home.ts";
|
||||
import { Action, State, ViewState, ViewStateKind, reduce } from "./state.ts";
|
||||
import { Dispatch } from "../../utils.ts";
|
||||
import { saveStateToLocalStorage } from "../../state/store.ts";
|
||||
import { Const } from "../../../common/utils.ts";
|
||||
import { SchemaEditor } from "../SchemaEditor/SchemaEditor.ts";
|
||||
import { goHome } from "./state.ts";
|
||||
import { MakeClient, makeClient } from "../../../common/deps/yaypi.ts";
|
||||
import { Api } from "../../../common/api.ts";
|
||||
|
||||
type ViewStates = {
|
||||
[K in ViewState["kind"]]: ViewState & { kind: K };
|
||||
};
|
||||
|
||||
type ViewRegistry_ = {
|
||||
// @ts-ignore: weird
|
||||
[K in keyof ViewStates]: ViewStates[K]["state"];
|
||||
};
|
||||
|
||||
type ViewRegistry = {
|
||||
[K in keyof ViewRegistry_]: (p: {
|
||||
state: ViewRegistry_[K];
|
||||
dispatch: Dispatch<Action>;
|
||||
client: Client;
|
||||
}) => VNode;
|
||||
};
|
||||
|
||||
const VIEW_REGISTRY: ViewRegistry = {
|
||||
Home: (p) => h(Home, p),
|
||||
SchemaEditor: (p) => h(SchemaEditor, p),
|
||||
};
|
||||
|
||||
const AppView = (
|
||||
p: ViewState & { dispatch: Dispatch<Action>; client: Client },
|
||||
) => {
|
||||
const View = VIEW_REGISTRY[p.kind];
|
||||
|
||||
return h(View as any, p);
|
||||
};
|
||||
|
||||
const VIEW_NAMES: Record<ViewStateKind, string> = {
|
||||
Home: "Home",
|
||||
SchemaEditor: "Schema Editor",
|
||||
};
|
||||
|
||||
const Layout = (p: {
|
||||
state: State;
|
||||
dispatch: Dispatch<Action>;
|
||||
client: Client;
|
||||
}) => {
|
||||
const view = h(AppView, {
|
||||
...p.state.view,
|
||||
dispatch: p.dispatch,
|
||||
client: p.client,
|
||||
});
|
||||
|
||||
const name = VIEW_NAMES[p.state.view.kind];
|
||||
|
||||
const onClickHome = () => {
|
||||
p.dispatch(goHome());
|
||||
};
|
||||
|
||||
return h(
|
||||
Fragment,
|
||||
{},
|
||||
h("nav", {}, h("a", { onClick: onClickHome }, "Zchema"), h("a", {}, name)),
|
||||
h("main", {}, view),
|
||||
);
|
||||
};
|
||||
|
||||
export const App = (p: State) => {
|
||||
const [appState, dispatch] = useReducer(reduce, p);
|
||||
const client = useMemo(() => makeClient(Api, "/api"), []);
|
||||
|
||||
useEffect(() => {
|
||||
saveStateToLocalStorage(appState as Const<State>);
|
||||
}, [appState]);
|
||||
|
||||
return h(Layout, { state: appState, dispatch, client });
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { unreachable } from "../../../common/lib/dev.ts";
|
||||
import { Reduce } from "../../state/lib/store.ts";
|
||||
import { Action as HomeAction, reduce as reduceHome } from "../Home/actions.ts";
|
||||
import {
|
||||
Action as SchemaEditorAction,
|
||||
reduce as reduceSchemaEditor,
|
||||
} from "../SchemaEditor/actions.ts";
|
||||
import { AppState } from "./view.ts";
|
||||
|
||||
export type Action = HomeAction | SchemaEditorAction;
|
||||
|
||||
export const reduce: Reduce<AppState, Action> = (state, action) => {
|
||||
if (state.view.kind == "Home") {
|
||||
return {
|
||||
view: reduceHome(state.view.state, action as HomeAction),
|
||||
};
|
||||
}
|
||||
|
||||
if (state.view.kind == "SchemaEditor") {
|
||||
return {
|
||||
...state,
|
||||
view: {
|
||||
...state.view,
|
||||
state: reduceSchemaEditor(
|
||||
state.view.state,
|
||||
action as SchemaEditorAction,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return unreachable();
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { Reducer } from "preact/hooks";
|
||||
import {
|
||||
Action as HomeAction,
|
||||
State as HomeState,
|
||||
reduce as reduceHome,
|
||||
} from "../Home/state.ts";
|
||||
import {
|
||||
Action as SchemaEditorAction,
|
||||
State as SchemaEditorState,
|
||||
reduce as reduceSchemaEditor,
|
||||
} from "../SchemaEditor/state.ts";
|
||||
import { ReturnTypes } from "../../../common/utils.ts";
|
||||
|
||||
export const goHome = () => ({ kind: "goHome" });
|
||||
export type Action =
|
||||
| HomeAction
|
||||
| SchemaEditorAction
|
||||
| ReturnTypes<[typeof goHome]>;
|
||||
type ViewState_<Kind extends string, T> = { kind: Kind; state: T };
|
||||
|
||||
export type ViewState =
|
||||
| ViewState_<"Home", HomeState>
|
||||
| ViewState_<"SchemaEditor", SchemaEditorState>;
|
||||
|
||||
export type ViewStateKind = ViewState["kind"];
|
||||
export type State = {
|
||||
view: ViewState;
|
||||
};
|
||||
|
||||
export const reduce: Reducer<State, Action> = (state, action) => {
|
||||
if (action.kind == "goHome") {
|
||||
return {
|
||||
...state,
|
||||
view: { kind: "Home", state: null },
|
||||
};
|
||||
} else if (state.view.kind === "Home") {
|
||||
return {
|
||||
...state,
|
||||
view: reduceHome(state.view, action as HomeAction),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
view: reduceSchemaEditor(state.view, action as SchemaEditorAction),
|
||||
};
|
||||
}
|
||||
};
|
@ -1,57 +0,0 @@
|
||||
import { Const } from "../../../common/utils.ts";
|
||||
import { swel } from "../../deps/swel.ts";
|
||||
import { Dispatch } from "../../state/lib/store.ts";
|
||||
import { Home, State as HomeState } from "../Home/view.ts";
|
||||
import {
|
||||
SchemaEditor,
|
||||
State as SchemaEditorState,
|
||||
} from "../SchemaEditor/view.ts";
|
||||
import { CmpPaintComponent, Paint } from "../component.ts";
|
||||
import { Action } from "./actions.ts";
|
||||
|
||||
type ViewState_<Kind extends string, T> = { kind: Kind; state: T };
|
||||
|
||||
export type ViewState =
|
||||
| ViewState_<"Home", HomeState>
|
||||
| ViewState_<"SchemaEditor", SchemaEditorState>;
|
||||
export type ViewStateKind = ViewState["kind"];
|
||||
|
||||
export type AppState = {
|
||||
view: ViewState;
|
||||
};
|
||||
|
||||
type ViewRegistry_ = {
|
||||
[K in ViewStateKind]: ViewState & { kind: K };
|
||||
};
|
||||
type ViewRegistry = {
|
||||
// @ts-ignore: Weird
|
||||
[K in keyof ViewRegistry_]: CmpPaintComponent<ViewRegistry_[K]["state"]>;
|
||||
};
|
||||
|
||||
export class App extends CmpPaintComponent<AppState> {
|
||||
private viewRegistry: ViewRegistry;
|
||||
|
||||
constructor(private dispatch: Dispatch<Action>) {
|
||||
super();
|
||||
|
||||
this.viewRegistry = {
|
||||
SchemaEditor: new SchemaEditor(this.dispatch),
|
||||
Home: new Home(this.dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
private container = swel("div", { className: "app" });
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
paint_: Paint<AppState> = (cur) => {
|
||||
const view = this.viewRegistry[cur.view.kind];
|
||||
if (!this.previousModel || cur.view.kind != this.previousModel.view.kind) {
|
||||
this.container.replaceChildren(view.el);
|
||||
}
|
||||
|
||||
view.paint(cur.view.state as any as never);
|
||||
};
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { h } from "preact";
|
||||
import { Dispatch } from "../../utils.ts";
|
||||
import { Action } from "./state.ts";
|
||||
import { Client } from "../../fetch.ts";
|
||||
|
||||
export const Home = (p: { dispatch: Dispatch<Action>; client: Client }) => {
|
||||
const button = h(
|
||||
"button",
|
||||
{
|
||||
onClick: () => p.dispatch({ kind: "editNewSchema" }),
|
||||
},
|
||||
"New schema",
|
||||
);
|
||||
|
||||
const b = h(
|
||||
"button",
|
||||
{
|
||||
onClick: () => p.dispatch({ kind: "hello" }),
|
||||
},
|
||||
"Say hello",
|
||||
);
|
||||
|
||||
return h("div", {}, button, b);
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import { unreachable } from "../../../common/lib/dev.ts";
|
||||
import { Const, ReturnTypes } from "../../../common/utils.ts";
|
||||
import { ViewState } from "../App/view.ts";
|
||||
import { State } from "./view.ts";
|
||||
|
||||
export const newSchema = () => ({ kind: "newSchema" as const });
|
||||
export type Action = ReturnTypes<[typeof newSchema]>;
|
||||
|
||||
export const reduce = (
|
||||
state: Const<State>,
|
||||
action: Action,
|
||||
): Const<ViewState> => {
|
||||
if (action.kind == "newSchema") {
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: {
|
||||
schema: {
|
||||
fields: [],
|
||||
name: "New schema",
|
||||
},
|
||||
errors: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return unreachable();
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { Reducer } from "preact/hooks";
|
||||
import { ViewState } from "../App/state.ts";
|
||||
|
||||
export type State = null;
|
||||
export type Action = { kind: "editNewSchema" } | { kind: "hello" };
|
||||
|
||||
export const reduce = (state: ViewState, action: Action): ViewState => {
|
||||
if (action.kind == "hello") {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: {
|
||||
fields: [],
|
||||
name: "New schema",
|
||||
},
|
||||
};
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { swel } from "../../deps/swel.ts";
|
||||
import { Dispatch } from "../../state/lib/store.ts";
|
||||
import { CmpPaintComponent, Paint } from "../component.ts";
|
||||
import { Action, newSchema } from "./actions.ts";
|
||||
|
||||
export type State = undefined;
|
||||
|
||||
export class Home extends CmpPaintComponent<State> {
|
||||
private newSchemaButton = swel(
|
||||
"button",
|
||||
{
|
||||
on: { click: () => this.dispatch(newSchema()) },
|
||||
},
|
||||
"New schema",
|
||||
);
|
||||
|
||||
private container = swel("div", { className: "home" }, [
|
||||
this.newSchemaButton,
|
||||
]);
|
||||
|
||||
constructor(private dispatch: Dispatch<Action>) {
|
||||
super();
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
paint_: Paint<State> = (cur) => {};
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
import { VNode, h } from "preact";
|
||||
import {
|
||||
FieldKind,
|
||||
FieldOptions as FieldOptionsModels,
|
||||
} from "../../../../common/schema.ts";
|
||||
import { ValidationErrors } from "../../../../common/validation.ts";
|
||||
import { Select } from "../../lib/input/Select.ts";
|
||||
import { Labeled } from "../../lib/Labeled.ts";
|
||||
import { NumberInput } from "../../lib/input/NumberInput.ts";
|
||||
import { Callback } from "../../../utils.ts";
|
||||
import { Checkbox } from "../../lib/input/Checkbox.ts";
|
||||
import { ShowHide } from "../../lib/ShowHide.ts";
|
||||
import { Validated } from "../../lib/input/Validated.ts";
|
||||
|
||||
const ToggleField = (p: {
|
||||
label: string;
|
||||
onToggle: Callback<[boolean]>;
|
||||
errors?: string[];
|
||||
show: boolean;
|
||||
inner: () => VNode;
|
||||
}) => {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "toggle-field" },
|
||||
h(
|
||||
Validated,
|
||||
{ errors: p.errors },
|
||||
h(
|
||||
Labeled,
|
||||
{ label: p.label },
|
||||
h(Checkbox, { onToggle: p.onToggle, checked: p.show }),
|
||||
),
|
||||
h(ShowHide, { show: p.show, inner: p.inner }),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export type SetFieldOption<K extends FieldKind = FieldKind> = <
|
||||
KK extends keyof FieldOptionsModels[K],
|
||||
>(
|
||||
optionProp: KK,
|
||||
value: FieldOptionsModels[K][KK],
|
||||
) => void;
|
||||
|
||||
type Props<K extends FieldKind> = {
|
||||
options: FieldOptionsModels[K];
|
||||
errors?: ValidationErrors<FieldOptionsModels[K]>;
|
||||
setFieldOption: SetFieldOption<K>;
|
||||
};
|
||||
|
||||
const StringFieldOptions = (p: Props<"string">) => {
|
||||
type DisplayAs = FieldOptionsModels["string"]["displayAs"];
|
||||
|
||||
const displayAs = h(
|
||||
Labeled,
|
||||
{ label: "Display as" },
|
||||
h(Select<DisplayAs>, {
|
||||
value: p.options.displayAs,
|
||||
options: {
|
||||
input: "input",
|
||||
textarea: "textarea",
|
||||
},
|
||||
onChange: (v) => p.setFieldOption("displayAs", v),
|
||||
}),
|
||||
);
|
||||
|
||||
const minLength = h(
|
||||
Validated,
|
||||
{ errors: p.errors?.minLength },
|
||||
h(
|
||||
Labeled,
|
||||
{ label: "Min length" },
|
||||
h(NumberInput, {
|
||||
max: p.options.maxLength,
|
||||
value: p.options.minLength,
|
||||
onInput: (v) => p.setFieldOption("minLength", v),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const maxLength = h(ToggleField, {
|
||||
label: "Max length?",
|
||||
errors: p.errors?.maxLength,
|
||||
show: p.options.maxLength != undefined,
|
||||
onToggle: (active) => p.setFieldOption("maxLength", active ? 0 : undefined),
|
||||
inner: () => {
|
||||
return h(NumberInput, {
|
||||
value: p.options.maxLength ?? 0,
|
||||
min: p.options.minLength,
|
||||
onInput: (v) => p.setFieldOption("maxLength", v),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "field-options string" },
|
||||
displayAs,
|
||||
minLength,
|
||||
maxLength,
|
||||
);
|
||||
};
|
||||
const NumberFieldOptions = (p: Props<"number">) => {
|
||||
const min = h(ToggleField, {
|
||||
errors: p.errors?.min,
|
||||
label: "Min?",
|
||||
show: p.options.min != undefined,
|
||||
onToggle: (b) => p.setFieldOption("min", b ? 0 : undefined),
|
||||
inner: () =>
|
||||
h(NumberInput, {
|
||||
max: p.options.max,
|
||||
value: p.options.min ?? 0,
|
||||
onInput: (v) => p.setFieldOption("min", v),
|
||||
}),
|
||||
});
|
||||
|
||||
const max = h(ToggleField, {
|
||||
errors: p.errors?.max,
|
||||
label: "Max?",
|
||||
show: p.options.max != undefined,
|
||||
onToggle: (b) => p.setFieldOption("max", b ? 0 : undefined),
|
||||
inner: () =>
|
||||
h(NumberInput, {
|
||||
min: p.options.min,
|
||||
value: p.options.max ?? 0,
|
||||
onInput: (v) => p.setFieldOption("max", v),
|
||||
}),
|
||||
});
|
||||
|
||||
return h("div", { class: "field-options number" }, min, max);
|
||||
};
|
||||
|
||||
type FieldOptionComponentRegistry = {
|
||||
[K in FieldKind]: (p: Props<K>) => VNode;
|
||||
};
|
||||
|
||||
const FIELD_OPTION_COMPONENT_REGISTRY: FieldOptionComponentRegistry = {
|
||||
number: NumberFieldOptions,
|
||||
string: StringFieldOptions,
|
||||
};
|
||||
|
||||
export const FieldOptions = (p: {
|
||||
kind: FieldKind;
|
||||
options: FieldOptionsModels[FieldKind];
|
||||
errors?: ValidationErrors<FieldOptionsModels[FieldKind]>;
|
||||
setFieldOption: SetFieldOption<FieldKind>;
|
||||
}) => {
|
||||
const Component = FIELD_OPTION_COMPONENT_REGISTRY[p.kind];
|
||||
|
||||
return h(Component as any, p);
|
||||
};
|
@ -0,0 +1,78 @@
|
||||
import { h } from "preact";
|
||||
import { FieldKind, Field as FieldModel } from "../../../../common/schema.ts";
|
||||
import { ValidationErrors } from "../../../../common/validation.ts";
|
||||
import { Callback, Dispatch } from "../../../utils.ts";
|
||||
import { Action, deleteField, setFieldOption, setFieldProp } from "../state.ts";
|
||||
import { Validated } from "../../lib/input/Validated.ts";
|
||||
import { TextInput } from "../../lib/input/TextInput.ts";
|
||||
import { Select } from "../../lib/input/Select.ts";
|
||||
import { Labeled } from "../../lib/Labeled.ts";
|
||||
import { FieldOptions, SetFieldOption } from "./FieldOptions.ts";
|
||||
|
||||
const Field = (p: {
|
||||
field: FieldModel;
|
||||
errors?: ValidationErrors<FieldModel>;
|
||||
setName: Callback<[string]>;
|
||||
setKind: Callback<[FieldKind]>;
|
||||
setFieldOption: SetFieldOption;
|
||||
delete: Callback<[]>;
|
||||
}) => {
|
||||
const nameInput = h(
|
||||
Validated,
|
||||
{ errors: p.errors?.name },
|
||||
h(TextInput, {
|
||||
value: p.field.name,
|
||||
placeholder: "Field name",
|
||||
onInput: p.setName,
|
||||
}),
|
||||
);
|
||||
|
||||
const kindSelect = h(
|
||||
Labeled,
|
||||
{ label: "Kind" },
|
||||
h(Select<FieldKind>, {
|
||||
options: { string: "string", number: "number" },
|
||||
value: p.field.kind,
|
||||
onChange: p.setKind,
|
||||
}),
|
||||
);
|
||||
|
||||
const options = h(FieldOptions, {
|
||||
kind: p.field.kind,
|
||||
options: p.field.options,
|
||||
errors: p.errors?.options,
|
||||
setFieldOption: p.setFieldOption,
|
||||
});
|
||||
|
||||
const deleteBtn = h("button", { onClick: p.delete }, "Delete");
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "field" },
|
||||
nameInput,
|
||||
kindSelect,
|
||||
options,
|
||||
deleteBtn,
|
||||
);
|
||||
};
|
||||
|
||||
export const Fields = (p: {
|
||||
fields: FieldModel[];
|
||||
errors?: ValidationErrors<FieldModel[]>;
|
||||
dispatch: Dispatch<Action>;
|
||||
}) => {
|
||||
const fields = p.fields.map((f, i) => {
|
||||
const errors = p.errors?.[i];
|
||||
|
||||
return h(Field, {
|
||||
field: f,
|
||||
errors,
|
||||
setName: (s) => p.dispatch(setFieldProp(i, "name", s)),
|
||||
setKind: (s) => p.dispatch(setFieldProp(i, "kind", s)),
|
||||
setFieldOption: (pr, v) => p.dispatch(setFieldOption(i, pr, v)),
|
||||
delete: () => p.dispatch(deleteField(i)),
|
||||
});
|
||||
});
|
||||
|
||||
return h("div", { class: "fields" }, ...fields);
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import { h } from "preact";
|
||||
import { Dispatch } from "../../utils.ts";
|
||||
import { State, Action, setName, addField } from "./state.ts";
|
||||
import { Action as AppAction } from "../App/state.ts";
|
||||
import { TextInput } from "../lib/input/TextInput.ts";
|
||||
import { Validated } from "../lib/input/Validated.ts";
|
||||
import { validateSchema } from "../../../common/schema.ts";
|
||||
import { Fields } from "./Fields/Fields.ts";
|
||||
import { Client } from "../App/App.ts";
|
||||
import { assert } from "../../../common/deps/yaypi.ts";
|
||||
import { goHome } from "../App/state.ts";
|
||||
|
||||
export const SchemaEditor = (p: {
|
||||
state: State;
|
||||
dispatch: Dispatch<AppAction>;
|
||||
client: Client;
|
||||
}) => {
|
||||
const errors = validateSchema(p.state);
|
||||
|
||||
const nameInput = h(
|
||||
Validated,
|
||||
{
|
||||
errors: errors.name,
|
||||
},
|
||||
h(TextInput, {
|
||||
placeholder: "Schema name",
|
||||
value: p.state.name,
|
||||
onInput: (s) => p.dispatch(setName(s)),
|
||||
class: "name",
|
||||
}),
|
||||
);
|
||||
|
||||
const fields = h(Fields, {
|
||||
fields: p.state.fields,
|
||||
dispatch: p.dispatch,
|
||||
errors: errors.fields,
|
||||
});
|
||||
|
||||
const addFieldBtn = h(
|
||||
"button",
|
||||
{ onClick: () => p.dispatch(addField()) },
|
||||
"Add field",
|
||||
);
|
||||
|
||||
const save = async () => {
|
||||
const result = assert(await p.client.v1.saveSchema(p.state));
|
||||
if (typeof result === "string") {
|
||||
console.error(result);
|
||||
}
|
||||
|
||||
p.dispatch(goHome());
|
||||
};
|
||||
const saveBtn = h("button", { onClick: () => save() }, "Save");
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "schema-editor" },
|
||||
nameInput,
|
||||
fields,
|
||||
addFieldBtn,
|
||||
saveBtn,
|
||||
);
|
||||
};
|
@ -1,118 +0,0 @@
|
||||
import { unreachable } from "../../../common/lib/dev.ts";
|
||||
import { push, setIdx, setProp } from "../../../common/lib/imm.ts";
|
||||
import {
|
||||
Field,
|
||||
FieldKind,
|
||||
FieldOptions,
|
||||
MaybePersistedSchema,
|
||||
defaultOptionsForFieldKind,
|
||||
validateSchema,
|
||||
} from "../../../common/schema.ts";
|
||||
import { Const, ReturnTypes } from "../../../common/utils.ts";
|
||||
import { Reduce } from "../../state/lib/store.ts";
|
||||
import { State } from "./view.ts";
|
||||
|
||||
export const setName = (name: string) => ({ setName: name });
|
||||
export const addField = () => "addField" as const;
|
||||
export const setFieldProp = <P extends keyof Field>(
|
||||
index: number,
|
||||
prop: P,
|
||||
value: Field[P],
|
||||
) => ({
|
||||
index,
|
||||
prop,
|
||||
value,
|
||||
});
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never;
|
||||
export type AllOptions = UnionToIntersection<FieldOptions[FieldKind]>;
|
||||
export const setFieldOptionProp = <P extends keyof AllOptions>(
|
||||
index: number,
|
||||
optionProp: P,
|
||||
value: Required<AllOptions[P]>,
|
||||
) => ({
|
||||
index,
|
||||
optionProp,
|
||||
value,
|
||||
});
|
||||
export type Action = ReturnTypes<
|
||||
[
|
||||
typeof setName,
|
||||
typeof addField,
|
||||
typeof setFieldProp,
|
||||
typeof setFieldOptionProp,
|
||||
]
|
||||
>;
|
||||
|
||||
const reduceSchema = (
|
||||
schema: Const<MaybePersistedSchema>,
|
||||
action: Action,
|
||||
): Const<MaybePersistedSchema> => {
|
||||
console.debug(action);
|
||||
if (action == "addField") {
|
||||
return setProp(
|
||||
schema,
|
||||
"fields",
|
||||
push(schema.fields, {
|
||||
kind: "string",
|
||||
name: "New field",
|
||||
options: defaultOptionsForFieldKind("string"),
|
||||
required: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if ("setName" in action) {
|
||||
return setProp(schema, "name", action.setName);
|
||||
}
|
||||
|
||||
if ("prop" in action) {
|
||||
let field = setProp(schema.fields[action.index], action.prop, action.value);
|
||||
|
||||
if (action.prop == "kind") {
|
||||
field = setProp(field, "options", {
|
||||
...defaultOptionsForFieldKind(action.value as Field["kind"]),
|
||||
...field.options,
|
||||
});
|
||||
}
|
||||
|
||||
return setProp(
|
||||
schema,
|
||||
"fields",
|
||||
setIdx(schema.fields, action.index, field),
|
||||
);
|
||||
}
|
||||
|
||||
if ("optionProp" in action) {
|
||||
return setProp(
|
||||
schema,
|
||||
"fields",
|
||||
setIdx(
|
||||
schema.fields,
|
||||
action.index,
|
||||
setProp(
|
||||
schema.fields[action.index],
|
||||
"options",
|
||||
setProp(
|
||||
schema.fields[action.index].options as any,
|
||||
action.optionProp as never,
|
||||
action.value as never,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return unreachable();
|
||||
};
|
||||
|
||||
export const reduce: Reduce<State, Action> = (state, action) => {
|
||||
const schema = reduceSchema(state.schema, action);
|
||||
const errors = validateSchema(schema);
|
||||
|
||||
return { schema, errors };
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
import { Reducer } from "preact/hooks";
|
||||
import {
|
||||
Field,
|
||||
FieldKind,
|
||||
FieldOptions,
|
||||
MaybePersistedSchema,
|
||||
defaultOptionsForFieldKind,
|
||||
} from "../../../common/schema.ts";
|
||||
import { ViewState } from "../App/state.ts";
|
||||
import { ReturnTypes, UnionToIntersection } from "../../../common/utils.ts";
|
||||
import { deleteIdx, setIdx, setProp } from "../../../common/lib/imm.ts";
|
||||
|
||||
export const setName = (name: string) => ({ kind: "setName" as const, name });
|
||||
export const addField = () => ({ kind: "addField" as const });
|
||||
export const setFieldProp = <K extends keyof Field>(
|
||||
index: number,
|
||||
prop: K,
|
||||
value: Field[K],
|
||||
) => ({
|
||||
kind: "setFieldProp" as const,
|
||||
index,
|
||||
prop,
|
||||
value,
|
||||
});
|
||||
|
||||
export type AllOptions = UnionToIntersection<FieldOptions[FieldKind]>;
|
||||
export const setFieldOption = <K extends keyof AllOptions>(
|
||||
index: number,
|
||||
option: K,
|
||||
value: AllOptions[K],
|
||||
) => ({
|
||||
kind: "setFieldOption" as const,
|
||||
index,
|
||||
option,
|
||||
value,
|
||||
});
|
||||
export const deleteField = (index: number) => ({
|
||||
kind: "deleteField" as const,
|
||||
index,
|
||||
});
|
||||
export type Action = ReturnTypes<
|
||||
[
|
||||
typeof setName,
|
||||
typeof addField,
|
||||
typeof setFieldProp,
|
||||
typeof deleteField,
|
||||
typeof setFieldOption,
|
||||
]
|
||||
>;
|
||||
|
||||
export type State = MaybePersistedSchema;
|
||||
|
||||
const reduce_ = (state: State, action: Action): ViewState => {
|
||||
if (action.kind == "setName") {
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: {
|
||||
...state,
|
||||
name: action.name,
|
||||
},
|
||||
};
|
||||
} else if (action.kind == "addField") {
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: {
|
||||
...state,
|
||||
fields: [
|
||||
...state.fields,
|
||||
{
|
||||
kind: "string",
|
||||
name: "New field",
|
||||
options: defaultOptionsForFieldKind("string"),
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (action.kind == "setFieldProp") {
|
||||
let field = setProp(state.fields[action.index], action.prop, action.value);
|
||||
if (action.prop == "kind") {
|
||||
field = setProp(field, "options", {
|
||||
...defaultOptionsForFieldKind(field.kind),
|
||||
...field.options,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: {
|
||||
...state,
|
||||
fields: setIdx(state.fields, action.index, field),
|
||||
},
|
||||
};
|
||||
} else if (action.kind == "setFieldOption") {
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: setProp(
|
||||
state,
|
||||
"fields",
|
||||
setIdx(
|
||||
state.fields,
|
||||
action.index,
|
||||
setProp(
|
||||
state.fields[action.index],
|
||||
"options",
|
||||
setProp(
|
||||
state.fields[action.index].options,
|
||||
action.option as never,
|
||||
action.value as never,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
} else if (action.kind == "deleteField") {
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state: setProp(state, "fields", deleteIdx(state.fields, action.index)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "SchemaEditor",
|
||||
state,
|
||||
};
|
||||
};
|
||||
|
||||
export const reduce: Reducer<ViewState, Action> = (state, action) => {
|
||||
return reduce_(state.state as State, action);
|
||||
};
|
@ -1,71 +0,0 @@
|
||||
import {
|
||||
MaybePersistedSchema,
|
||||
ValidateSchemaResult,
|
||||
validateSchema,
|
||||
} from "../../../common/schema.ts";
|
||||
import { swel } from "../../deps/swel.ts";
|
||||
import { Dispatch } from "../../state/lib/store.ts";
|
||||
import { CmpPaintComponent, Paint } from "../component.ts";
|
||||
import { TextInput } from "../lib/input/TextInput.ts";
|
||||
import { Validated } from "../lib/input/Validated.ts";
|
||||
import { Fields } from "./Fields.ts";
|
||||
import { Action, setName, addField } from "./actions.ts";
|
||||
|
||||
export type State = {
|
||||
schema: MaybePersistedSchema;
|
||||
errors: ValidateSchemaResult;
|
||||
};
|
||||
|
||||
export class SchemaEditor extends CmpPaintComponent<State> {
|
||||
private nameInput: Validated<string, TextInput>;
|
||||
private fields: Fields;
|
||||
private container: HTMLDivElement;
|
||||
|
||||
constructor(dispatch: Dispatch<Action>) {
|
||||
super();
|
||||
|
||||
this.nameInput = new Validated(
|
||||
new TextInput({
|
||||
onInput: (s) => dispatch(setName(s)),
|
||||
}),
|
||||
);
|
||||
|
||||
const addFieldBtn = swel(
|
||||
"button",
|
||||
{
|
||||
on: {
|
||||
click: () => {
|
||||
dispatch(addField());
|
||||
},
|
||||
},
|
||||
},
|
||||
"Add field",
|
||||
);
|
||||
|
||||
this.fields = new Fields(dispatch);
|
||||
|
||||
this.container = swel("div", { className: "schema-editor" }, [
|
||||
this.nameInput.el,
|
||||
this.fields.el,
|
||||
addFieldBtn,
|
||||
]);
|
||||
}
|
||||
|
||||
get el() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
paint_: Paint<State> = (cur) => {
|
||||
this.nameInput.paint({
|
||||
model: cur.schema.name,
|
||||
error: cur.errors.name?.[0],
|
||||
});
|
||||
|
||||
const fields = cur.schema.fields.map((f, i) => ({
|
||||
field: f,
|
||||
errors: cur.errors.fields?.[i],
|
||||
}));
|
||||
|
||||
this.fields.paint(fields);
|
||||
};
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { h } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { Callback } from "../../../utils.ts";
|
||||
|
||||
export const Checkbox = (p: {
|
||||
checked: boolean;
|
||||
onToggle: Callback<[boolean]>;
|
||||
}) => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
return h("input", {
|
||||
ref,
|
||||
type: "checkbox",
|
||||
checked: p.checked,
|
||||
onChange: () => p.onToggle(ref.current!.checked),
|
||||
});
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { Reducer, useMemo, useReducer } from "preact/hooks";
|
||||
import { MakeClient, assert, makeClient } from "../common/deps/yaypi.ts";
|
||||
import { Api } from "../common/api.ts";
|
||||
|
||||
export type Client = MakeClient<typeof Api>;
|
||||
|
||||
export type Loadable<T, E = string> =
|
||||
| null
|
||||
| { loading: true }
|
||||
| { loading: boolean; data: T }
|
||||
| { loading: boolean; error: string };
|
||||
export const isLoading = <T>(loadable: Loadable<T>) => loadable?.loading;
|
||||
|
||||
export type Data = {
|
||||
schemas: Loadable<ReturnType<Client["v1"]["listSchemas"]>>;
|
||||
};
|
||||
export type Action = never;
|
||||
|
||||
const reduce: Reducer<Data, Action> = (data, action) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const emptyData: Data = {
|
||||
schemas: null,
|
||||
};
|
||||
|
||||
// NOTE: this should only be used in App.ts
|
||||
export const useFetch = () => {
|
||||
const client = useMemo(() => makeClient(Api, "/api"), []);
|
||||
const [data, reducer] = useReducer(reduce, emptyData);
|
||||
|
||||
const listSchemas = async (
|
||||
...args: Parameters<typeof client.v1.listSchemas>
|
||||
) => {
|
||||
const schemas = assert(await client.v1.listSchemas(...args));
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue