Some more type fixes around images
This commit is contained in:
parent
be4d4e3168
commit
318f8df49f
493
package-lock.json
generated
493
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -6,7 +6,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "docker-compose up -d db && node .",
|
"start": "docker-compose up -d db && node .",
|
||||||
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/back",
|
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2021,dom -p src/back",
|
||||||
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/front",
|
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/front",
|
||||||
"build": "sealgen build",
|
"build": "sealgen build",
|
||||||
"watch": "multiple-scripts-tmux -p watch",
|
"watch": "multiple-scripts-tmux -p watch",
|
||||||
@ -35,19 +35,20 @@
|
|||||||
"@hotwired/turbo": "^8.0.2",
|
"@hotwired/turbo": "^8.0.2",
|
||||||
"@koa/router": "^12.0.1",
|
"@koa/router": "^12.0.1",
|
||||||
"@playwright/test": "^1.36.1",
|
"@playwright/test": "^1.36.1",
|
||||||
"@sealcode/jdd": "^0.2.18",
|
"@sealcode/jdd": "^0.3.0",
|
||||||
"@sealcode/sealgen": "^0.12.13",
|
"@sealcode/sealgen": "^0.13.0",
|
||||||
"@sealcode/ts-predicates": "^0.4.3",
|
"@sealcode/ts-predicates": "^0.4.3",
|
||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"get-port": "^7.0.0",
|
"get-port": "^7.0.0",
|
||||||
"js-convert-case": "^4.2.0",
|
"js-convert-case": "^4.2.0",
|
||||||
|
"koa-responsive-image-router": "^0.2.18",
|
||||||
"locreq": "^3.0.0",
|
"locreq": "^3.0.0",
|
||||||
"multiple-scripts-tmux": "^1.0.4",
|
"multiple-scripts-tmux": "^1.0.4",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"object-path": "^0.11.8",
|
"object-path": "^0.11.8",
|
||||||
"sealious": "^0.17.48",
|
"sealious": "^0.17.48",
|
||||||
"stimulus": "^2.0.0",
|
"stimulus": "^2.0.0",
|
||||||
"tempstream": "^0.3.12",
|
"tempstream": "^0.3.15",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"vitest": "^1.1.0"
|
"vitest": "^1.1.0"
|
||||||
},
|
},
|
||||||
@ -57,6 +58,7 @@
|
|||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.8.4",
|
||||||
"@types/object-path": "^0.11.4",
|
"@types/object-path": "^0.11.4",
|
||||||
"@types/tedious": "^4.0.7",
|
"@types/tedious": "^4.0.7",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
"@typescript-eslint/eslint-plugin": "^5.10.0",
|
||||||
"@typescript-eslint/parser": "^5.10.2",
|
"@typescript-eslint/parser": "^5.10.2",
|
||||||
"@vitest/coverage-istanbul": "^1.1.0",
|
"@vitest/coverage-istanbul": "^1.1.0",
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import _locreq from "locreq";
|
||||||
|
import { module_dirname } from "./util.js";
|
||||||
|
const locreq = _locreq(module_dirname(import.meta.url));
|
||||||
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
|
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
|
||||||
export const PORT = process.env.SEALIOUS_PORT
|
export const PORT = process.env.SEALIOUS_PORT
|
||||||
? parseInt(process.env.SEALIOUS_PORT)
|
? parseInt(process.env.SEALIOUS_PORT)
|
||||||
@ -16,3 +19,8 @@ export const MAILCATCHER_API_PORT = parseInt(
|
|||||||
);
|
);
|
||||||
export const MAILER = process.env.SEALIOUS_MAILER;
|
export const MAILER = process.env.SEALIOUS_MAILER;
|
||||||
export const DEFAULT_HTML_LANG = "pl";
|
export const DEFAULT_HTML_LANG = "pl";
|
||||||
|
|
||||||
|
export const IMAGE_CACHE_FS_DIR =
|
||||||
|
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/images");
|
||||||
|
export const SMARTCROP_CACHE_FS_DIR =
|
||||||
|
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/smartcrop");
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import kill from "kill-port";
|
import kill from "kill-port";
|
||||||
|
import type { KoaResponsiveImageRouter } from "koa-responsive-image-router";
|
||||||
import _locreq from "locreq";
|
import _locreq from "locreq";
|
||||||
|
import { FlatTemplatable } from "tempstream";
|
||||||
import TheApp from "./app.js";
|
import TheApp from "./app.js";
|
||||||
import { PORT, SEALIOUS_SANITY } from "./config.js";
|
import { PORT, SEALIOUS_SANITY } from "./config.js";
|
||||||
import { mainRouter } from "./routes/index.js";
|
import { mainRouter } from "./routes/index.js";
|
||||||
|
@ -3,5 +3,8 @@ import { Registry } from "@sealcode/jdd";
|
|||||||
|
|
||||||
export const registry = new Registry();
|
export const registry = new Registry();
|
||||||
|
|
||||||
|
import { ImageDemo } from "./image-demo/image-demo.jdd.js";
|
||||||
|
registry.add("image-demo", ImageDemo);
|
||||||
|
|
||||||
import { NiceBox } from "./nice-box/nice-box.jdd.js";
|
import { NiceBox } from "./nice-box/nice-box.jdd.js";
|
||||||
registry.add("nice-box", NiceBox);
|
registry.add("nice-box", NiceBox);
|
||||||
|
@ -29,8 +29,7 @@ export class ImageDemo extends Component<typeof component_arguments> {
|
|||||||
return (
|
return (
|
||||||
<div class="image-demo">
|
<div class="image-demo">
|
||||||
<h2>Image with alt text</h2>
|
<h2>Image with alt text</h2>
|
||||||
{" " ||
|
{render_image(image_with_alt.image, {
|
||||||
render_image(image_with_alt.image, {
|
|
||||||
container: { width: 200, height: 200 },
|
container: { width: 200, height: 200 },
|
||||||
alt: image_with_alt.alt,
|
alt: image_with_alt.alt,
|
||||||
})}
|
})}
|
||||||
@ -39,7 +38,11 @@ export class ImageDemo extends Component<typeof component_arguments> {
|
|||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
{multiple_images.map((image) =>
|
{multiple_images.map((image) =>
|
||||||
render_image(image, {
|
render_image(image, {
|
||||||
container: { width: 200, height: 200, objectFit: "cover" },
|
container: {
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
objectFit: "cover",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,10 +5,17 @@ import {
|
|||||||
ExtractStructuredComponentArgumentsValues,
|
ExtractStructuredComponentArgumentsValues,
|
||||||
JDDContext,
|
JDDContext,
|
||||||
} from "@sealcode/jdd";
|
} from "@sealcode/jdd";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
const component_arguments = {
|
const component_arguments = {
|
||||||
title: new ComponentArguments.ShortText(),
|
title: new ComponentArguments.ShortText(),
|
||||||
content: new ComponentArguments.Markdown(),
|
content: new ComponentArguments.Markdown(),
|
||||||
|
images: new ComponentArguments.List(
|
||||||
|
new ComponentArguments.Structured({
|
||||||
|
image: new ComponentArguments.Image(),
|
||||||
|
alt: new ComponentArguments.ShortText(),
|
||||||
|
})
|
||||||
|
),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export class NiceBox extends Component<typeof component_arguments> {
|
export class NiceBox extends Component<typeof component_arguments> {
|
||||||
@ -16,17 +23,28 @@ export class NiceBox extends Component<typeof component_arguments> {
|
|||||||
return component_arguments;
|
return component_arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML(
|
async toHTML(
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
|
images,
|
||||||
}: ExtractStructuredComponentArgumentsValues<typeof component_arguments>,
|
}: ExtractStructuredComponentArgumentsValues<typeof component_arguments>,
|
||||||
{ render_markdown }: JDDContext
|
{ render_markdown, decode_file, render_image }: JDDContext
|
||||||
): FlatTemplatable {
|
): Promise<Readable> {
|
||||||
return (
|
return (
|
||||||
<div class="nice-box">
|
<div class="nice-box">
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
<div>{render_markdown(content)}</div>
|
<div>{render_markdown(content)}</div>
|
||||||
|
{images.map((image) =>
|
||||||
|
render_image(image.image, {
|
||||||
|
container: {
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
objectFit: "contain",
|
||||||
|
},
|
||||||
|
alt: image?.alt || "",
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,82 +1,157 @@
|
|||||||
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 {
|
import {
|
||||||
ComponentArgument,
|
ComponentArgument,
|
||||||
Enum,
|
|
||||||
List,
|
List,
|
||||||
render,
|
render,
|
||||||
simpleJDDContext,
|
simpleJDDContext,
|
||||||
Structured,
|
Structured,
|
||||||
} from "@sealcode/jdd";
|
} from "@sealcode/jdd";
|
||||||
import objectPath from "object-path";
|
import { StateAndMetadata, StatefulPage, to_base64 } from "@sealcode/sealgen";
|
||||||
|
import { hasFieldOfType, hasShape, is, predicates } from "@sealcode/ts-predicates";
|
||||||
|
import { BaseContext } from "koa";
|
||||||
|
import { Templatable, TempstreamJSX } from "tempstream";
|
||||||
|
import html from "../html.js";
|
||||||
|
import { registry } from "../jdd-components/components.js";
|
||||||
|
import { ComponentInput } from "./component-preview/component-input.js";
|
||||||
|
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
|
||||||
|
import { jdd_context } from "./jdd-context.js";
|
||||||
|
|
||||||
export const actionName = "Components";
|
export const actionName = "Components";
|
||||||
|
|
||||||
const actions = {
|
function id<X>(_: any, __: any, x: X): X {
|
||||||
add_array_item: (
|
return x;
|
||||||
state: State,
|
}
|
||||||
_: Record<string, string>,
|
|
||||||
arg_path: string[],
|
async function encodeSealiousFile(maybe_file: Record<string, unknown>) {
|
||||||
empty_value: unknown
|
if (maybe_file?.getDataPath) {
|
||||||
) => {
|
return simpleJDDContext.encode_file(
|
||||||
const args = state.args;
|
{
|
||||||
objectPath.insert(
|
type: "path",
|
||||||
args,
|
// asserting that this is an instance of sealious' FileFromPath
|
||||||
arg_path,
|
path: (maybe_file as unknown as { data: { path: string } }).data
|
||||||
empty_value,
|
.path as string,
|
||||||
((objectPath.get(args, arg_path) as unknown[]) || []).length
|
},
|
||||||
|
false
|
||||||
);
|
);
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
change_component: (state: State, inputs: Record<string, string>) => {
|
|
||||||
const component_name = inputs.component;
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
component: component_name,
|
|
||||||
args: component?.getExampleValues() || {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
randomize_args: (state: State, inputs: Record<string, string>) => {
|
|
||||||
const component_name = inputs.component;
|
|
||||||
const component = registry.get(component_name);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
args: component?.getExampleValues() || {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type State = {
|
const componentArgToRequestProcessor = {
|
||||||
|
list: async function (arg, arg_name, value: unknown) {
|
||||||
|
if (
|
||||||
|
!is(value, predicates.array(predicates.object)) &&
|
||||||
|
!is(value, predicates.object)
|
||||||
|
) {
|
||||||
|
throw new Error(`$.${arg_name} is not a list or object`);
|
||||||
|
}
|
||||||
|
const values = Array.isArray(value) ? value : Object.values(value);
|
||||||
|
const nested_arg_type = (arg as List<ComponentArgument<unknown>>).item_type;
|
||||||
|
let array_result: Array<unknown> = await Promise.all(
|
||||||
|
values.map((value, index) => {
|
||||||
|
return (
|
||||||
|
componentArgToRequestProcessor[nested_arg_type.getTypeName()] || id
|
||||||
|
)(nested_arg_type, `${arg_name}[${index}]`, value);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (nested_arg_type.getTypeName() != "list") {
|
||||||
|
array_result = array_result.flat();
|
||||||
|
}
|
||||||
|
return array_result;
|
||||||
|
},
|
||||||
|
structured: async function (arg, arg_name, value) {
|
||||||
|
if (!is(value, predicates.object)) {
|
||||||
|
throw new Error(`${arg_name} is not an object`);
|
||||||
|
}
|
||||||
|
let result = Object.fromEntries(
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(value).map(async ([obj_key, obj_value]) => {
|
||||||
|
const nested_arg_type: ComponentArgument<unknown> = (
|
||||||
|
arg as Structured<Record<string, ComponentArgument<unknown>>>
|
||||||
|
).structure[obj_key];
|
||||||
|
if (!nested_arg_type) {
|
||||||
|
return [obj_key, null];
|
||||||
|
}
|
||||||
|
const new_value = await (
|
||||||
|
componentArgToRequestProcessor[nested_arg_type.getTypeName()] ||
|
||||||
|
id
|
||||||
|
)(arg, `${arg_name}[${obj_key}]`, obj_value);
|
||||||
|
return [obj_key, new_value];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// if we're in a list and any of the values return an array, we will multiply the object
|
||||||
|
if (arg.hasParent("list")) {
|
||||||
|
const keys_with_unexpected_arrays = Object.entries(result)
|
||||||
|
.filter(([key, value]) => {
|
||||||
|
const nested_arg_type: ComponentArgument<unknown> = (
|
||||||
|
arg as Structured<Record<string, ComponentArgument<unknown>>>
|
||||||
|
).structure[key];
|
||||||
|
return (
|
||||||
|
nested_arg_type.getTypeName() !== "list" && Array.isArray(value)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(([key]) => key);
|
||||||
|
|
||||||
|
if (keys_with_unexpected_arrays.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
"Multiplying on multiple fields at the same time is not implemented yet"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (keys_with_unexpected_arrays.length == 1) {
|
||||||
|
const key = keys_with_unexpected_arrays[0];
|
||||||
|
const old_result = result;
|
||||||
|
result = (old_result[key] as Array<unknown>).map((value) => ({
|
||||||
|
...old_result,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
image: async function (arg, _, value: unknown) {
|
||||||
|
if (
|
||||||
|
!hasShape(
|
||||||
|
{
|
||||||
|
new: predicates.maybe(predicates.array(predicates.object)),
|
||||||
|
old: predicates.string,
|
||||||
|
},
|
||||||
|
value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const files = (value.new || []).filter((e) => e);
|
||||||
|
if (files.length == 0) {
|
||||||
|
return value.old;
|
||||||
|
} else if (files.length == 1) {
|
||||||
|
return encodeSealiousFile(files[0]);
|
||||||
|
} else if (arg.hasParent("list")) {
|
||||||
|
return Promise.all(files.map(encodeSealiousFile));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as Record<
|
||||||
|
string,
|
||||||
|
(arg: ComponentArgument<any>, arg_name: string, value: unknown) => Promise<unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ComponentPreviewState = {
|
||||||
component: string;
|
component: string;
|
||||||
args: Record<string, unknown>;
|
component_args: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default new (class ComponentsPage extends StatefulPage<State, typeof actions> {
|
export default new (class ComponentsPage extends StatefulPage<
|
||||||
actions = actions;
|
ComponentPreviewState,
|
||||||
|
typeof ComponentPreviewActions
|
||||||
|
> {
|
||||||
|
actions = ComponentPreviewActions;
|
||||||
|
|
||||||
getInitialState() {
|
async getInitialState() {
|
||||||
const [component_name, component] = Object.entries(registry.getAll())[0];
|
const [component_name, component] = Object.entries(registry.getAll())[0];
|
||||||
return { component: component_name, args: component.getExampleValues() };
|
const initial_state = {
|
||||||
|
component: component_name,
|
||||||
|
component_args: await component.getExampleValues(jdd_context),
|
||||||
|
};
|
||||||
|
return initial_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
|
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
|
||||||
@ -88,149 +163,68 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderListArgument<T>(
|
wrapInForm(state: ComponentPreviewState, content: Templatable): Templatable {
|
||||||
state: State,
|
// overwriting this method in order to add enctype to form
|
||||||
arg_path: string[],
|
|
||||||
arg: List<ComponentArgument<T>>,
|
|
||||||
value: T[] = []
|
|
||||||
): FlatTemplatable {
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<form action="./" method="POST" enctype="multipart/form-data">
|
||||||
<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.getExampleValue()
|
|
||||||
)}
|
|
||||||
</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)}`}
|
|
||||||
onchange={this.rerender()}
|
|
||||||
>
|
|
||||||
{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.rerender()}
|
|
||||||
cols="70"
|
|
||||||
>
|
|
||||||
{value as string}
|
|
||||||
</textarea>
|
|
||||||
) : (
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
name="state"
|
||||||
name={`$.args${this.printArgPath(arg_path)}`}
|
type="hidden"
|
||||||
value={value as string}
|
value={to_base64(JSON.stringify(state))}
|
||||||
size="70"
|
|
||||||
/>
|
/>
|
||||||
)}
|
{content}
|
||||||
</label>
|
</form>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(ctx: BaseContext, state: State, inputs: Record<string, string>) {
|
async preprocessRequestBody<
|
||||||
|
T extends StateAndMetadata<ComponentPreviewState, typeof ComponentPreviewActions>
|
||||||
|
>(values: Record<string, unknown>): Promise<T> {
|
||||||
|
let old_component = hasFieldOfType(values, "component", predicates.string)
|
||||||
|
? values.component
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const new_component = hasShape(
|
||||||
|
{ $: predicates.shape({ component: predicates.string }) },
|
||||||
|
values
|
||||||
|
)
|
||||||
|
? values.$.component
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const component_name = new_component || old_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(
|
||||||
|
{ $: predicates.shape({ component_args: predicates.object }) },
|
||||||
|
values
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// no component args to overwrite
|
||||||
|
return values as T;
|
||||||
|
}
|
||||||
|
for (const [arg_name, arg] of Object.entries(component.getArguments())) {
|
||||||
|
let value = values.$.component_args[arg_name];
|
||||||
|
if (value) {
|
||||||
|
const new_value = await (
|
||||||
|
componentArgToRequestProcessor[arg.getTypeName()] || id
|
||||||
|
)(arg, arg_name, value);
|
||||||
|
values.$.component_args[arg_name] = new_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
ctx: BaseContext,
|
||||||
|
state: ComponentPreviewState,
|
||||||
|
inputs: Record<string, string>
|
||||||
|
) {
|
||||||
const all_components = registry.getAll();
|
const all_components = registry.getAll();
|
||||||
const component =
|
const component =
|
||||||
registry.get(state.component) || Object.values(all_components)[0];
|
registry.get(state.component) || Object.values(all_components)[0];
|
||||||
@ -253,19 +247,26 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
|||||||
{this.makeActionButton(state, "randomize_args")}
|
{this.makeActionButton(state, "randomize_args")}
|
||||||
<fieldset class="component-preview-parameters">
|
<fieldset class="component-preview-parameters">
|
||||||
<legend>Parameters</legend>
|
<legend>Parameters</legend>
|
||||||
{Object.entries(component.getArguments()).map(([arg_name, arg]) =>
|
{Object.entries(component.getArguments()).map(
|
||||||
this.renderArgumentInput(
|
async ([arg_name, arg]) => (
|
||||||
|
<ComponentInput
|
||||||
|
{...{
|
||||||
state,
|
state,
|
||||||
[arg_name],
|
arg_path: [arg_name],
|
||||||
arg,
|
arg,
|
||||||
state.args[arg_name] === undefined
|
value:
|
||||||
? arg.getExampleValue()
|
state.component_args[arg_name] === undefined
|
||||||
: state.args[arg_name]
|
? await arg.getExampleValue(jdd_context)
|
||||||
|
: state.component_args[arg_name],
|
||||||
|
onblur: this.rerender(),
|
||||||
|
page: this,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<input type="submit" value="Preview" />
|
<input type="submit" value="Preview" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div>{JSON.stringify(state)}</div>
|
<code>{JSON.stringify(state)}</code>
|
||||||
</div>
|
</div>
|
||||||
<div class="resize-gutter"></div>
|
<div class="resize-gutter"></div>
|
||||||
{
|
{
|
||||||
@ -307,18 +308,30 @@ export default new (class ComponentsPage extends StatefulPage<State, typeof acti
|
|||||||
<legend>Preview</legend>
|
<legend>Preview</legend>
|
||||||
{render(
|
{render(
|
||||||
registry,
|
registry,
|
||||||
[{ component_name: state.component, args: state.args }],
|
[
|
||||||
simpleJDDContext
|
{
|
||||||
|
component_name: state.component,
|
||||||
|
args: state.component_args,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
jdd_context
|
||||||
)}
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
/* HTML */ `<script>
|
/* HTML */ `<script>
|
||||||
document.documentElement.addEventListener("ts-rebuilt", () => {
|
const main_form = document
|
||||||
document
|
|
||||||
.querySelector("#component-debugger")
|
.querySelector("#component-debugger")
|
||||||
.closest("form")
|
.closest("form");
|
||||||
.requestSubmit();
|
document.documentElement.addEventListener("ts-rebuilt", () => {
|
||||||
|
main_form.requestSubmit();
|
||||||
|
});
|
||||||
|
main_form.addEventListener("turbo:submit-end", () => {
|
||||||
|
// this clears the values of file inputs, so they don't get unecessarily
|
||||||
|
// re-uploaded on future submissions - the file is alreade there on the server
|
||||||
|
main_form
|
||||||
|
.querySelectorAll("input[type=file]")
|
||||||
|
.forEach((input) => (input.value = ""));
|
||||||
});
|
});
|
||||||
</script>`
|
</script>`
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Router from "@koa/router";
|
import Router from "@koa/router";
|
||||||
import { Middlewares } from "sealious";
|
import { Middlewares } from "sealious";
|
||||||
|
import { imageRouter, RESPONSIVE_IMAGES_URL_PATH } from "../image-router.js";
|
||||||
import { MainView } from "./common/main-view.js";
|
import { MainView } from "./common/main-view.js";
|
||||||
import mountAutoRoutes from "./routes.js";
|
import mountAutoRoutes from "./routes.js";
|
||||||
|
|
||||||
@ -15,5 +16,7 @@ export const mainRouter = (router: Router): void => {
|
|||||||
ctx.body = { status: ctx.$app.status, started_at };
|
ctx.body = { status: ctx.$app.status, started_at };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.use(RESPONSIVE_IMAGES_URL_PATH, imageRouter.getRoutes());
|
||||||
|
|
||||||
mountAutoRoutes(router);
|
mountAutoRoutes(router);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"target": "ES2019",
|
"target": "ES2019",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["es6", "esnext"],
|
"lib": ["es2021"],
|
||||||
"outDir": "../../dist/back",
|
"outDir": "../../dist/back",
|
||||||
"keyofStringsOnly": true,
|
"keyofStringsOnly": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import * as Turbo from "@hotwired/turbo";
|
import * as Turbo from "@hotwired/turbo";
|
||||||
import { Application } from "stimulus";
|
import { Application } from "stimulus";
|
||||||
|
import InputImagePreview from "./controllers/input-image-preview";
|
||||||
import TaskController from "./controllers/task-controller";
|
import TaskController from "./controllers/task-controller";
|
||||||
|
|
||||||
export { Turbo };
|
export { Turbo };
|
||||||
|
|
||||||
const application = Application.start();
|
const application = Application.start();
|
||||||
application.register("task", TaskController);
|
application.register("task", TaskController);
|
||||||
|
application.register("input-image-preview", InputImagePreview);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-css-includes */
|
/* DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-css-includes */
|
||||||
|
|
||||||
@import "../node_modules/@sealcode/sealgen/src/forms/forms.css";
|
@import "../node_modules/@sealcode/sealgen/src/forms/forms.css";
|
||||||
|
@import "back/jdd-components/image-demo/image-demo.css";
|
||||||
@import "back/routes/common/ui/input.css";
|
@import "back/routes/common/ui/input.css";
|
||||||
@import "back/routes/components.css";
|
@import "back/routes/components.css";
|
||||||
@import "colors.css";
|
@import "colors.css";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user