Preactify
parent
8079a1bd79
commit
4cd9a47dc3
@ -1,23 +1,24 @@
|
|||||||
import { Const } from "../utils.ts";
|
export const setIdx = <T>(arr: T[], index: number, newValue: T): T[] => {
|
||||||
|
const newArr = [...arr];
|
||||||
export const setIdx = <T>(
|
|
||||||
arr: Const<T[]>,
|
|
||||||
index: number,
|
|
||||||
newValue: Const<T>,
|
|
||||||
): Const<T[]> => {
|
|
||||||
const newArr = [...arr] as Const<T>[];
|
|
||||||
newArr[index] = newValue;
|
newArr[index] = newValue;
|
||||||
|
|
||||||
return newArr;
|
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>(
|
export const setProp = <T, K extends keyof T>(
|
||||||
obj: Const<T>,
|
obj: T,
|
||||||
key: K,
|
key: K,
|
||||||
newValue: Const<T[K]>,
|
newValue: T[K],
|
||||||
): Const<T> => ({
|
): T => ({
|
||||||
...obj,
|
...obj,
|
||||||
[key]: newValue,
|
[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);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,27 +1,5 @@
|
|||||||
import { swel } from "../../deps/swel.ts";
|
import { h, ComponentChildren } from "preact";
|
||||||
import {
|
|
||||||
CmpPaintComponent,
|
|
||||||
ComponentModel,
|
|
||||||
Paint,
|
|
||||||
PaintComponent,
|
|
||||||
} from "../component.ts";
|
|
||||||
|
|
||||||
export class Labeled<
|
export const Labeled = (p: { label: string; children?: ComponentChildren }) => {
|
||||||
Child extends PaintComponent<any>,
|
return h("label", {}, p.label, p.children);
|
||||||
> extends CmpPaintComponent<ComponentModel<Child>> {
|
};
|
||||||
private label: HTMLLabelElement;
|
|
||||||
|
|
||||||
constructor(text: string, public child: Child) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.label = swel("label", {}, [text, this.child.el]);
|
|
||||||
}
|
|
||||||
|
|
||||||
get el(): HTMLElement {
|
|
||||||
return this.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
paint_: Paint<ComponentModel<Child>> = (c) => {
|
|
||||||
this.child.paint(c);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,35 +1,9 @@
|
|||||||
import { swel } from "../../deps/swel.ts";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { ComponentModel, Paint } from "../component.ts";
|
|
||||||
import { CmpPaintComponent } from "../component.ts";
|
|
||||||
|
|
||||||
export type ShowHideModel<T> = {
|
export const ShowHide = (p: { show: boolean; inner: () => VNode }) => {
|
||||||
show: boolean;
|
if (p.show) {
|
||||||
model: T;
|
return p.inner();
|
||||||
};
|
|
||||||
|
|
||||||
export class ShowHide<
|
|
||||||
Child extends CmpPaintComponent<any>,
|
|
||||||
> extends CmpPaintComponent<ShowHideModel<ComponentModel<Child>>> {
|
|
||||||
private container: HTMLDivElement;
|
|
||||||
|
|
||||||
constructor(private child: Child) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.container = swel("div", {}, this.child.el);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get el(): HTMLElement {
|
return h(Fragment, {});
|
||||||
return this.container;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
protected paint_: Paint<ShowHideModel<ComponentModel<Child>>> = (cur) => {
|
|
||||||
if (cur.show) {
|
|
||||||
this.child.paint(cur.model);
|
|
||||||
if (!this.previousModel?.show) {
|
|
||||||
this.container.replaceChildren(this.child.el);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.container.replaceChildren("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -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),
|
||||||
|
});
|
||||||
|
};
|
@ -1,33 +1,24 @@
|
|||||||
import { swel } from "../../../deps/swel.ts";
|
import { JSX, h } from "preact";
|
||||||
|
import { useRef } from "preact/hooks";
|
||||||
import { Callback } from "../../../utils.ts";
|
import { Callback } from "../../../utils.ts";
|
||||||
import { CmpPaintComponent, Paint } from "../../component.ts";
|
|
||||||
|
|
||||||
export class NumberInput extends CmpPaintComponent<number> {
|
export const NumberInput = (
|
||||||
private input: HTMLInputElement = swel("input", { type: "number" });
|
p: Omit<JSX.HTMLAttributes<HTMLInputElement>, "onInput"> & {
|
||||||
|
value: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
onInput: Callback<[number]>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
constructor(p?: Partial<{ onChange: Callback<[number]> }>) {
|
return h("input", {
|
||||||
super();
|
...p,
|
||||||
|
type: "number",
|
||||||
if (p?.onChange) {
|
ref,
|
||||||
this.input.addEventListener("input", () =>
|
min: p.min,
|
||||||
p.onChange!(Number(this.input.value)),
|
max: p.max,
|
||||||
);
|
value: p.value,
|
||||||
}
|
onInput: () => p.onInput(Number(ref.current!.value)),
|
||||||
}
|
});
|
||||||
|
};
|
||||||
get el() {
|
|
||||||
return this.input;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMin = (n?: number) => {
|
|
||||||
this.input.min = `${n ?? ""}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
setMax = (n?: number) => {
|
|
||||||
this.input.max = `${n ?? ""}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
paint_: Paint<number> = (cur) => {
|
|
||||||
this.input.value = `${cur}`;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,33 +1,26 @@
|
|||||||
import { swel } from "../../../deps/swel.ts";
|
import { h } from "preact";
|
||||||
|
import { useRef } from "preact/hooks";
|
||||||
import { Callback } from "../../../utils.ts";
|
import { Callback } from "../../../utils.ts";
|
||||||
import { CmpPaintComponent, Paint } from "../../component.ts";
|
|
||||||
|
|
||||||
export type Options<Values extends string> = Record<Values, string>;
|
export type Options<Values extends string> = Record<Values, string>;
|
||||||
|
|
||||||
export class Select<Values extends string> extends CmpPaintComponent<Values> {
|
export const Select = <Values extends string>(p: {
|
||||||
private select: HTMLSelectElement;
|
value: Values;
|
||||||
|
options: Options<Values>;
|
||||||
|
onChange: Callback<[Values]>;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLSelectElement>(null);
|
||||||
|
const select = h(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
ref,
|
||||||
|
value: p.value,
|
||||||
|
onChange: () => p.onChange(ref.current!.value as Values),
|
||||||
|
},
|
||||||
|
...Object.entries(p.options).map(([k, v]) =>
|
||||||
|
h("option", { value: `${v}` }, k),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(options: Options<Values>, onChange: Callback<[Values]>) {
|
return select;
|
||||||
super();
|
};
|
||||||
|
|
||||||
this.select = swel(
|
|
||||||
"select",
|
|
||||||
{
|
|
||||||
on: {
|
|
||||||
change: () => onChange(this.select.value as Values),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
(Object.keys(options) as (keyof typeof options)[]).map((o) =>
|
|
||||||
swel("option", { value: `${o}` }, options[o]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get el(): HTMLElement {
|
|
||||||
return this.select;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected paint_: Paint<Values> = (cur) => {
|
|
||||||
this.select.value = cur as string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
import { swel } from "../../../deps/swel.ts";
|
import { h, JSX } from "preact";
|
||||||
|
import { useRef } from "preact/hooks";
|
||||||
import { Callback } from "../../../utils.ts";
|
import { Callback } from "../../../utils.ts";
|
||||||
import { CmpPaintComponent, Paint } from "../../component.ts";
|
|
||||||
|
|
||||||
export class TextInput extends CmpPaintComponent<string> {
|
export const TextInput = (
|
||||||
private input: HTMLInputElement = swel("input");
|
p: Omit<JSX.HTMLAttributes<HTMLInputElement>, "onInput"> & {
|
||||||
|
placeholder?: string;
|
||||||
|
value: string;
|
||||||
|
onInput: Callback<[string]>;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
constructor(p?: Partial<{ onInput: Callback<[string]> }>) {
|
return h("input", {
|
||||||
super();
|
...p,
|
||||||
|
ref,
|
||||||
if (p?.onInput) {
|
onInput: () => p.onInput(ref.current!.value),
|
||||||
this.input.addEventListener("input", () => p.onInput!(this.input.value));
|
});
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
get el() {
|
|
||||||
return this.input;
|
|
||||||
}
|
|
||||||
|
|
||||||
paint_: Paint<string> = (cur) => {
|
|
||||||
this.input.value = cur;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -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