Sreact page for editing JDD

Summary: Ref T2764

Reviewers: #testers, kuba-orlik

Reviewed By: #testers, kuba-orlik

Subscribers: kuba-orlik, jenkins-user

Maniphest Tasks: T2764

Differential Revision: https://hub.sealcode.org/D1406
This commit is contained in:
Kuba Orlik 2024-05-05 18:10:42 +02:00
parent a3965ae47e
commit bec96ac604
17 changed files with 1022 additions and 663 deletions

View File

@ -16,6 +16,7 @@ module.exports = {
project: "./tsconfig-back.json",
},
rules: {
"no-unused-vars": [2, { varsIgnorePattern: "TempstreamJSX" }],
"@typescript-eslint/no-unused-vars": [2, { varsIgnorePattern: "TempstreamJSX" }],
"no-unused-vars": 0,
"@typescript-eslint/require-await": 0,

619
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,8 +43,8 @@
"@koa/router": "^12.0.1",
"@playwright/test": "^1.36.1",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.4.7",
"@sealcode/sealgen": "^0.15.15",
"@sealcode/jdd": "^0.4.10",
"@sealcode/sealgen": "^0.15.16",
"@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0",
"@types/leaflet": "^1.9.8",

View File

@ -1,9 +1,9 @@
import type { Context } from "koa";
import { TempstreamJSX } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import html from "../html.js";
import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js";
import { documentContainerFromParsed, render } from "@sealcode/jdd";
import { documentContainerFromParsed, render, renderEarlyAssets } from "@sealcode/jdd";
import { shuffle } from "../util.js";
import { makeJDDContext } from "../jdd-context.js";
@ -33,7 +33,14 @@ export default new (class AllComponentsPage extends Page {
"AllComponents",
<div>
{render(registry, documentContainerFromParsed(document), jdd_context)}
</div>
</div>,
{},
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed(document),
jdd_context
)}`
);
}
})();

View File

@ -18,10 +18,7 @@ export function ComponentInputEnum<State, S extends string, T extends Enum<S>>({
<div>
<label>
{arg_path.at(-1) || ""}
<select
name={`$.component_args${printArgPath(arg_path)}`}
onchange={onchange}
>
<select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
{arg.values.map((v: S) => (
<option value={v} selected={value == v}>
{v}

View File

@ -1,15 +1,15 @@
import type { Image } from "@sealcode/jdd";
import type { BaseContext } from "koa";
import type { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import type { FilePointer } from "@sealcode/file-manager";
import type { Image } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import { makeJDDContext } from "../../jdd-context.js";
import { printArgPath } from "./print-arg-path.js";
import { htmlEscape } from "escape-goat";
import type { FilePointer } from "@sealcode/file-manager";
import { makeJDDContext } from "../../jdd-context.js";
export function ComponentInputImage<State extends ComponentPreviewState>({
export function ComponentInputImage<State extends JDDPageState>({
arg_path,
arg,
value,
@ -19,9 +19,9 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
arg_path: string[];
arg: Image;
value: FilePointer | null;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
ctx: BaseContext;
}) {
}): JSX.Element {
const jdd_context = makeJDDContext(ctx);
return (
<div style="margin-bottom: 10px">
@ -38,7 +38,7 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
})}
<input
type="file"
name={`$.component_args${printArgPath(arg_path)}.new`}
name={`$${printArgPath(arg_path)}.new`}
value=""
autocomplete="off"
data-action="change->input-image-preview#handleChange"
@ -48,7 +48,7 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
<div>
<input
type="hidden"
name={`$.component_args${printArgPath(arg_path)}.old`}
name={`$${printArgPath(arg_path)}.old`}
value={htmlEscape(value?.token || "")}
autocomplete="off"
/>

View File

@ -1,13 +1,14 @@
import type { ComponentArgument, List } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import type { ComponentArgument, List } from "@sealcode/jdd";
import type { JDDPageState } from "./jdd-page.js";
import type { StatefulPage } from "@sealcode/sealgen";
import type { Readable } from "node:stream";
import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
export async function ComponentInputList<
State extends ComponentPreviewState,
State extends JDDPageState,
T extends ComponentArgument<unknown>
>({
state,
@ -20,10 +21,11 @@ export async function ComponentInputList<
state: State;
ctx: BaseContext;
arg_path: string[];
arg: List<T>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arg: List<T, any>;
value: T[];
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
}) {
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Promise<Readable> {
if (!value) {
value = [];
}

View File

@ -2,8 +2,8 @@ import type { BaseContext } from "koa";
import type { ComponentArgument, Structured } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import { ComponentInput } from "./component-input.js";
import type { JDDPageState } from "./jdd-page.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
export function ComponentInputStructured<
@ -17,13 +17,13 @@ export function ComponentInputStructured<
rerender_callback,
page,
}: {
state: ComponentPreviewState;
state: JDDPageState;
ctx: BaseContext;
arg_path: string[];
arg: T;
value: Record<string, unknown>;
rerender_callback?: string;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}) {
return (
<fieldset>

View File

@ -3,10 +3,9 @@ import { isTableHeader } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import add_column_right_icon from "./table-add-column-right.svg";
import add_row_below_icon from "./table-add-row-below.svg";
import add_column_header_icon from "./table-add-row-header-below.svg";
@ -16,7 +15,7 @@ import move_column_right_icon from "./table-move-column-right.svg";
import move_row_down_icon from "./table-move-row-down.svg";
export async function ComponentInputTable<
State extends ComponentPreviewState,
State extends JDDPageState,
CellType,
HeaderType
>({
@ -32,8 +31,8 @@ export async function ComponentInputTable<
arg_path: string[];
arg: Table<CellType, HeaderType>;
value: TableData<CellType, HeaderType>;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
}) {
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Promise<import("stream").Readable> {
if (!value) {
value = arg.getEmptyValue();
}

View File

@ -1,23 +1,24 @@
import type { FilePointer } from "@sealcode/file-manager";
import { printArgPath } from "./print-arg-path.js";
import type { BaseContext } from "koa";
import type { ComponentArgument, TableData } from "@sealcode/jdd";
import { ComponentArguments, Enum, Image, List, Structured, Table } from "@sealcode/jdd";
import { ComponentInputStructured } from "./component-input-structured.js";
import type { StatefulPage } from "@sealcode/sealgen";
import { is, predicates } from "@sealcode/ts-predicates";
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { Readable } from "node:stream";
import { ComponentInputList } from "./component-input-list.js";
import type { JDDPageState } from "./jdd-page.js";
import { ComponentInputEnum } from "./component-input-enum.js";
import { ComponentInputImage } from "./component-input-image.js";
import { ComponentInputList } from "./component-input-list.js";
import { ComponentInputStructured } from "./component-input-structured.js";
import { ComponentInputTable } from "./component-input-table.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import { printArgPath } from "./print-arg-path.js";
import { TempstreamJSX } from "tempstream";
import type { FilePointer } from "@sealcode/file-manager";
import { is, predicates } from "@sealcode/ts-predicates";
export const actionName = "Components";
const absoluteUrlPattern = "http(s?)(://)((www.)?)(([^.]+).)?([a-zA-z0-9-_]+)";
export function ComponentInput<State extends ComponentPreviewState, T>({
export function ComponentInput<State extends JDDPageState, T>({
ctx,
state,
arg_path,
@ -30,8 +31,8 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
arg_path: string[];
arg: ComponentArgument<T>;
value: T;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
}) {
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Readable | Promise<Readable> {
if (value === undefined) {
value = arg.getEmptyValue();
}
@ -106,7 +107,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
{arg_path.at(-1) || ""}
{argType == "markdown" ? (
<textarea
name={`$.component_args${printArgPath(arg_path)}`}
name={`$${printArgPath(arg_path)}`}
onblur={page.rerender()}
cols="40"
>
@ -115,7 +116,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
) : (
<input
type={inputType}
name={`$.component_args${printArgPath(arg_path)}`}
name={`$${printArgPath(arg_path)}`}
value={is(value, predicates.string) ? value : ""}
size="40"
pattern={isUrlAbsolute ? absoluteUrlPattern : undefined}

View File

@ -1,9 +1,10 @@
import type { ComponentArgument, List, Table, TableData } from "@sealcode/jdd";
import type { Registry, TableData } from "@sealcode/jdd";
import { List, Table } from "@sealcode/jdd";
import type { BaseContext } from "koa";
import { isTableData, isTableRegularRow } from "@sealcode/jdd";
import objectPath from "object-path";
import { registry } from "../../jdd-components/components.js";
import type { ComponentPreviewState } from "../components.sreact.js";
import type { JDDPageState } from "./jdd-page.js";
import { registry } from "../../jdd-components/registry.js";
import { makeJDDContext } from "../../jdd-context.js";
function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Array<T> {
@ -12,109 +13,152 @@ function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Ar
return array;
}
export function getComponentData(
state: JDDPageState,
arg_path: string[],
registry: Registry
) {
const component_index = parseInt(arg_path[1]);
const component_args = state.components[component_index].args;
const component_name = state.components[component_index].component_name;
const component = registry.get(component_name);
const arg_path_within_component = arg_path.slice(3); // remove "components" and the index of the component and "args"
const [argument, , argument_value] = component?.getArgumentAtPath(
arg_path_within_component,
component_args
) || [null, null, null];
return {
component_index,
component_args,
component_name,
component,
argument,
argument_value,
arg_path_within_component,
};
}
export const ComponentPreviewActions = <const>{
add_array_item: async (
ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[]
) => {
const component_args = state.component_args;
const component = registry.get(state.component);
const {
component_name,
component,
argument,
arg_path_within_component,
argument_value,
} = getComponentData(state, arg_path, registry);
if (!component) {
console.error("unknown component: ", state.component);
console.error("unknown component: ", component_name);
return state;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argument = component.getArgumentAtPath(
arg_path,
state.component_args
) as List<ComponentArgument<unknown>> | null;
if (!argument) {
console.error("Didn't find a list argument at this path", arg_path);
console.error(
"Didn't find a list argument at this path",
arg_path_within_component
);
return state;
}
if (!(argument instanceof List)) {
throw new Error(
`Expected argument in path ${arg_path.join(
"."
)} to be an instance of List`
);
}
objectPath.insert(
component_args,
state,
arg_path,
await argument.item_type.getExampleValue(makeJDDContext(ctx)),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
((objectPath.get(component_args, arg_path) as unknown[]) || []).length
Array.isArray(argument_value) ? argument_value.length : 0
);
return {
...state,
component_args,
};
return state;
},
remove_array_item: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
index_to_remove: number
) => {
const component_args = state.component_args;
objectPath.del(component_args, [...arg_path, index_to_remove]);
return {
...state,
component_args,
};
): JDDPageState => {
objectPath.del(state, [...arg_path, index_to_remove]);
return state;
},
change_component: async (
ctx: BaseContext,
state: ComponentPreviewState,
inputs: Record<string, string>
) => {
state: JDDPageState,
inputs: Record<string, unknown>
): Promise<JDDPageState> => {
const component_name = inputs.component;
if (!component_name || typeof component_name !== "string") {
throw new Error(
"Missing input: 'component' for action change_component. It should contain the name of the new component type"
);
}
const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown or disallowed component name: ${component_name}`);
}
return {
...state,
component: component_name,
component_args:
(await component?.getExampleValues(makeJDDContext(ctx))) || {},
components: [
{
component_name: component_name,
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
},
],
};
},
randomize_args: async (
ctx: BaseContext,
state: ComponentPreviewState,
inputs: Record<string, string>
) => {
const component_name = inputs.component;
const component = registry.get(component_name);
state: JDDPageState,
_inputs: Record<string, string>,
component_index_str: string
): Promise<JDDPageState> => {
const { component_index, component } = getComponentData(
state,
["components", component_index_str],
registry
);
state.components[component_index].args =
(await component?.getExampleValues(makeJDDContext(ctx))) || {};
return {
...state,
component_args:
(await component?.getExampleValues(makeJDDContext(ctx))) || {},
};
},
add_table_row: async (
ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
columns: number,
type: "header" | "row" = "row"
) => {
const jdd_context = makeJDDContext(ctx);
const component_args = state.component_args;
const { component_args, argument } = getComponentData(state, arg_path, registry);
let row;
const component = registry.get(state.component);
if (!component) {
console.error("Unknown component: ", state.component);
return state;
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const argument = component.getArgumentAtPath(
arg_path,
state.component_args
) as Table<unknown, unknown> | null;
if (!argument) {
console.error("Unknown component at path", arg_path);
return state;
}
if (!(argument instanceof Table)) {
throw new Error(
`Expected argument at path ${arg_path.join(".")} to be of type Table`
);
}
if (type == "header") {
row = {
type: "header",
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
header_content: await argument.header_type.getExampleValue(jdd_context),
};
} else {
@ -126,42 +170,28 @@ export const ComponentPreviewActions = <const>{
row = { type: "row", cells };
}
objectPath.insert(
component_args,
state,
[...arg_path, "rows"],
row,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || [])
.length
);
const result = {
...state,
component_args,
};
return result;
return state;
},
add_table_column: async (
ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[]
) => {
const jdd_context = makeJDDContext(ctx);
const component_args = state.component_args;
const component = registry.get(state.component);
if (!component) {
console.error("Unknown component: ", state.component);
return state;
}
const argument = component.getArgumentAtPath(arg_path, state.component_args);
const { argument } = getComponentData(state, arg_path, registry);
if (!argument) {
console.error("Unknown component at path", arg_path);
return state;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get(
component_args,
arg_path
);
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
@ -170,29 +200,22 @@ export const ComponentPreviewActions = <const>{
const row = tableData.rows[i];
if (isTableRegularRow(row)) {
// eslint-disable-next-line no-await-in-loop
row.cells.push(await argument.getExampleValue(jdd_context));
row.cells.push(await argument.getExampleValue(makeJDDContext(ctx)));
}
}
objectPath.set(component_args, arg_path, tableData);
return {
...state,
component_args,
};
objectPath.set(state, arg_path, tableData);
return state;
},
remove_table_column: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
column_index_to_remove: number
) => {
const component_args = state.component_args;
): JDDPageState => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get(
component_args,
arg_path
);
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
@ -203,84 +226,135 @@ export const ComponentPreviewActions = <const>{
row.cells = row.cells.filter((_, i) => i != column_index_to_remove);
}
}
objectPath.set(component_args, arg_path, tableData);
return {
...state,
component_args,
};
objectPath.set(state, arg_path, tableData);
return state;
},
remove_table_row: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
row_index: number
) => {
const component_args = state.component_args;
objectPath.del(component_args, [...arg_path, "rows", row_index]);
const result = {
...state,
component_args,
};
return result;
): JDDPageState => {
objectPath.del(state, [...arg_path, "rows", row_index]);
return state;
},
move_table_column_right: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
column_index: number
) => {
const component_args = state.component_args;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const data = objectPath.get(component_args, arg_path) as TableData<
unknown,
unknown
>;
const { component_args } = getComponentData(state, arg_path, registry);
const last_path_element = arg_path.at(-1);
if (!last_path_element) {
throw new Error("arg path is empty");
}
const data = objectPath.get<unknown>(component_args, last_path_element, "");
if (!isTableData(data)) {
throw new Error("Expected arg value for a table to be properly shaped");
}
for (const row of data.rows) {
if (row.type == "row") {
moveElement(row.cells, column_index, column_index + 1);
}
}
objectPath.set(component_args, [...arg_path, "rows"], data.rows);
const result = {
...state,
component_args,
};
return result;
objectPath.set(state, [...arg_path, "rows"], data.rows);
return state;
},
move_table_row_down: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
row_index: number
) => {
const component_args = state.component_args;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const data = objectPath.get(component_args, arg_path) as TableData<
unknown,
unknown
>;
const { component_args } = getComponentData(state, arg_path, registry);
const last_path_element = arg_path.at(-1);
if (!last_path_element) {
throw new Error("arg path is empty");
}
const data = objectPath.get<unknown>(component_args, last_path_element, "");
if (!isTableData(data)) {
throw new Error("Expected arg value for a table to be properly shaped");
}
moveElement(data.rows, row_index, row_index + 1);
objectPath.set(component_args, [...arg_path, "rows"], data.rows);
const result = {
...state,
component_args,
};
return result;
objectPath.set(state, [...arg_path, "rows"], data.rows);
return state;
},
change_size: (
_ctx: BaseContext,
state: ComponentPreviewState,
state: JDDPageState,
inputs: Record<string, string>
) => {
return {
...state,
...inputs,
current_size: inputs.size,
preview_size: inputs.size,
};
},
add_component: async (
ctx: BaseContext,
state: JDDPageState,
inputs: Record<string, string>
): Promise<JDDPageState> => {
const component_name = inputs.component;
const component = registry.get(component_name);
return {
...state,
components: [
...state.components,
{
component_name: component_name,
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
},
],
};
},
remove_component: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComponentState = [...state.components];
newComponentState.splice(component_index, 1);
return {
...state,
components: newComponentState,
};
},
move_component_up: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComps = [...state.components];
// prettier-ignore
[newComps[component_index], newComps[component_index - 1]] =
[newComps[component_index - 1], newComps[component_index]];
return { ...state, components: newComps };
},
move_component_down: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComps = [...state.components];
// prettier-ignore
[newComps[component_index], newComps[component_index + 1]] =
[newComps[component_index + 1], newComps[component_index]];
return { ...state, components: newComps };
},
};

