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", project: "./tsconfig-back.json",
}, },
rules: { rules: {
"no-unused-vars": [2, { varsIgnorePattern: "TempstreamJSX" }],
"@typescript-eslint/no-unused-vars": [2, { varsIgnorePattern: "TempstreamJSX" }], "@typescript-eslint/no-unused-vars": [2, { varsIgnorePattern: "TempstreamJSX" }],
"no-unused-vars": 0, "no-unused-vars": 0,
"@typescript-eslint/require-await": 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", "@koa/router": "^12.0.1",
"@playwright/test": "^1.36.1", "@playwright/test": "^1.36.1",
"@sealcode/file-manager": "^1.0.2", "@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.4.7", "@sealcode/jdd": "^0.4.10",
"@sealcode/sealgen": "^0.15.15", "@sealcode/sealgen": "^0.15.16",
"@sealcode/ts-predicates": "^0.6.2", "@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0", "@types/kill-port": "^2.0.0",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",

View File

@ -1,9 +1,9 @@
import type { Context } from "koa"; import type { Context } from "koa";
import { TempstreamJSX } from "tempstream"; import { tempstream, TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen"; import { Page } from "@sealcode/sealgen";
import html from "../html.js"; import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.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 { shuffle } from "../util.js";
import { makeJDDContext } from "../jdd-context.js"; import { makeJDDContext } from "../jdd-context.js";
@ -33,7 +33,14 @@ export default new (class AllComponentsPage extends Page {
"AllComponents", "AllComponents",
<div> <div>
{render(registry, documentContainerFromParsed(document), jdd_context)} {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> <div>
<label> <label>
{arg_path.at(-1) || ""} {arg_path.at(-1) || ""}
<select <select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
name={`$.component_args${printArgPath(arg_path)}`}
onchange={onchange}
>
{arg.values.map((v: S) => ( {arg.values.map((v: S) => (
<option value={v} selected={value == v}> <option value={v} selected={value == v}>
{v} {v}

View File

@ -1,15 +1,15 @@
import type { Image } from "@sealcode/jdd";
import type { BaseContext } from "koa"; import type { BaseContext } from "koa";
import type { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream"; 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 { 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 { printArgPath } from "./print-arg-path.js";
import { htmlEscape } from "escape-goat"; 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_path,
arg, arg,
value, value,
@ -19,9 +19,9 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
arg_path: string[]; arg_path: string[];
arg: Image; arg: Image;
value: FilePointer | null; value: FilePointer | null;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>; page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
ctx: BaseContext; ctx: BaseContext;
}) { }): JSX.Element {
const jdd_context = makeJDDContext(ctx); const jdd_context = makeJDDContext(ctx);
return ( return (
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
@ -38,7 +38,7 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
})} })}
<input <input
type="file" type="file"
name={`$.component_args${printArgPath(arg_path)}.new`} name={`$${printArgPath(arg_path)}.new`}
value="" value=""
autocomplete="off" autocomplete="off"
data-action="change->input-image-preview#handleChange" data-action="change->input-image-preview#handleChange"
@ -48,7 +48,7 @@ export function ComponentInputImage<State extends ComponentPreviewState>({
<div> <div>
<input <input
type="hidden" type="hidden"
name={`$.component_args${printArgPath(arg_path)}.old`} name={`$${printArgPath(arg_path)}.old`}
value={htmlEscape(value?.token || "")} value={htmlEscape(value?.token || "")}
autocomplete="off" 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 type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream"; 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 { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js"; import type { ComponentPreviewActions } from "./component-preview-actions.js";
export async function ComponentInputList< export async function ComponentInputList<
State extends ComponentPreviewState, State extends JDDPageState,
T extends ComponentArgument<unknown> T extends ComponentArgument<unknown>
>({ >({
state, state,
@ -20,10 +21,11 @@ export async function ComponentInputList<
state: State; state: State;
ctx: BaseContext; ctx: BaseContext;
arg_path: string[]; arg_path: string[];
arg: List<T>; // eslint-disable-next-line @typescript-eslint/no-explicit-any
arg: List<T, any>;
value: T[]; value: T[];
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>; page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}) { }): Promise<Readable> {
if (!value) { if (!value) {
value = []; value = [];
} }

View File

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

View File

@ -3,10 +3,9 @@ import { isTableHeader } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen"; import type { StatefulPage } from "@sealcode/sealgen";
import type { BaseContext } from "koa"; import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream"; import { TempstreamJSX } from "tempstream";
import type { ComponentPreviewState } from "../components.sreact.js";
import { ComponentInput } from "./component-input.js"; import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.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_column_right_icon from "./table-add-column-right.svg";
import add_row_below_icon from "./table-add-row-below.svg"; import add_row_below_icon from "./table-add-row-below.svg";
import add_column_header_icon from "./table-add-row-header-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"; import move_row_down_icon from "./table-move-row-down.svg";
export async function ComponentInputTable< export async function ComponentInputTable<
State extends ComponentPreviewState, State extends JDDPageState,
CellType, CellType,
HeaderType HeaderType
>({ >({
@ -32,8 +31,8 @@ export async function ComponentInputTable<
arg_path: string[]; arg_path: string[];
arg: Table<CellType, HeaderType>; arg: Table<CellType, HeaderType>;
value: TableData<CellType, HeaderType>; value: TableData<CellType, HeaderType>;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>; page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}) { }): Promise<import("stream").Readable> {
if (!value) { if (!value) {
value = arg.getEmptyValue(); 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 type { ComponentArgument, TableData } from "@sealcode/jdd";
import { ComponentArguments, Enum, Image, List, Structured, Table } 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 type { StatefulPage } from "@sealcode/sealgen";
import { is, predicates } from "@sealcode/ts-predicates"; import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { BaseContext } from "koa"; import type { Readable } from "node:stream";
import { TempstreamJSX } from "tempstream"; import { ComponentInputList } from "./component-input-list.js";
import type { ComponentPreviewState } from "../components.sreact.js"; import type { JDDPageState } from "./jdd-page.js";
import { ComponentInputEnum } from "./component-input-enum.js"; import { ComponentInputEnum } from "./component-input-enum.js";
import { ComponentInputImage } from "./component-input-image.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 { ComponentInputTable } from "./component-input-table.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js"; import { TempstreamJSX } from "tempstream";
import { printArgPath } from "./print-arg-path.js"; import type { FilePointer } from "@sealcode/file-manager";
import { is, predicates } from "@sealcode/ts-predicates";
export const actionName = "Components"; export const actionName = "Components";
const absoluteUrlPattern = "http(s?)(://)((www.)?)(([^.]+).)?([a-zA-z0-9-_]+)"; const absoluteUrlPattern = "http(s?)(://)((www.)?)(([^.]+).)?([a-zA-z0-9-_]+)";
export function ComponentInput<State extends ComponentPreviewState, T>({ export function ComponentInput<State extends JDDPageState, T>({
ctx, ctx,
state, state,
arg_path, arg_path,
@ -30,8 +31,8 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
arg_path: string[]; arg_path: string[];
arg: ComponentArgument<T>; arg: ComponentArgument<T>;
value: T; value: T;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>; page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}) { }): Readable | Promise<Readable> {
if (value === undefined) { if (value === undefined) {
value = arg.getEmptyValue(); value = arg.getEmptyValue();
} }
@ -106,7 +107,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
{arg_path.at(-1) || ""} {arg_path.at(-1) || ""}
{argType == "markdown" ? ( {argType == "markdown" ? (
<textarea <textarea
name={`$.component_args${printArgPath(arg_path)}`} name={`$${printArgPath(arg_path)}`}
onblur={page.rerender()} onblur={page.rerender()}
cols="40" cols="40"
> >
@ -115,7 +116,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
) : ( ) : (
<input <input
type={inputType} type={inputType}
name={`$.component_args${printArgPath(arg_path)}`} name={`$${printArgPath(arg_path)}`}
value={is(value, predicates.string) ? value : ""} value={is(value, predicates.string) ? value : ""}
size="40" size="40"
pattern={isUrlAbsolute ? absoluteUrlPattern : undefined} 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 type { BaseContext } from "koa";
import { isTableData, isTableRegularRow } from "@sealcode/jdd"; import { isTableData, isTableRegularRow } from "@sealcode/jdd";
import objectPath from "object-path"; import objectPath from "object-path";
import { registry } from "../../jdd-components/components.js"; import type { JDDPageState } from "./jdd-page.js";
import type { ComponentPreviewState } from "../components.sreact.js"; import { registry } from "../../jdd-components/registry.js";
import { makeJDDContext } from "../../jdd-context.js"; import { makeJDDContext } from "../../jdd-context.js";
function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Array<T> { 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; 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>{ export const ComponentPreviewActions = <const>{
add_array_item: async ( add_array_item: async (
ctx: BaseContext, ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[] arg_path: string[]
) => { ) => {
const component_args = state.component_args; const {
const component = registry.get(state.component); component_name,
component,
argument,
arg_path_within_component,
argument_value,
} = getComponentData(state, arg_path, registry);
if (!component) { if (!component) {
console.error("unknown component: ", state.component); console.error("unknown component: ", component_name);
return state; 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) { 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; return state;
} }
if (!(argument instanceof List)) {
throw new Error(
`Expected argument in path ${arg_path.join(
"."
)} to be an instance of List`
);
}
objectPath.insert( objectPath.insert(
component_args, state,
arg_path, arg_path,
await argument.item_type.getExampleValue(makeJDDContext(ctx)), await argument.item_type.getExampleValue(makeJDDContext(ctx)),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // 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 { return state;
...state,
component_args,
};
}, },
remove_array_item: ( remove_array_item: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
index_to_remove: number index_to_remove: number
) => { ): JDDPageState => {
const component_args = state.component_args; objectPath.del(state, [...arg_path, index_to_remove]);
objectPath.del(component_args, [...arg_path, index_to_remove]); return state;
return {
...state,
component_args,
};
}, },
change_component: async ( change_component: async (
ctx: BaseContext, ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
inputs: Record<string, string> inputs: Record<string, unknown>
) => { ): Promise<JDDPageState> => {
const component_name = inputs.component; 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); const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown or disallowed component name: ${component_name}`);
}
return { return {
...state, components: [
component: component_name, {
component_args: component_name: component_name,
(await component?.getExampleValues(makeJDDContext(ctx))) || {}, args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
},
],
}; };
}, },
randomize_args: async ( randomize_args: async (
ctx: BaseContext, ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
inputs: Record<string, string> _inputs: Record<string, string>,
) => { component_index_str: string
const component_name = inputs.component; ): Promise<JDDPageState> => {
const component = registry.get(component_name); const { component_index, component } = getComponentData(
state,
["components", component_index_str],
registry
);
state.components[component_index].args =
(await component?.getExampleValues(makeJDDContext(ctx))) || {};
return { return {
...state, ...state,
component_args:
(await component?.getExampleValues(makeJDDContext(ctx))) || {},
}; };
}, },
add_table_row: async ( add_table_row: async (
ctx: BaseContext, ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
columns: number, columns: number,
type: "header" | "row" = "row" type: "header" | "row" = "row"
) => { ) => {
const jdd_context = makeJDDContext(ctx); const jdd_context = makeJDDContext(ctx);
const component_args = state.component_args; const { component_args, argument } = getComponentData(state, arg_path, registry);
let row; 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) { if (!argument) {
console.error("Unknown component at path", arg_path); console.error("Unknown component at path", arg_path);
return state; return state;
} }
if (!(argument instanceof Table)) {
throw new Error(
`Expected argument at path ${arg_path.join(".")} to be of type Table`
);
}
if (type == "header") { if (type == "header") {
row = { row = {
type: "header", type: "header",
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
header_content: await argument.header_type.getExampleValue(jdd_context), header_content: await argument.header_type.getExampleValue(jdd_context),
}; };
} else { } else {
@ -126,42 +170,28 @@ export const ComponentPreviewActions = <const>{
row = { type: "row", cells }; row = { type: "row", cells };
} }
objectPath.insert( objectPath.insert(
component_args, state,
[...arg_path, "rows"], [...arg_path, "rows"],
row, row,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || []) ((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || [])
.length .length
); );
const result = { return state;
...state,
component_args,
};
return result;
}, },
add_table_column: async ( add_table_column: async (
ctx: BaseContext, ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[] arg_path: string[]
) => { ) => {
const jdd_context = makeJDDContext(ctx); const { argument } = getComponentData(state, arg_path, registry);
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);
if (!argument) { if (!argument) {
console.error("Unknown component at path", arg_path); console.error("Unknown component at path", arg_path);
return state; return state;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get( const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
component_args,
arg_path
);
if (!isTableData(tableData)) { if (!isTableData(tableData)) {
throw new Error("wrong table data"); throw new Error("wrong table data");
} }
@ -170,29 +200,22 @@ export const ComponentPreviewActions = <const>{
const row = tableData.rows[i]; const row = tableData.rows[i];
if (isTableRegularRow(row)) { if (isTableRegularRow(row)) {
// eslint-disable-next-line no-await-in-loop // 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); objectPath.set(state, arg_path, tableData);
return { return state;
...state,
component_args,
};
}, },
remove_table_column: ( remove_table_column: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
column_index_to_remove: number column_index_to_remove: number
) => { ): JDDPageState => {
const component_args = state.component_args;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get( const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
component_args,
arg_path
);
if (!isTableData(tableData)) { if (!isTableData(tableData)) {
throw new Error("wrong table data"); 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); row.cells = row.cells.filter((_, i) => i != column_index_to_remove);
} }
} }
objectPath.set(component_args, arg_path, tableData); objectPath.set(state, arg_path, tableData);
return { return state;
...state,
component_args,
};
}, },
remove_table_row: ( remove_table_row: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
row_index: number row_index: number
) => { ): JDDPageState => {
const component_args = state.component_args; objectPath.del(state, [...arg_path, "rows", row_index]);
objectPath.del(component_args, [...arg_path, "rows", row_index]); return state;
const result = {
...state,
component_args,
};
return result;
}, },
move_table_column_right: ( move_table_column_right: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
column_index: number column_index: number
) => { ) => {
const component_args = state.component_args; const { component_args } = getComponentData(state, arg_path, registry);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const data = objectPath.get(component_args, arg_path) as TableData< const last_path_element = arg_path.at(-1);
unknown, if (!last_path_element) {
unknown 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) { for (const row of data.rows) {
if (row.type == "row") { if (row.type == "row") {
moveElement(row.cells, column_index, column_index + 1); moveElement(row.cells, column_index, column_index + 1);
} }
} }
objectPath.set(component_args, [...arg_path, "rows"], data.rows); objectPath.set(state, [...arg_path, "rows"], data.rows);
const result = { return state;
...state,
component_args,
};
return result;
}, },
move_table_row_down: ( move_table_row_down: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
_: Record<string, string>, _: Record<string, string>,
arg_path: string[], arg_path: string[],
row_index: number row_index: number
) => { ) => {
const component_args = state.component_args; const { component_args } = getComponentData(state, arg_path, registry);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions const last_path_element = arg_path.at(-1);
const data = objectPath.get(component_args, arg_path) as TableData< if (!last_path_element) {
unknown, throw new Error("arg path is empty");
unknown }
>; 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); moveElement(data.rows, row_index, row_index + 1);
objectPath.set(component_args, [...arg_path, "rows"], data.rows); objectPath.set(state, [...arg_path, "rows"], data.rows);
const result = { return state;
...state,
component_args,
};
return result;
}, },
change_size: ( change_size: (
_ctx: BaseContext, _ctx: BaseContext,
state: ComponentPreviewState, state: JDDPageState,
inputs: Record<string, string> inputs: Record<string, string>
) => { ) => {
return { return {
...state, ...state,
...inputs, preview_size: inputs.size,
current_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 { documentContainerFromParsed, renderEarlyAssets } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { hasShape, predicates } from "@sealcode/ts-predicates";
import type { BaseContext } from "koa"; import type { BaseContext } from "koa";
import type { Templatable } from "tempstream"; import type { Templatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream"; import { tempstream, TempstreamJSX } from "tempstream";
import type { Stringifiable } from "tempstream/@types/stringify.js";
import html, { defaultHead } from "../html.js"; import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js"; import { registry } from "../jdd-components/components.js";
import { makeJDDContext } from "../jdd-context.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 { 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 const actionName = "Components";
export type ComponentPreviewState = { export default new (class JddcomponentDebuggerPage extends JDDPage {
component: string; renderParameterButtons(state: JDDPageState): Stringifiable {
component_args: Record<string, unknown>; const all_components = super.getRegistryCompoments();
current_size?: string; return (
}; <div>
<input type="submit" value="Preview" />
export default new (class ComponentsPage extends StatefulPage< <select
ComponentPreviewState, name="component"
typeof ComponentPreviewActions 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; actions = ComponentPreviewActions;
async getInitialState(ctx: BaseContext) { async getInitialState(ctx: BaseContext) {
const [component_name, component] = Object.entries(registry.getAll())[0]; const [component_name, component] = Object.entries(registry.getAll())[0];
const initial_state = { const initial_state = {
component: component_name, components: [
component_args: await component.getExampleValues(makeJDDContext(ctx)), {
component_name: component_name,
args: await component.getExampleValues(makeJDDContext(ctx)),
},
],
}; };
return initial_state; return initial_state;
} }
@ -73,7 +91,7 @@ export default new (class ComponentsPage extends StatefulPage<
wrapInLayout( wrapInLayout(
ctx: BaseContext, ctx: BaseContext,
content: Templatable, content: Templatable,
state: ComponentPreviewState state: JDDPageState
): Templatable { ): Templatable {
return html( return html(
ctx, ctx,
@ -88,147 +106,11 @@ export default new (class ComponentsPage extends StatefulPage<
(...args) => (...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets( tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry, registry,
documentContainerFromParsed([ documentContainerFromParsed(state.components),
{
component_name: state.component,
args: state.component_args,
},
]),
makeJDDContext(ctx) 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"]; 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": { "compilerOptions": {
"module": "node16", "module": "es2022",
"moduleResolution": "node16", "moduleResolution": "node16",
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitThis": true, "noImplicitThis": true,