Support for tables UI

This commit is contained in:
Kuba Orlik 2024-03-24 21:33:34 +01:00
parent 3e1a4a2365
commit f49b49e1ca
20 changed files with 1921 additions and 440 deletions

1273
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,8 +35,8 @@
"@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.3.0", "@sealcode/jdd": "^0.3.1",
"@sealcode/sealgen": "^0.13.0", "@sealcode/sealgen": "^0.14.3",
"@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",
@ -46,7 +46,8 @@
"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", "qs": "^6.12.0",
"sealious": "^0.18.1",
"stimulus": "^2.0.0", "stimulus": "^2.0.0",
"tempstream": "^0.3.15", "tempstream": "^0.3.15",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",

View File

@ -1,5 +1,5 @@
import _locreq from "locreq"; import _locreq from "locreq";
import Sealious, { App, LoggerMailer, SMTPMailer } from "sealious"; import { App, LoggerMailer, SMTPMailer, Context as SealiousContext } from "sealious";
import type { LoggerLevel } from "sealious/@types/src/app/logger.js"; import type { LoggerLevel } from "sealious/@types/src/app/logger.js";
import { collections } from "./collections/collections.js"; import { collections } from "./collections/collections.js";
import { import {
@ -17,7 +17,7 @@ const locreq = _locreq(module_dirname(import.meta.url));
declare module "koa" { declare module "koa" {
interface BaseContext { interface BaseContext {
$context: Sealious.Context; $context: SealiousContext;
$app: TheApp; $app: TheApp;
} }
} }

View File

@ -3,11 +3,14 @@ import { Registry } from "@sealcode/jdd";
export const registry = new Registry(); export const registry = new Registry();
import { MapWithPins } from "./map-with-pins/map-with-pins.jdd.js";
registry.add("map-with-pins", MapWithPins);
import { ImageDemo } from "./image-demo/image-demo.jdd.js"; import { ImageDemo } from "./image-demo/image-demo.jdd.js";
registry.add("image-demo", ImageDemo); registry.add("image-demo", ImageDemo);
import { MapWithPins } from "./map-with-pins/map-with-pins.jdd.js";
registry.add("map-with-pins", MapWithPins);
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);
import { Table } from "./table/table.jdd.js";
registry.add("table", Table);

View File

@ -0,0 +1,2 @@
.table {
}

View File

@ -0,0 +1,63 @@
import { FlatTemplatable, TempstreamJSX } from "tempstream";
import {
Component,
ComponentArguments,
ExtractStructuredComponentArgumentsValues,
isTableHeader,
JDDContext,
} from "@sealcode/jdd";
const component_arguments = {
table: new ComponentArguments.Table(
new ComponentArguments.ShortText(),
new ComponentArguments.Structured({
color: new ComponentArguments.Enum(["red", "blue", "green", "aquamarine"]),
word: new ComponentArguments.ShortText().setExampleValues([
"apple",
"banana",
"pineapple",
"carrot",
]),
})
),
} as const;
export class Table extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
toHTML({
table,
}: ExtractStructuredComponentArgumentsValues<
typeof component_arguments
>): FlatTemplatable {
return (
<div class="table">
<table>
<tbody>
{table.rows.map((row) =>
isTableHeader(row) ? (
<tr>
<th
colspan={this.getArguments()
.table.getColumnsCount(table)
.toString()}
>
{row.header_content}
</th>
</tr>
) : (
<tr>
{row.cells.map(({ color, word }) => (
<td style={`color: ${color}`}>{word}</td>
))}
</tr>
)
)}
</tbody>
</table>
</div>
);
}
}

View File