View File

@ -0,0 +1,105 @@
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import JDDPage from "./jdd-page.js";
import move_row_down_icon from "./table-move-row-down.svg";
import move_row_up_icon from "./table-move-row-up.svg";
export default abstract class JDDCreator extends JDDPage {
actions = ComponentPreviewActions;
/**
* This method returns list of components allowed in JDD Editor instance.
* If list is empty it will allow all of the components in registry,
* if you overide this function you can decide on what components should
* available.
*/
getAllowedComponents(): string[] {
return [];
}
getRegistryCompoments() {
const all_components = super.getRegistryCompoments();
const allowed_components = this.getAllowedComponents();
if (allowed_components.length > 0) {
return Object.fromEntries(
Object.entries(all_components).filter(([name]) =>
allowed_components.includes(name)
)
);
}
return all_components;
}
renderParameterButtons(state: JDDPageState) {
{
/*The below button has to be here in order for it to be the default behavior */
}
return (
<div>
<input type="submit" value="Preview" />
<select name="component">
{Object.keys(this.getRegistryCompoments()).map((cmp) => (
<option value={cmp}>{cmp}</option>
))}
</select>
{this.makeActionButton(state, {
action: "add_component",
label: "Add component",
})}
</div>
);
}
renderComponentBlock(
ctx: BaseContext,
state: JDDPageState,
component: {
component_name: string;
args: Record<string, unknown>;
},
component_index: number
) {
return (
<fieldset>
<legend>Component - {component.component_name}</legend>
{this.makeActionButton(
state,
{ action: "remove_component", label: "❌" },
component_index
)}
{this.makeActionButton(
state,
{
action: "move_component_up",
label: "Move this row up",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_up_icon.url}"
/>`,
},
component_index
)}
{this.makeActionButton(
state,
{
action: "move_component_down",
label: "Move this row down",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_down_icon.url}"
/>`,
},
component_index
)}
{super.renderComponentBlock(ctx, state, component, component_index)}
</fieldset>
);
}
}

