Page MenuHomeSealhub

list.ts
No OneTemporary

import { Context } from "koa";
import { Templatable, tempstream, FlatTemplatable } from "tempstream";
import { Page } from "./page.js";
import { ShapeToType } from "@sealcode/ts-predicates";
import { FormControl } from "../forms/controls/controls.js";
import { FormField } from "../forms/fields/field.js";
import { naturalNumbers, UrlWithNewParams } from "../utils/utils.js";
import { makeHiddenInputs } from "../make-hidden-inputs.js";
import qs from "qs";
import { NumberField } from "../forms/fields/number.js";
import { SimpleFormField } from "../forms/fields/simple-form-field.js";
import { FormDataValue } from "../forms/form-types.js";
export const BasePagePropsShape = <const>{};
export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
export const DEFAULT_ITEMS_PER_PAGE = 12;
export const BaseListPageFields = <const>{
page: new NumberField(false, 1),
itemsPerPage: new NumberField(false, DEFAULT_ITEMS_PER_PAGE),
sort: new SimpleFormField(false),
};
export type ListSort = { field: string; order: "asc" | "desc" };
const SORT_SEPARATOR = ":";
function decodeSort(s: unknown): ListSort | null {
if (typeof s !== "string" || !s.includes(SORT_SEPARATOR)) {
return null;
}
let order = s.split(SORT_SEPARATOR)[1];
const field = s.split(SORT_SEPARATOR)[0];
if (order !== "asc" && order !== "desc") {
order = "asc";
return {
field,
order: order as "asc",
};
} else {
return { field, order };
}
}
function encodeSort(field: string, order: "asc" | "desc"): string {
return [field, order].join(SORT_SEPARATOR);
}
export abstract class ListPage<
ItemType,
F extends typeof BaseListPageFields
> extends Page<F> {
abstract getItems(
ctx: Context,
page: number,
itemsPerPage: number | null,
values: Record<string, FormDataValue>
): Promise<ItemType[]>;
abstract getTotalPages(
ctx: Context,
itemsPerPage: number,
values: Record<string, FormDataValue>
): Promise<number>;
abstract renderItem(
ctx: Context,
item: ItemType,
index: number
): Promise<FlatTemplatable>;
filterFields: Record<string, FormField> = {};
filterControls: FormControl[] = [];
init(): void {
super.init();
for (const [fieldname, field] of Object.entries(this.filterFields)) {
void field.init(fieldname);
}
}
renderListContainer(_: Context, content: Templatable): FlatTemplatable {
return tempstream`<div>${content}</div>`;
}
async getPaginationConfig(ctx: Context) {
const values = await this.extractRawValues(ctx);
let { parsed: page } = await this.fields.page.getValue(ctx, values);
if (!page) {
page = 1;
}
const { parsed: itemsPerPage } =
await this.fields.itemsPerPage.getValue(ctx, values);
return { page, itemsPerPage };
}
async renderItems(
ctx: Context,
values?: Record<string, FormDataValue>,
items?: ItemType[]
): Promise<FlatTemplatable> {
if (!values) {
values = await this.extractRawValues(ctx);
}
const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
const items_promise = this.getItems(ctx, page, itemsPerPage, values);
return tempstream`${(items
? Promise.resolve(items)
: items_promise
).then((items) =>
items.map((item, index) => this.renderItem(ctx, item, index))
)}`;
}
async renderPagination(
ctx: Context,
values: Record<string, FormDataValue>
): Promise<FlatTemplatable> {
const { itemsPerPage, page } = await this.getPaginationConfig(ctx);
const totalIems = await this.getTotalPages(
ctx,
itemsPerPage || DEFAULT_ITEMS_PER_PAGE,
values
);
return tempstream/* HTML */ `<div class="list-pagination">
<div class="list-pagination__left">
${page > 1
? this.renderPageButton(ctx, 1, "Pierwsza strona")
: ""}
${page > 1
? this.renderPageButton(ctx, page - 1, "Poprzednia strona")
: ""}
</div>
<div class="list-pagination__center">
<select
title="choose page"
onchange="if (this.value) Turbo.visit(this.value)"
>
${Array.from(naturalNumbers(1, totalIems)).map(
(n) => /* HTML */ `<option
value="${UrlWithNewParams(
ctx,
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.propsParser.overwriteProp(ctx, {
page: n,
} as Partial<Record<string, unknown>>)
)}"
${page === n ? "selected" : ""}
>
${n}
</option>`
)}
</select>
</div>
<div class="list-pagination__right">
${page < totalIems
? this.renderPageButton(ctx, page + 1, "Następna strona")
: ""}
${page < totalIems
? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
: ""}
</div>
</div>`;
}
private renderPageButton(ctx: Context, page: number, text: string) {
return /* HTML */ `<a
href="${UrlWithNewParams(
ctx,
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.propsParser.overwriteProp(ctx, {
page,
} as Partial<Record<string, unknown>>)
)}"
>${text}</a
>`;
}
async getFilterValues(ctx: Context): Promise<Record<string, unknown>> {
const filter = {} as Record<string, unknown>;
const raw_values = await this.extractRawValues(ctx);
for (const [fieldname, field] of Object.entries(this.filterFields)) {
// eslint-disable-next-line no-await-in-loop
const { parsed } = await field.getValue(ctx, raw_values);
filter[fieldname] = field.mapToFilter(parsed);
}
return filter;
}
async getSort(ctx: Context): Promise<ListSort | null> {
const { sort } = await this.extractRawValues(ctx);
const decoded = decodeSort(sort);
if (decoded === null) {
return this.getDefaultSort(ctx);
} else {
return decoded;
}
}
makeSortLink(ctx: Context, field: string, order: "asc" | "desc"): string {
const url = new URL(ctx.url, "https://example.com");
const params = qs.parse(url.search.slice(1));
params.sort = encodeSort(field, order);
url.search = qs.stringify(params);
return url.pathname + url.search;
}
async renderFilters(ctx: Context): Promise<FlatTemplatable> {
const values = await this.extractRawValues(ctx);
return tempstream/* HTML */ `<form method="GET">
${makeHiddenInputs(ctx, this.fields, values, [
"page",
...Object.values(this.filterFields).map((f) => f.name),
])}
${this.filterControls.map((control) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return control.render(
this.makeFormControlContext(
ctx,
{ raw_values: values, messages: [] },
false
)
);
})}
</form>`;
}
async renderHeading(
ctx: Context,
field: string,
label = field
): Promise<FlatTemplatable> {
const current_sort = await this.getSort(ctx);
const current_order =
current_sort?.field == field ? current_sort.order : null;
const order = current_order == "desc" ? "asc" : "desc";
return /* HTML */ `<th>
<a href="${this.makeSortLink(ctx, field, order)}"
>${label}
${(current_order && (current_order == "asc" ? "↑" : "↓")) ||
""}</a
>
</th>`;
}
getDefaultSort(_: Context): ListSort | null {
return null;
}
renderTableHead(
ctx: Context,
fields: { field: string; label?: string }[]
): FlatTemplatable {
return tempstream/* HTML */ `<thead>
<tr>
${fields.map(({ label, field }) =>
this.renderHeading(ctx, field, label)
)}
</tr>
</thead>`;
}
async render(ctx: Context): Promise<FlatTemplatable> {
const values = await this.extractRawValues(ctx);
return tempstream`${this.renderPagination(ctx, values)}
${this.renderFilters(ctx)}
${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
}
}

File Metadata

Mime Type
text/x-java
Expires
Fri, Jan 24, 15:15 (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
598011
Default Alt Text
list.ts (7 KB)

Event Timeline