@ -15,7 +15,7 @@ export async function ComponentInputList<State extends ComponentPreviewState, T>
}: { }: {
state: State; state: State;
arg_path: string[]; arg_path: string[];
arg: List<ComponentArgument<T>>; arg: List<T>;
value: T[]; value: T[];
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>; page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
}) { }) {

View File

@ -0,0 +1,229 @@
import { ComponentArgument, isTableHeader, List, Table, TableData } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream";
import { ComponentPreviewState } from "../components.sreact.js";
import { jdd_context } from "../jdd-context.js";
import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import delete_column_icon from "./table-delete-column.svg";
import add_row_below_icon from "./table-add-row-below.svg";
import add_column_right_icon from "./table-add-column-right.svg";
import delete_row_icon from "./table-delete-row.svg";
import add_column_header_icon from "./table-add-row-header-below.svg";
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,
CellType,
HeaderType
>({
state,
arg_path,
arg,
value,
page,
}: {
state: State;
arg_path: string[];
arg: Table<CellType, HeaderType>;
value: TableData<CellType, HeaderType>;
page: StatefulPage<ComponentPreviewState, typeof ComponentPreviewActions>;
}) {
if (!value) {
value = arg.getEmptyValue();
}
const empty_cell_value = arg.cell_type.getExampleValue(jdd_context);
const empty_header_value = arg.header_type.getExampleValue(jdd_context);
return (
<fieldset>
<legend>{arg_path.at(-1)}</legend>
<div
style={`max-width: calc(var(--resizable-column-width) - ${
arg_path.length + 3
} * 14px); overflow-x: auto; max-height: 500px; overflow-y: auto;`}
>
<table style="position: relative; /* necessary for sticky th*/">
<tbody>
<tr>
<td></td>
{[...Array(arg.getColumnsCount(value)).keys()].map(
(column_index) => (
<th class="sticky sticky--top subdued">
{page.makeActionButton(
state,
{
action: "remove_table_column",
label: "Remove column",
content: /* HTML */ `<img
width="20"
height="20"
src="${delete_column_icon.url}"
/>`,
},
arg_path,
column_index
)}
{column_index >= arg.getColumnsCount(value) - 1
? ""
: page.makeActionButton(
state,
{
action: "move_table_column_right",
label: "Move column to the right",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_column_right_icon.url}"
/>`,
},
arg_path,
column_index
)}
</th>
)
)}
</tr>
{value.rows.map((row, row_index) => (
<tr>
<td class="sticky sticky--left subdued">
<div style="display: flex; flex-flow: column; row-gap: 5px;">
{page.makeActionButton(
state,
{
action: "remove_table_row",
label: "Remove row",
content: /* HTML */ `<img
width="20"
height="20"
src="${delete_row_icon.url}"
/>`,
},
arg_path,
row_index
)}
{page.makeActionButton(
state,
{
action: "move_table_row_down",
label: "Move this row down",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_down_icon.url}"
/>`,
},
arg_path,
row_index
)}
</div>
</td>
{isTableHeader(row) ? (
<th colspan={arg.getColumnsCount(value).toString()}>
<ComponentInput
{...{
state,
arg_path: [
...arg_path,
"rows",
row_index.toString(),
"header_content",
],
arg: arg.header_type,
value: row.header_content,
page,
}}
/>
</th>
) : (
row.cells.map((cell, cell_index) => (
<td>
<ComponentInput
{...{
state,
arg_path: [
...arg_path,
"rows",
row_index.toString(),
"cells",
cell_index.toString(),
],
arg: arg.cell_type,
value: cell,
page,
}}
/>
</td>
))
)}
{row_index == 0 ? (
<td
class="subdued"
rowspan={value.rows.length.toString()}
>
{page.makeActionButton(
state,
{
action: "add_table_column",
label: "Add column",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_column_right_icon.url}"
/>`,
},
arg_path,
empty_cell_value
)}
</td>
) : (
""
)}
</tr>
))}
<tr>
<td
class="subdued"
colspan={(arg.getColumnsCount(value) + 1).toString()}
>
{page.makeActionButton(
state,
{
action: "add_table_row",
label: "Add table row",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_row_below_icon.url}"
/>`,
},
arg_path,
empty_cell_value,
arg.getColumnsCount(value)
)}
{page.makeActionButton(
state,
{
action: "add_table_row",
label: "Add table header",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_column_header_icon.url}"
/>`,
},
arg_path,
empty_header_value,
arg.getColumnsCount(value),
"header"
)}
</td>
</tr>
</tbody>
</table>
</div>
</fieldset>
);
}

View File

@ -1,4 +1,4 @@
import { Enum, Image, List, Structured } from "@sealcode/jdd"; import { Enum, Image, List, Structured, Table, TableData } from "@sealcode/jdd";
import { ComponentArgument } from "@sealcode/jdd"; import { ComponentArgument } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen"; import { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream"; import { TempstreamJSX } from "tempstream";
@ -7,6 +7,7 @@ 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 { ComponentInputList } from "./component-input-list.js";
import { ComponentInputStructured } from "./component-input-structured.js"; import { ComponentInputStructured } from "./component-input-structured.js";
import { ComponentInputTable } from "./component-input-table.js";
import { printArgPath } from "./print-arg-path.js"; import { printArgPath } from "./print-arg-path.js";
export function ComponentInput<State extends ComponentPreviewState, T>({ export function ComponentInput<State extends ComponentPreviewState, T>({
@ -59,6 +60,16 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
}); });
} }
if (arg instanceof Table) {
return ComponentInputTable({
state,
arg_path,
arg,
value: value as TableData<unknown, unknown>,
page,
});
}
return ( return (
<div> <div>
<label> <label>
@ -67,7 +78,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
<textarea <textarea
name={`$.component_args${printArgPath(arg_path)}`} name={`$.component_args${printArgPath(arg_path)}`}
onblur={page.rerender()} onblur={page.rerender()}
cols="70" cols="40"
> >
{value as string} {value as string}
</textarea> </textarea>
@ -76,7 +87,7 @@ export function ComponentInput<State extends ComponentPreviewState, T>({
type="text" type="text"
name={`$.component_args${printArgPath(arg_path)}`} name={`$.component_args${printArgPath(arg_path)}`}
value={value as string} value={value as string}
size="70" size="40"
/> />
)} )}
</label> </label>

View File

@ -1,8 +1,16 @@
import { isTableData, isTableRegularRow, TableData } from "@sealcode/jdd";
import { is, predicates } from "@sealcode/ts-predicates";
import objectPath from "object-path"; import objectPath from "object-path";
import { registry } from "../../jdd-components/components.js"; import { registry } from "../../jdd-components/components.js";
import type { ComponentPreviewState } from "../components.sreact.js"; import type { ComponentPreviewState } from "../components.sreact.js";
import { jdd_context } from "../jdd-context.js"; import { jdd_context } from "../jdd-context.js";
function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Array<T> {
const element = array.splice(fromIndex, 1)[0];
array.splice(toIndex, 0, element);
return array;
}
export const ComponentPreviewActions = <const>{ export const ComponentPreviewActions = <const>{
add_array_item: ( add_array_item: (
state: ComponentPreviewState, state: ComponentPreviewState,
@ -58,4 +66,140 @@ export const ComponentPreviewActions = <const>{
component_args: (await component?.getExampleValues(jdd_context)) || {}, component_args: (await component?.getExampleValues(jdd_context)) || {},
}; };
}, },
add_table_row: (
state: ComponentPreviewState,
_: Record<string, string>,
arg_path: string[],
empty_value: unknown,
columns: number,
type: "header" | "row" = "row"
) => {
const component_args = state.component_args;
let row;
if (type == "header") {
row = { type: "header", header_content: empty_value };
} else {
const cells = [];
for (let i = 0; i < columns; i++) {
cells.push(empty_value);
}
row = { type: "row", cells };
}
objectPath.insert(
component_args,
[...arg_path, "rows"],
row,
((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || [])
.length
);
const result = {
...state,
component_args,
};
return result;
},
add_table_column: (
state: ComponentPreviewState,
_: Record<string, string>,
arg_path: string[],
empty_value: unknown
) => {
const component_args = state.component_args;
const tableData = objectPath.get(component_args, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
for (const i in tableData.rows) {
const row = tableData.rows[i];
if (isTableRegularRow(row)) {
row.cells.push(empty_value);
}
}
objectPath.set(component_args, arg_path, tableData);
return {
...state,
component_args,
};
},
remove_table_column: (
state: ComponentPreviewState,
_: Record<string, string>,
arg_path: string[],
column_index_to_remove: number
) => {
const component_args = state.component_args;
const tableData = objectPath.get(component_args, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
for (const i in tableData.rows) {
const row = tableData.rows[i];
if (isTableRegularRow(row)) {
row.cells = row.cells.filter((_, i) => i != column_index_to_remove);
}
}
objectPath.set(component_args, arg_path, tableData);
return {
...state,
component_args,
};
},
remove_table_row: (
state: ComponentPreviewState,
_: 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;
},
move_table_column_right: (
state: ComponentPreviewState,
_: Record<string, string>,
arg_path: string[],
column_index: number
) => {
const component_args = state.component_args;
const data = objectPath.get(component_args, arg_path) as TableData<
unknown,
unknown
>;
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;
},
move_table_row_down: (
state: ComponentPreviewState,
_: Record<string, string>,
arg_path: string[],
row_index: number
) => {
const component_args = state.component_args;
const data = objectPath.get(component_args, arg_path) as TableData<
unknown,
unknown
>;
moveElement(data.rows, row_index, row_index + 1);
objectPath.set(component_args, [...arg_path, "rows"], data.rows);
const result = {
...state,
component_args,
};
return result;
},
}; };

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-column-right.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,16 H 16 V 0 H 0 Z m 1.5,-1.5 v -2 h 3 v 2 z m 4.5,0 v -13 h 8.5 v 13 z M 4.5,1.5 v 2.51959 h -3 V 1.5 Z m -3,4.01959 h 3 V 7.5 h -3 z M 1.5,9 h 3 v 2 h -3 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 9.5,8.5 V 11 H 11 V 8.5 h 2.5001 V 7 H 11 V 4.5 H 9.5 V 7 H 7 v 1.5 z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-row-below.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 7.5,9.5 V 7 H 9 v 2.5 h 2.5001 V 11 H 9 v 2.5 H 7.5 V 11 H 5 V 9.5 Z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-row-header-below.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 4.4849442,9.891467 V 7.692536 H 5.8043028 V 9.891467 H 8.003322 v 1.319359 H 5.8043028 v 2.198931 H 4.4849442 V 11.210826 H 2.286013 V 9.891467 Z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573" />
<g
id="g1"
transform="translate(-3.2633543,-4)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 12.442783,13.891467 v -2.198931 h 1.319359 v 2.198931 h 2.199019 v 1.319359 h -2.199019 v 2.198931 h -1.319359 v -2.198931 z"
fill="#1F2328"
id="path2-6"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
sodipodi:nodetypes="ccccccccccc" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 16.758927,13.898931 V 11.7 h -1.319359 v 2.198931 h -2.199019 v 1.319359 h 2.199019 v 2.198931 h 1.319359 V 15.21829 Z"
fill="#1F2328"
id="path2-6-7"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
sodipodi:nodetypes="ccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="566.59668"
height="533.33331"
viewBox="0 0 16.9979 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-delete-column.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 16,12 v 4 H 0 V 0 H 16 V 4 H 14.5 V 1.5 H 11 v 13 h 3.5 V 12 Z M 1.5,12.5 v 2 h 3 v -2 z m 4.5,0 v 2 h 3.5 v -2 z M 9.5,4.01957 V 1.5 H 6 v 2.51958 z m -5,1e-5 V 1.5 h -3 v 2.51959 z m 0,1.5 -3,10e-6 V 7.5 h 3 z M 4.5,9 h -3 v 2 h 3 z M 6,9 v 2 H 9.5 V 9 Z M 6,7.5 H 9.5 V 5.51957 l -3.5,1e-5 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 14.7176,9.0606 15.9373,10.2803 16.9979,9.2197 15.7782,8 16.9979,6.7803 15.9372,5.71967 14.7176,6.9393 13.4979,5.71968 12.4373,6.7803 13.6569,8 l -1.2196,1.2196 1.0606,1.0607 z"
fill="#1F2328"
id="path2"
style="fill:#ff0000;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33667"
viewBox="0 0 15.999999 16.0001"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-delete-row.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 16,0 H 0 V 16 H 4 V 14.5 H 1.5 V 9 h 13 v 5.5 H 12 V 16 h 4 z M 1.5,4 V 1.5 h 3 V 4 Z M 6,4 V 1.5 h 4 L 9.9999,4 Z M 11.4999,4 11.5,1.5 h 3 V 4 Z m 0,1.5 H 14.5 v 2 H 11.4998 Z M 4.5,7.5 h -3 v -2 h 3 z m 1.5,0 v -2 h 3.9999 l -10e-5,2 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 8,14.7804 9.2197,16.0001 10.2803,14.9395 9.0606,13.7197 10.2802,12.5001 9.2196,11.4395 8,12.6591 6.7803,11.4395 5.71968,12.5001 6.9393,13.7197 5.71967,14.9394 6.7803,16.0001 Z"
fill="#1F2328"
id="path2"
style="fill:#ff0000;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,48 @@
<?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="800px"
height="800px"
viewBox="0 0 14 14"
role="img"
focusable="false"
aria-hidden="true"
version="1.1"
id="svg2"
sodipodi:docname="gui-table-col-after-svgrepo-com.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="399.42446"
inkscape:cy="400"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M.52539062.00195313C.23546962.00195313 0 .17085937 0 .38085938V13.623047c0 .2101.23546962.378906.52539062.378906H5.3164062c.2900214 0 .5234375-.168806.5234376-.378906V.38085938c0-.21-.2334162-.37890626-.5234376-.37890626H.52539062zM1.2089844 1.0605469h3.4199218v3.2148437H1.2089844V1.0605469zm0 4.2832031h3.4199218v3.2148438H1.2089844V5.34375zm0 4.2832031h3.4199218V12.84375H1.2089844V9.6269531z"
id="path1" />
<path
fill="green"
d="M9.969294 11.0336q-.2639283 0-.4656736-.1915l-.3880946-.3882q-.1964467-.1966-.1964467-.4709 0-.2795.1964467-.4658l1.5159882-1.5217H6.988903q-.269027 0-.4370814-.194-.1682543-.1941-.1682543-.4684v-.6625q0-.2743.1682543-.4684.1680544-.1941.4370814-.1941h3.642611L9.1155258 4.4865q-.1964467-.1863-.1964467-.4658 0-.2795.1964467-.4658l.3880946-.3882q.1966467-.1966.4656736-.1966.274126 0 .470872.1966l3.368286 3.3693Q14 6.7172 14 7.0018q0 .2795-.191548.471l-3.368286 3.3693q-.201845.1915-.470872.1915z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,48 @@
<?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"
sodipodi:docname="table-move-row-up.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="399.42446"
inkscape:cy="400"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 0,0.525391 C 0,0.23547 0.16890625,0 0.37890626,0 H 13.621094 C 13.831194,0 14,0.23547 14,0.525391 v 4.7910152 c 0,0.2900214 -0.168806,0.5234375 -0.378906,0.5234376 H 0.37890626 C 0.16890626,5.8398438 0,5.6064276 0,5.3164062 Z M 1.0585937,1.208984 V 4.6289062 H 4.2734374 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 8.5566406 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 12.841797 V 1.208984 Z"
id="path1" />
<path
fill="green"
d="m 11.031647,9.969294 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 6.988903 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.642611 l -1.5216,-1.5159882 q -0.1863,-0.1964467 -0.4658,-0.1964467 -0.2795,0 -0.4658,0.1964467 l -0.3882,0.3880946 q -0.1966,0.1966467 -0.1966,0.4656736 0,0.274126 0.1966,0.470872 l 3.3693,3.368286 Q 6.7152468,14 6.9998468,14 q 0.2795,0 0.471,-0.191548 l 3.3693002,-3.368286 q 0.1915,-0.201845 0.1915,-0.470872 z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -29,6 +29,36 @@
.component-preview-parameters { .component-preview-parameters {
fieldset { fieldset {
background-color: #80808024; background-color: #80808024;
table {
td,
th {
outline: 0.5px solid #0000006b;
&.subdued > * {
opacity: 50%;
}
&.subdued:hover > * {
opacity: 100%;
}
&.sticky {
position: sticky;
background-color: #ececec;
}
&.sticky--left {
left: 0;
box-shadow: 5px 0px 10px -4px #00000047;
}
&.sticky--top {
top: 0;
box-shadow: 0px 5px 10px -4px #00000047;
}
}
}
} }
} }

View File

@ -1,12 +1,6 @@
import { import { render } from "@sealcode/jdd";
ComponentArgument,
List,
render,
simpleJDDContext,
Structured,
} from "@sealcode/jdd";
import { StateAndMetadata, StatefulPage, to_base64 } from "@sealcode/sealgen"; import { StateAndMetadata, StatefulPage, to_base64 } from "@sealcode/sealgen";
import { hasFieldOfType, hasShape, is, predicates } from "@sealcode/ts-predicates"; import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
import { BaseContext } from "koa"; import { BaseContext } from "koa";
import { Templatable, TempstreamJSX } from "tempstream"; import { Templatable, TempstreamJSX } from "tempstream";
import html from "../html.js"; import html from "../html.js";
@ -17,149 +11,6 @@ import { jdd_context } from "./jdd-context.js";
export const actionName = "Components"; export const actionName = "Components";
function id<X>(_: unknown, __: unknown, x: X): X {
return x;
}
function isSealiousFile(x: unknown): x is { data: { path: string } } {
return hasShape(
{
getDataPath: predicates.any,
data: predicates.shape({ path: predicates.string }),
},
x
);
}
async function encodeSealiousFile(maybe_file: Record<string, unknown>) {
if (isSealiousFile(maybe_file)) {
return simpleJDDContext.encode_file(
{
type: "path",
// asserting that this is an instance of sealious' FileFromPath
path: maybe_file.data.path,
},
false
);
}
}
const componentArgToRequestProcessor: Record<
string,
(
arg: ComponentArgument<unknown>,
arg_name: string,
value: unknown
) => Promise<unknown>
> = {
list: async function (
arg: List<ComponentArgument<unknown>>,
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.item_type;
let array_result: Array<unknown> = await Promise.all(
values.map(async (value, index) => {
const result = await (
componentArgToRequestProcessor[nested_arg_type.getTypeName()] || id
)(nested_arg_type, `${arg_name}[${index}]`, value);
return result;
})
);
if (nested_arg_type.getTypeName() != "list") {
array_result = array_result.flat();
}
return array_result;
},
structured: async function (
arg: Structured<Record<string, ComponentArgument<unknown>>>,
arg_name,
value
) {
if (!is(value, predicates.object)) {
throw new Error(`${arg_name} is not an object`);
}
const result: Record<string, unknown> = {};
await Promise.all(
Object.entries(value).map(async ([obj_key, obj_value]) => {
const nested_arg_type: ComponentArgument<unknown> =
arg.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);
result[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.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;
const array = old_result[key];
if (!Array.isArray(array)) {
throw new Error("expected an array");
}
return array.map((value: unknown) => ({
...old_result,
[key]: value,
}));
} else {
return result;
}
} else {
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));
}
},
};
export type ComponentPreviewState = { export type ComponentPreviewState = {
component: string; component: string;
component_args: Record<string, unknown>; component_args: Record<string, unknown>;
@ -203,21 +54,11 @@ export default new (class ComponentsPage extends StatefulPage<
); );
} }
async preprocessRequestBody< async preprocessOverrides(
T extends StateAndMetadata<ComponentPreviewState, typeof ComponentPreviewActions> state: ComponentPreviewState,
>(values: Record<string, unknown>): Promise<T> { overrides: Record<string, unknown>
const old_component = hasFieldOfType(values, "component", predicates.string) ) {
? values.component const component_name = state.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) { if (!component_name) {
throw new Error("Unspecified component name"); throw new Error("Unspecified component name");
} }
@ -225,30 +66,25 @@ export default new (class ComponentsPage extends StatefulPage<
if (!component) { if (!component) {
throw new Error(`Unknown component: ${component_name}`); throw new Error(`Unknown component: ${component_name}`);
} }
if ( if (!hasShape({ component_args: predicates.object }, overrides)) {
!hasShape( return overrides;
{ $: predicates.shape({ component_args: predicates.object }) },
values
)
) {
// no component args to overwrite
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return values as T;
} }
const promises = Object.entries(component.getArguments()).map( const promises = Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => { async ([arg_name, arg]) => {
const value = values.$.component_args[arg_name]; const value = overrides.component_args[arg_name];
if (value) { if (value) {
const new_value = await ( const new_value = await arg.parseFormInput(
componentArgToRequestProcessor[arg.getTypeName()] || id jdd_context,
)(arg, arg_name, value); value,
values.$.component_args[arg_name] = new_value; arg_name
);
overrides.component_args[arg_name] = new_value;
} }
} }
); );
await Promise.all(promises); await Promise.all(promises);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return values as T; return overrides;
} }
render(_ctx: BaseContext, state: ComponentPreviewState) { render(_ctx: BaseContext, state: ComponentPreviewState) {
@ -256,7 +92,11 @@ export default new (class ComponentsPage extends StatefulPage<
const component = const component =
registry.get(state.component) || Object.values(all_components)[0]; registry.get(state.component) || Object.values(all_components)[0];
return ( return (
<div class="two-column" id="component-debugger"> <div
class="two-column"
id="component-debugger"
style="--resizable-column-width: 50vw"
>
<div class="resizable"> <div class="resizable">
{/*The below button has to be here in order for it to be the default behavior */} {/*The below button has to be here in order for it to be the default behavior */}
<input type="submit" value="Preview" /> <input type="submit" value="Preview" />
@ -309,10 +149,12 @@ export default new (class ComponentsPage extends StatefulPage<
origin_width + (e.clientX - origin_x), origin_width + (e.clientX - origin_x),
1 1
); );
document.documentElement.style.setProperty( document
"--resizable-column-width", .getElementById("component-debugger")
new_width + "px" .style.setProperty(
); "--resizable-column-width",
new_width + "px"
);
}; };
gutter.addEventListener("mousedown", (e) => { gutter.addEventListener("mousedown", (e) => {
is_resizing = true; is_resizing = true;

View File

@ -3,6 +3,7 @@
@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/jdd-components/image-demo/image-demo.css";
@import "back/jdd-components/map-with-pins/map-with-pins.css"; @import "back/jdd-components/map-with-pins/map-with-pins.css";
@import "back/jdd-components/table/table.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";