|
|
|
@ -1,13 +1,53 @@
|
|
|
|
|
import { TempstreamJSX, Templatable } from "tempstream";
|
|
|
|
|
import { TempstreamJSX, Templatable, FlatTemplatable, tempstream } from "tempstream";
|
|
|
|
|
import { BaseContext } from "koa";
|
|
|
|
|
import { StatefulPage } from "@sealcode/sealgen";
|
|
|
|
|
import html from "../html.js";
|
|
|
|
|
import { registry } from "../jdd-components/components.js";
|
|
|
|
|
import { render, simpleJDDContext } from "@sealcode/jdd";
|
|
|
|
|
import {
|
|
|
|
|
ComponentArgument,
|
|
|
|
|
Enum,
|
|
|
|
|
List,
|
|
|
|
|
render,
|
|
|
|
|
simpleJDDContext,
|
|
|
|
|
Structured,
|
|
|
|
|
} from "@sealcode/jdd";
|
|
|
|
|
import objectPath from "object-path";
|
|
|
|
|
|
|
|
|
|
export const actionName = "Components";
|
|
|
|
|
|
|
|
|
|
const actions = {} as const;
|
|
|
|
|
const actions = {
|
|
|
|
|
add_array_item: (
|
|
|
|
|
state: State,
|
|
|
|
|
_: Record<string, string>,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
empty_value: unknown
|
|
|
|
|
) => {
|
|
|
|
|
const args = state.args;
|
|
|
|
|
objectPath.insert(
|
|
|
|
|
args,
|
|
|
|
|
arg_path,
|
|
|
|
|
empty_value,
|
|
|
|
|
((objectPath.get(args, arg_path) as unknown[]) || []).length
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
args,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
remove_array_item: (
|
|
|
|
|
state: State,
|
|
|
|
|
_: Record<string, string>,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
index_to_remove: number
|
|
|
|
|
) => {
|
|
|
|
|
const args = state.args;
|
|
|
|
|
objectPath.del(args, [...arg_path, index_to_remove]);
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
args,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
type State = {
|
|
|
|
|
component: string;
|
|
|
|
@ -22,7 +62,150 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
|
|
|
|
|
return html(ctx, "Components", content);
|
|
|
|
|
return html(ctx, "Components", content, {
|
|
|
|
|
morphing: true,
|
|
|
|
|
preserveScroll: true,
|
|
|
|
|
navbar: () => ``,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderListArgument<T>(
|
|
|
|
|
state: State,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
arg: List<ComponentArgument<T>>,
|
|
|
|
|
value: T[] = []
|
|
|
|
|
): FlatTemplatable {
|
|
|
|
|
return (
|
|
|
|
|
<fieldset>
|
|
|
|
|
<legend>{arg_path.at(-1)}</legend>
|
|
|
|
|
{value.map((e, i) => (
|
|
|
|
|
<div style="display: flex">
|
|
|
|
|
{this.renderArgumentInput(
|
|
|
|
|
state,
|
|
|
|
|
[...arg_path, i.toString()],
|
|
|
|
|
arg.item_type,
|
|
|
|
|
e
|
|
|
|
|
)}
|
|
|
|
|
{this.makeActionButton(
|
|
|
|
|
state,
|
|
|
|
|
{ action: "remove_array_item", label: "❌" },
|
|
|
|
|
arg_path,
|
|
|
|
|
i
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{this.makeActionButton(
|
|
|
|
|
state,
|
|
|
|
|
{
|
|
|
|
|
action: "add_array_item",
|
|
|
|
|
label: "➕",
|
|
|
|
|
},
|
|
|
|
|
arg_path,
|
|
|
|
|
arg.item_type.getEmptyValue()
|
|
|
|
|
)}
|
|
|
|
|
</fieldset>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderStructuredArgument<
|
|
|
|
|
T extends Structured<Record<string, ComponentArgument<unknown>>>
|
|
|
|
|
>(
|
|
|
|
|
state: State,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
arg: T,
|
|
|
|
|
value: Record<string, unknown>
|
|
|
|
|
): FlatTemplatable {
|
|
|
|
|
return (
|
|
|
|
|
<fieldset>
|
|
|
|
|
<legend>{arg_path.at(-1)}</legend>
|
|
|
|
|
{Object.entries(arg.structure).map(([arg_name, arg]) => (
|
|
|
|
|
<div>
|
|
|
|
|
{this.renderArgumentInput(
|
|
|
|
|
state,
|
|
|
|
|
[...arg_path, arg_name],
|
|
|
|
|
arg,
|
|
|
|
|
(value as Record<string, unknown>)[arg_name]
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</fieldset>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printArgPath(path: string[]): string {
|
|
|
|
|
return path.map((e) => `[${e}]`).join("");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderEnumArgument<T extends Enum<any>>(
|
|
|
|
|
state: State,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
arg: T,
|
|
|
|
|
value: string
|
|
|
|
|
): FlatTemplatable {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<label>
|
|
|
|
|
{arg_path.at(-1)}
|
|
|
|
|
<select name={`$.args${this.printArgPath(arg_path)}`}>
|
|
|
|
|
{arg.values.map((v) => (
|
|
|
|
|
<option value={v} selected={value == v}>
|
|
|
|
|
{v}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderArgumentInput<T>(
|
|
|
|
|
state: State,
|
|
|
|
|
arg_path: string[],
|
|
|
|
|
arg: ComponentArgument<T>,
|
|
|
|
|
value: T
|
|
|
|
|
): FlatTemplatable {
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
value = arg.getEmptyValue();
|
|
|
|
|
}
|
|
|
|
|
if (arg instanceof List) {
|
|
|
|
|
return this.renderListArgument(state, arg_path, arg, value as T[]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (arg instanceof Structured) {
|
|
|
|
|
return this.renderStructuredArgument(
|
|
|
|
|
state,
|
|
|
|
|
arg_path,
|
|
|
|
|
arg,
|
|
|
|
|
value as Record<string, unknown>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (arg instanceof Enum) {
|
|
|
|
|
return this.renderEnumArgument(state, arg_path, arg, value as string);
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<label>
|
|
|
|
|
{arg_path.at(-1)}
|
|
|
|
|
{arg.getTypeName() == "markdown" ? (
|
|
|
|
|
<textarea
|
|
|
|
|
name={`$.args${this.printArgPath(arg_path)}`}
|
|
|
|
|
onblur="this.closest('form').requestSubmit()"
|
|
|
|
|
cols="70"
|
|
|
|
|
>
|
|
|
|
|
{value as string}
|
|
|
|
|
</textarea>
|
|
|
|
|
) : (
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
name={`$.args${this.printArgPath(arg_path)}`}
|
|
|
|
|
value={value as string}
|
|
|
|
|
size="70"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(ctx: BaseContext, state: State, inputs: Record<string, string>) {
|
|
|
|
@ -30,46 +213,87 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
|
|
|
|
const component =
|
|
|
|
|
registry.get(state.component) || Object.values(all_components)[0];
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div>{JSON.stringify(state)}</div>
|
|
|
|
|
<select name="$.component" onchange="this.closest('form').submit()">
|
|
|
|
|
{Object.entries(all_components).map(([name]) => (
|
|
|
|
|
<option value={name} selected={name == state.component}>
|
|
|
|
|
{name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<fieldset>
|
|
|
|
|
<legend>Parameters</legend>
|
|
|
|
|
{Object.entries(component.getArguments()).map(([arg_name, arg]) => (
|
|
|
|
|
<div>
|
|
|
|
|
<label>
|
|
|
|
|
{arg_name}
|
|
|
|
|
{arg.getTypeName() == "markdown" ? (
|
|
|
|
|
<textarea name={`$.args[${arg_name}]`}>
|
|
|
|
|
{state.args[arg_name] as string}
|
|
|
|
|
</textarea>
|
|
|
|
|
) : (
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
name={`$.args[${arg_name}]`}
|
|
|
|
|
value={state.args[arg_name] as string}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<div class="two-column">
|
|
|
|
|
<div class="resizable">
|
|
|
|
|
{/*The below button has to be here in order for it to be the default behavior */}
|
|
|
|
|
<input type="submit" value="Preview" />
|
|
|
|
|
</fieldset>
|
|
|
|
|
<fieldset>
|
|
|
|
|
<legend>Preview</legend>
|
|
|
|
|
{render(
|
|
|
|
|
registry,
|
|
|
|
|
[{ component_name: state.component, args: state.args }],
|
|
|
|
|
simpleJDDContext
|
|
|
|
|
)}
|
|
|
|
|
</fieldset>
|
|
|
|
|
<select
|
|
|
|
|
name="$.component"
|
|
|
|
|
onchange="this.closest('form').requestSubmit()"
|
|
|
|
|
>
|
|
|
|
|
{Object.entries(all_components).map(([name]) => (
|
|
|
|
|
<option value={name} selected={name == state.component}>
|
|
|
|
|
{name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<fieldset class="component-preview-parameters">
|
|
|
|
|
<legend>Parameters</legend>
|
|
|
|
|
{Object.entries(component.getArguments()).map(([arg_name, arg]) =>
|
|
|
|
|
this.renderArgumentInput(
|
|
|
|
|
state,
|
|
|
|
|
[arg_name],
|
|
|
|
|
arg,
|
|
|
|
|
state.args[arg_name]
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
<input type="submit" value="Preview" />
|
|
|
|
|
</fieldset>
|
|
|
|
|
<div>{JSON.stringify(state)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="resize-gutter"></div>
|
|
|
|
|
{
|
|
|
|
|
/* HTML */ `<script>
|
|
|
|
|
(function () {
|
|
|
|
|
let is_resizing = false;
|
|
|
|
|
let origin_x;
|
|
|
|
|
let origin_width;
|
|
|
|
|
const gutter = document.querySelector(".resize-gutter");
|
|
|
|
|
const resizable = document.querySelector(".resizable");
|
|
|
|
|
const move_listener = (e) => {
|
|
|
|
|
const new_width = Math.max(
|
|
|
|
|
origin_width + (e.clientX - origin_x),
|
|
|
|
|
1
|
|
|
|
|
);
|
|
|
|
|
document.documentElement.style.setProperty(
|
|
|
|
|
"--resizable-column-width",
|
|
|
|
|
new_width + "px"
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
gutter.addEventListener("mousedown", (e) => {
|
|
|
|
|
is_resizing = true;
|
|
|
|
|
origin_x = e.clientX;
|
|
|
|
|
origin_width = resizable.getBoundingClientRect().width;
|
|
|
|
|
document.addEventListener("mousemove", move_listener);
|
|
|
|
|
document.addEventListener("mouseup", () => {
|
|
|
|
|
document.removeEventListener(
|
|
|
|
|
"mousemove",
|
|
|
|
|
move_listener
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>`
|
|
|
|
|
}
|
|
|
|
|
<div>
|
|
|
|
|
<fieldset>
|
|
|
|
|
<legend>Preview</legend>
|
|
|
|
|
{render(
|
|
|
|
|
registry,
|
|
|
|
|
[{ component_name: state.component, args: state.args }],
|
|
|
|
|
simpleJDDContext
|
|
|
|
|
)}
|
|
|
|
|
</fieldset>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wrapInForm(state: State, content: Templatable): Templatable {
|
|
|
|
|
// return tempstream/* HTML */ `<turbo-frame id="components">
|
|
|
|
|
// ${super.wrapInForm(state, content)}
|
|
|
|
|
// </turbo-frame> `;
|
|
|
|
|
// }
|
|
|
|
|
})();
|
|
|
|
|