Page MenuHomeSealhub

list.ts
No OneTemporary

import { Context } from "koa";
import { Collection, CollectionItem, ItemFields, ItemList } from "sealious";
import { Templatable, tempstream, FlatTemplatable } from "tempstream";
import { peopleWhoCan } from "./access-control";
import { Page } from "./page";
import { ShapeToType } from "@sealcode/ts-predicates";
import { FormFieldControl } from "../forms/controls/controls";
import { FormField, NumberField } from "../forms/fields/field";
import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
import { FormDataValue } from "..";
import { makeHiddenInputs } from "../make-hidden-inputs";
export const BasePagePropsShape = <const>{};
export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
export const BaseListPageFields = <const>{
page: new NumberField(false, 1),
itemsPerPage: new NumberField(false, 12),
};
export abstract class ListPage<
ItemType,
F extends typeof BaseListPageFields
> extends Page<F> {
abstract getItems(
ctx: Context,
page: number,
itemsPerPage: number,
values: Record<string, FormDataValue>
): Promise<ItemType[]>;
abstract getTotalPages(
ctx: Context,
itemsPerPage: number,
values: Record<string, FormDataValue>
): Promise<number>;
abstract renderItem(ctx: Context, item: ItemType): Promise<FlatTemplatable>;
filterFields: Record<string, FormField> = {};
filterControls: FormFieldControl[] = [];
init(): void {
super.init();
for (const [fieldname, field] of Object.entries(this.filterFields)) {
field.init(fieldname);
}
}
renderListContainer(_: Context, content: Templatable): FlatTemplatable {
return tempstream`<div>${content}</div>`;
}
async render(ctx: Context): Promise<FlatTemplatable> {
const values = this.extractRawValues(ctx);
const { parsed: page } = await this.fields.page.getValue(ctx, values);
const { parsed: itemsPerPage } =
await this.fields.itemsPerPage.getValue(ctx, values);
const items_promise = this.getItems(ctx, page, itemsPerPage, values);
return tempstream`${this.renderPagination(ctx, values)}
${this.renderFilters(ctx)}
${items_promise.then((items) =>
this.renderListContainer(
ctx,
items.map((item) => this.renderItem(ctx, item))
)
)}`;
}
async renderPagination(
ctx: Context,
values: Record<string, FormDataValue>
): Promise<FlatTemplatable> {
const { parsed: page } = await this.fields.page.getValue(ctx, values);
const { parsed: itemsPerPage } =
await this.fields.itemsPerPage.getValue(ctx, values);
const totalIems = await this.getTotalPages(ctx, itemsPerPage, values);
return tempstream/* HTML */ `<center>
${page > 1 ? this.renderPageButton(ctx, 1, "Pierwsza strona") : ""}
${page > 1
? this.renderPageButton(ctx, page - 1, "Poprzednia strona")
: ""}
<select 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>
${page < totalIems
? this.renderPageButton(ctx, page + 1, "Następna strona")
: ""}
${page < totalIems
? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
: ""}
</center>`;
}
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 = 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] = parsed;
}
return filter;
}
renderFilters(ctx: Context): FlatTemplatable {
const values = 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
control.render(ctx, values)
)}
</form>`;
}
}
export abstract class SealiousItemListPage<
C extends Collection,
F extends typeof BaseListPageFields
> extends ListPage<CollectionItem<C>, F> {
constructor(public collection: C) {
super();
}
async getTotalPages(ctx: Context, itemsPerPage: number): Promise<number> {
const query = this.collection.list(ctx.$context);
await this.addFilter(ctx, query);
const { items } = await query.fetch();
return Math.ceil(items.length / itemsPerPage);
}
protected async addFilter(ctx: Context, list: ItemList<C>): Promise<void> {
const filters = await this.getFilterValues(ctx);
if (Object.keys(filters).length) {
list.filter(filters as Partial<ItemFields<C>>);
}
}
async getItems(
ctx: Context,
page: number,
itemsPerPage: number
): Promise<CollectionItem<C>[]> {
const query = this.collection.list(ctx.$context);
await this.addFilter(ctx, query);
const { items } = await query
.paginate({ items: itemsPerPage, page })
.fetch();
return items;
}
async renderItem(
_: Context,
item: CollectionItem<C>
): Promise<FlatTemplatable> {
return `<div>${item.id}</div>`;
}
canAccess = peopleWhoCan("list", this.collection);
}

File Metadata

Mime Type
text/x-java
Expires
Wed, May 7, 19:48 (20 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
664225
Default Alt Text
list.ts (5 KB)

Event Timeline