View File

@ -0,0 +1,298 @@
import type { Component, RawJDDocument } from "@sealcode/jdd";
import { documentContainerFromParsed } from "@sealcode/jdd";
import { render, renderEarlyAssets } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
import type { BaseContext } from "koa";
import type { FlatTemplatable, Templatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import html, { defaultHead } from "../../html.js";
import { registry } from "../../jdd-components/components.js";
import { makeJDDContext } from "../../jdd-context.js";
import { ComponentInput } from "./component-input.js";
import { ComponentPreviewActions } from "./component-preview-actions.js";
export const actionName = "Components";
export type JDDPageState = {
components: RawJDDocument;
preview_size?: string;
};
export default abstract class JDDPage extends StatefulPage<
JDDPageState,
typeof ComponentPreviewActions
> {
actions = ComponentPreviewActions;
previewSizes = ["320", "600", "800", "1024", "1300", "1920"];
getRegistryCompoments() {
return registry.getAll();
}
async getInitialState(ctx: BaseContext) {
const [component_name, component] = Object.entries(
this.getRegistryCompoments()
)[0];
const initial_state = {
components: [
{
component_name: component_name,
args: await component.getExampleValues(makeJDDContext(ctx)),
},
],
};
return initial_state;
}
wrapInLayout(
ctx: BaseContext,
content: Templatable,
state: JDDPageState
): Templatable {
return html(
ctx,
"Components",
content,
{
morphing: true,
preserveScroll: true,
autoRefreshCSS: true,
navbar: () => ``,
},
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}`
);
}
async preprocessOverrides(
_ctx: BaseContext,
state: JDDPageState,
overrides: Record<string, unknown>
) {
const jdd_context = makeJDDContext(_ctx);
if (
!hasFieldOfType(
"components",
overrides,
predicates.array(
predicates.shape({
args: predicates.object,
})
)
)
) {
return {};
}
for (const [component_index, { component_name }] of Object.entries(
state.components
)) {
const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown component: ${component_name}`);
}
const overrides_for_component =
overrides.components[parseInt(component_index)];
const promises = Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => {
const value = overrides_for_component.args[arg_name];
if (value) {
const new_value = await arg.receivedToParsed(jdd_context, value);
overrides_for_component.args[arg_name] = new_value;
}
}
);
// eslint-disable-next-line no-await-in-loop
await Promise.all(promises);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return overrides;
}
// eslint-disable-next-line no-unused-vars
abstract renderParameterButtons(_state: JDDPageState): FlatTemplatable;
renderComponentArgs<C extends Component>(
ctx: BaseContext,
state: JDDPageState,
component: C,
args: Record<string, unknown>,
index: number
) {
const jdd_context = makeJDDContext(ctx);
return (
<fieldset class="component-preview-parameters">
<legend>Parameters</legend>
{Object.entries(component.getArguments()).map(async ([arg_name, arg]) => (
<ComponentInput
{...{
state,
arg_path: ["components", index.toString(), "args", arg_name],
ctx,
arg,
value:
args[arg_name] === undefined
? await arg.getExampleValue(jdd_context)
: args[arg_name],
onblur: this.rerender(),
page: this,
}}
/>
))}
<input type="submit" value="Preview" />
</fieldset>
);
}
renderComponentBlock(
ctx: BaseContext,
state: JDDPageState,
{
component_name,
args: component_args,
}: {
component_name: string;
args: Record<string, unknown>;
},
component_index: number
) {
const component = registry.get(component_name);
if (!component) {
return null;
}
return this.renderComponentArgs(
ctx,
state,
component,
component_args,
component_index
);
}
async serializeState(ctx: BaseContext, state: JDDPageState) {
const serialized_components = await Promise.all(
state.components.map(async ({ component_name, args }) => {
const component = registry.get(component_name);
const single_result = {
component_name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
args: component
? await component.convertParsedToStorage(
makeJDDContext(ctx),
args
)
: {},
};
return single_result;
})
);
const serialized_state = JSON.stringify({ components: serialized_components });
return serialized_state;
}
async deserializeState(ctx: BaseContext, state_string: string) {
const jdd_context = makeJDDContext(ctx);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const raw = JSON.parse(state_string);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const components_storage = raw.components;
if (!Array.isArray(components_storage)) {
throw new Error(
"'components' key is not an array, got ${components_storage}"
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const components_parsed = await Promise.all(
components_storage.map(async (entry) => {
if (
!hasShape(
{
component_name: predicates.string,
args: predicates.object,
},
entry
)
) {
throw new Error(
`Expected components[] items to be objects with 'component_name' and 'args' keys, got ${entry}`
);
}
const { component_name, args } = entry;
const component = registry.get(component_name);
if (!component) {
throw new Error("Unknown component: ${component_name}");
}
return {
component_name,
args: await component.convertStorageToParsed(jdd_context, args),
};
})
);
const result = { components: components_parsed };
return result;
}
render(ctx: BaseContext, state: JDDPageState) {
return (
<div
class="two-column"
id="component-debugger"
style="--resizable-column-width: 50vw"
data-controller="component-debugger"
>
<div class="component-arguments">
{this.renderParameterButtons(state)}
{state.components.map((component, component_index) =>
this.renderComponentBlock(ctx, state, component, component_index)
)}
<code>{this.serializeState(ctx, state)}</code>
</div>
<div class="resize-gutter" data-component-debugger-target="gutter"></div>
<div class="component-preview" data-component-debugger-target="preview">
<fieldset>
<legend>
Preview{" "}
<span data-component-debugger-target="component-width"></span>
<select
name="size"
autocomplete="off"
class="component-preview-size-select"
data-component-debugger-target="size-select"
data-action="change->component-debugger#handleWidthDropdown"
>
{this.previewSizes.map((size) => (
<option
value={size}
selected={size === (state.preview_size || "800")}
>
{`${size} px`}
</option>
))}
</select>
<noscript>
{this.makeActionButton(state, "change_size")}
</noscript>
</legend>
{render(
registry,
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}
</fieldset>
{
/* HTML */ `<script>
(function () {
const gutter = document.querySelector(".resize-gutter");
})();
</script>`
}
</div>
</div>
);
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="800"
viewBox="0 0 14 14"
role="img"
focusable="false"
aria-hidden="true"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<path
d="M 14,13.474609 C 14,13.76453 13.831094,14 13.621094,14 H 0.378906 C 0.168806,14 0,13.76453 0,13.474609 V 8.6835938 C 0,8.3935724 0.168806,8.1601563 0.378906,8.1601562 h 13.242188 c 0.21,0 0.378906,0.2334162 0.378906,0.5234376 z M 12.941406,12.791016 V 9.3710938 H 9.7265626 v 3.4199222 z m -4.2832028,0 V 9.3710938 H 5.4433594 v 3.4199222 z m -4.2832031,0 V 9.3710938 H 1.158203 v 3.4199222 z"
id="path1" />
<path
fill="green"
d="m 2.968353,4.030706 q 0,0.2639283 0.1915,0.4656736 l 0.3882,0.3880946 q 0.1966,0.1964467 0.4709002,0.1964467 0.2795,0 0.4658,-0.1964467 l 1.5217,-1.5159882 v 3.642611 q 0,0.269027 0.194,0.4370814 0.1941,0.1682543 0.4684,0.1682543 h 0.6625 q 0.2743,0 0.4684,-0.1682543 0.1941,-0.1680544 0.1941,-0.4370814 V 3.368486 l 1.5216,1.5159882 q 0.1863,0.1964467 0.4658,0.1964467 0.2794998,0 0.4657998,-0.1964467 l 0.3882,-0.3880946 q 0.1966,-0.1966467 0.1966,-0.4656736 0,-0.274126 -0.1966,-0.470872 L 7.4659532,0.191548 Q 7.2847532,0 7.0001532,0 q -0.2795,0 -0.471,0.191548 L 3.159853,3.559834 q -0.1915,0.201845 -0.1915,0.470872 z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,34 +1,52 @@
import { documentContainerFromParsed, render, renderEarlyAssets } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { hasShape, predicates } from "@sealcode/ts-predicates";
import { documentContainerFromParsed, renderEarlyAssets } from "@sealcode/jdd";
import type { BaseContext } from "koa";
import type { Templatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import type { Stringifiable } from "tempstream/@types/stringify.js";
import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js";
import { makeJDDContext } from "../jdd-context.js";
import { ComponentInput } from "./component-preview/component-input.js";
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
import type { JDDPageState } from "./component-preview/jdd-page.js";
import JDDPage from "./component-preview/jdd-page.js";
export const actionName = "Components";
export type ComponentPreviewState = {
component: string;
component_args: Record<string, unknown>;
current_size?: string;
};
export default new (class ComponentsPage extends StatefulPage<
ComponentPreviewState,
typeof ComponentPreviewActions
> {
export default new (class JddcomponentDebuggerPage extends JDDPage {
renderParameterButtons(state: JDDPageState): Stringifiable {
const all_components = super.getRegistryCompoments();
return (
<div>
<input type="submit" value="Preview" />
<select
name="component"
onchange={this.makeActionCallback("change_component")}
autocomplete="off"
>
{Object.entries(all_components).map(([name]) => (
<option
value={name}
selected={name == state.components[0].component_name}
>
{name}
</option>
))}
</select>
{this.makeActionButton(state, "randomize_args", "0")}
</div>
);
}
actions = ComponentPreviewActions;
async getInitialState(ctx: BaseContext) {
const [component_name, component] = Object.entries(registry.getAll())[0];
const initial_state = {
component: component_name,
component_args: await component.getExampleValues(makeJDDContext(ctx)),
components: [
{
component_name: component_name,
args: await component.getExampleValues(makeJDDContext(ctx)),
},
],
};
return initial_state;
}
@ -73,7 +91,7 @@ export default new (class ComponentsPage extends StatefulPage<
wrapInLayout(
ctx: BaseContext,
content: Templatable,
state: ComponentPreviewState
state: JDDPageState
): Templatable {
return html(
ctx,
@ -88,147 +106,11 @@ export default new (class ComponentsPage extends StatefulPage<
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed([
{
component_name: state.component,
args: state.component_args,
},
]),
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}`
);
}
async preprocessOverrides(
ctx: BaseContext,
state: ComponentPreviewState,
overrides: Record<string, unknown>
) {
const jdd_context = makeJDDContext(ctx);
const component_name = state.component;
if (!component_name) {
throw new Error("Unspecified component name");
}
const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown component: ${component_name}`);
}
if (!hasShape({ component_args: predicates.object }, overrides)) {
return overrides;
}
const promises = Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => {
const value = overrides.component_args[arg_name];
if (value) {
const new_value = await arg.receivedToParsed(jdd_context, value);
overrides.component_args[arg_name] = new_value;
}
}
);
await Promise.all(promises);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return overrides;
}
containerSizes = ["320", "600", "800", "1024", "1300", "1920"];
render(ctx: BaseContext, state: ComponentPreviewState) {
const jdd_context = makeJDDContext(ctx);
const all_components = registry.getAll();
const component =
registry.get(state.component) || Object.values(all_components)[0];
return (
<div
class="two-column"
id="component-debugger"
style="--resizable-column-width: 50vw"
data-controller="component-debugger"
>
<div class="component-arguments">
{/*The below button has to be here in order for it to be the default behavior */}
<input type="submit" value="Preview" />
<select
name="component"
onchange={this.makeActionCallback("change_component")}
autocomplete="off"
>
{Object.entries(all_components).map(([name]) => (
<option value={name} selected={name == state.component}>
{name}
</option>
))}
</select>
{this.makeActionButton(state, "randomize_args")}
<fieldset class="component-preview-parameters">
<legend>Parameters</legend>
{Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => (
<ComponentInput
{...{
state,
ctx,
arg_path: [arg_name],
arg,
value:
state.component_args[arg_name] === undefined
? await arg.getExampleValue(jdd_context)
: state.component_args[arg_name],
onblur: this.rerender(),
page: this,
}}
/>
)
)}
<input type="submit" value="Preview" />
</fieldset>
<code>{this.serializeState(ctx, state)}</code>
</div>
<div class="resize-gutter" data-component-debugger-target="gutter"></div>
<div class="component-preview" data-component-debugger-target="preview">
<fieldset>
<legend>
Preview{" "}
<span data-component-debugger-target="component-width"></span>
<select
name="size"
autocomplete="off"
class="component-preview-size-select"
data-component-debugger-target="size-select"
data-action="change->component-debugger#handleWidthDropdown"
>
{this.containerSizes.map((size) => (
<option
value={size}
selected={size === (state.current_size || "800")}
>
{`${size} px`}
</option>
))}
</select>
<noscript>
{this.makeActionButton(state, "change_size")}
</noscript>
</legend>
{render(
registry,
documentContainerFromParsed([
{
component_name: state.component,
args: state.component_args,
},
]),
jdd_context
)}
</fieldset>
{
/* HTML */ `<script>
(function () {
const gutter = document.querySelector(".resize-gutter");
})();
</script>`
}
</div>
</div>
);
}
})();

View File

@ -0,0 +1,10 @@
import JDDCreator from "./component-preview/jdd-creator.js";
export const actionName = "JDDPreview";
export default new (class JDDCreatePreviewPage extends JDDCreator {
// uncomment to create whitelist of allowed components
// getAllowedComponents() {
// return ["nice-box"];
// }
})();

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "node16",
"module": "es2022",
"moduleResolution": "node16",
"noImplicitAny": true,
"noImplicitThis": true,