Page MenuHomeSealhub

No OneTemporary

diff --git a/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx
new file mode 100644
index 0000000..145018e
--- /dev/null
+++ b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx
@@ -0,0 +1,31 @@
+import type { Context } from "koa";
+import type { FieldNames } from "sealious";
+import { TempstreamJSX } from "tempstream";
+import type Pages from "src/back/collections/pages.js";
+import { EditJDDField } from "@sealcode/jdd-editor";
+import html from "src/back/html.js";
+import { registry } from "src/back/jdd-components/registry.js";
+import { makeJDDContext } from "src/back/jdd-context.js";
+import { defaultHead } from "src/back/defaultHead.js";
+
+export const actionName = "EditPageContent";
+
+export default new (class JDDCreatePreviewPage extends EditJDDField<Pages> {
+ getCollection(ctx: Context) {
+ return ctx.$app.collections["pages"];
+ }
+
+ getJDDFieldName(): FieldNames<Pages["fields"]> {
+ return "content";
+ }
+
+ async renderPreParameterButtons(ctx: Context) {
+ const item = await this.getItem(ctx);
+ return (
+ <div>
+ <h1>Edit pages: {item.id}</h1>{" "}
+ </div>
+ );
+ }
+})({ html, registry, makeJDDContext, defaultHead });
+
diff --git a/src/back/routes/admin/pages/[id]/delete.page.tsx b/src/back/routes/admin/pages/[id]/delete.page.tsx
new file mode 100644
index 0000000..69de776
--- /dev/null
+++ b/src/back/routes/admin/pages/[id]/delete.page.tsx
@@ -0,0 +1,25 @@
+import type { Context } from "koa";
+import { Mountable } from "@sealcode/sealgen";
+import type Router from "@koa/router";
+
+import { Pages } from "../../../../collections/collections.js";
+
+import { PagesCRUDListURL } from "../../../urls.js";
+
+export const actionName = "PagesCRUDDelete";
+
+export default new (class PagesCRUDDeleteRedirect extends Mountable {
+ canAccess = async (ctx: Context) => {
+ const policy = Pages.getPolicy("edit");
+ const response = await policy.check(ctx.$context);
+ return { canAccess: response?.allowed || false, message: response?.reason || "" };
+ };
+
+ mount(router: Router, path: string) {
+ router.post(path, async (ctx) => {
+ await ctx.$app.collections["pages"].removeByID(ctx.$context, ctx.params.id!);
+ ctx.status = 302;
+ ctx.redirect(PagesCRUDListURL);
+ });
+ }
+})();
diff --git a/src/back/routes/admin/pages/[id]/edit.form.ts b/src/back/routes/admin/pages/[id]/edit.form.ts
new file mode 100644
index 0000000..cce5c7a
--- /dev/null
+++ b/src/back/routes/admin/pages/[id]/edit.form.ts
@@ -0,0 +1,117 @@
+import type { Context } from "koa";
+import type { FormData } from "@sealcode/sealgen";
+import { Form, Controls, fieldsToShape } from "@sealcode/sealgen";
+import html from "../../../../html.js";
+
+import { PagesFormFields, PagesFormControls } from "../shared.js";
+import { Pages } from "../../../../collections/collections.js";
+import { PagesCRUDListURL } from "../../../urls.js";
+import { tempstream } from "tempstream";
+
+import { withFallback } from "@sealcode/sealgen";
+
+export const actionName = "PagesCRUDEdit";
+
+const fields = {
+ ...PagesFormFields,
+};
+
+export const PagesCRUDEditShape = fieldsToShape(fields);
+
+export default new (class PagesCRUDEditForm extends Form<typeof fields, void> {
+ defaultSuccessMessage = "Formularz wypełniony poprawnie";
+ fields = fields;
+
+ controls = [new Controls.FormHeader("Edit Pages"), ...PagesFormControls];
+
+ async getID(ctx: Context): Promise<string> {
+ const param_name = "id";
+ const id = ctx.params[param_name];
+ if (!id) {
+ throw new Error("Missing URL parameter: " + param_name);
+ }
+ return id;
+ }
+
+ async getInitialValues(ctx: Context) {
+ const id = await this.getID(ctx);
+
+ const {
+ items: [item],
+ } = await ctx.$app.collections["pages"]
+ .list(ctx.$context)
+ .ids([id])
+ .attach({})
+ .fetch();
+
+ if (!item) {
+ throw new Error("Item with given id not found: " + id);
+ }
+
+ return {
+ url: item.get("url"),
+ domain: withFallback(item.get("domain"), ""),
+ title: withFallback(item.get("title"), ""),
+ heading: withFallback(item.get("heading"), ""),
+ description: withFallback(item.get("description"), ""),
+ imageForMetadata: { old: item.get("imageForMetadata") },
+ hideNavigation: withFallback(
+ item.get("hideNavigation"),
+ String(item.get("hideNavigation")),
+ "false"
+ ),
+ };
+ }
+
+ async onSubmit(ctx: Context) {
+ const data = await this.getParsedValues(ctx);
+ const id = await this.getID(ctx);
+ const {
+ items: [item],
+ } = await ctx.$app.collections["pages"].list(ctx.$context).ids([id]).fetch();
+
+ if (!item) {
+ throw new Error("Unknown id: " + id);
+ }
+
+ if (!data) {
+ throw new Error("Error when parsing the form values");
+ }
+
+ const preparedImageForMetadata =
+ data.imageForMetadata.new || data.imageForMetadata.old;
+ if (!preparedImageForMetadata) {
+ throw new Error("Missing field: imageForMetadata");
+ }
+
+ item.setMultiple({
+ url: data["url"],
+ content: [],
+ domain: data["domain"] != null ? data["domain"] : "",
+ title: data["title"] != null ? data["title"] : "",
+ heading: data["heading"] != null ? data["heading"] : "",
+ description: data["description"] != null ? data["description"] : "",
+ imageForMetadata: preparedImageForMetadata,
+ hideNavigation: !!data.hideNavigation != null ? !!data.hideNavigation : false,
+ });
+ await item.save(ctx.$context);
+ }
+
+ canAccess = async (ctx: Context) => {
+ const policy = Pages.getPolicy("edit");
+ const response = await policy.check(ctx.$context);
+ return { canAccess: response?.allowed || false, message: response?.reason || "" };
+ };
+
+ async render(ctx: Context, data: FormData, show_field_errors: boolean) {
+ return html({
+ ctx,
+ title: "Edit Pages",
+ body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
+ <a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
+ ${await super.render(ctx, data, show_field_errors)}
+ </div>`,
+ description: "",
+ });
+ }
+})();
diff --git a/src/back/routes/admin/pages/create.form.ts b/src/back/routes/admin/pages/create.form.ts
new file mode 100644
index 0000000..01de0fa
--- /dev/null
+++ b/src/back/routes/admin/pages/create.form.ts
@@ -0,0 +1,69 @@
+import type { Context } from "koa";
+import type { FormData } from "@sealcode/sealgen";
+import { Form, Controls, fieldsToShape } from "@sealcode/sealgen";
+import html from "../../../html.js";
+
+import { PagesFormFields, PagesFormControls } from "./shared.js";
+
+import { Pages } from "../../../collections/collections.js";
+
+import { PagesCRUDListURL } from "../../urls.js";
+
+import { tempstream } from "tempstream";
+
+export const actionName = "PagesCRUDCreate";
+
+const fields = {
+ ...PagesFormFields,
+};
+
+export const PagesCRUDCreateShape = fieldsToShape(fields);
+
+export default new (class PagesCRUDCreateForm extends Form<typeof fields, void> {
+ defaultSuccessMessage = "Formularz wypełniony poprawnie";
+ fields = fields;
+
+ controls = [new Controls.FormHeader("Create Pages"), ...PagesFormControls];
+
+ async onSubmit(ctx: Context) {
+ const data = await this.getParsedValues(ctx);
+ if (!data) {
+ throw new Error("Error when parsing the form values");
+ }
+
+ const preparedImageForMetadata =
+ data.imageForMetadata.new || data.imageForMetadata.old;
+ if (!preparedImageForMetadata) {
+ throw new Error("Missing field: imageForMetadata");
+ }
+
+ await ctx.$app.collections["pages"].create(ctx.$context, {
+ url: data["url"],
+ content: [],
+ domain: data["domain"] != null ? data["domain"] : "",
+ title: data["title"] != null ? data["title"] : "",
+ heading: data["heading"] != null ? data["heading"] : "",
+ description: data["description"] != null ? data["description"] : "",
+ imageForMetadata: preparedImageForMetadata,
+ hideNavigation: !!data.hideNavigation != null ? !!data.hideNavigation : false,
+ });
+ }
+
+ canAccess = async (ctx: Context) => {
+ const policy = Pages.getPolicy("create");
+ const response = await policy.check(ctx.$context);
+ return { canAccess: response?.allowed || false, message: response?.reason || "" };
+ };
+
+ async render(ctx: Context, data: FormData, show_field_errors: boolean) {
+ return html({
+ ctx,
+ title: "Create pages",
+ body: tempstream/* HTML */ ` <div class="sealgen-crud-form">
+ <a class="" href="${PagesCRUDListURL}">← Back to pages list</a>
+ ${await super.render(ctx, data, show_field_errors)}
+ </div>`,
+ description: "",
+ });
+ }
+})();
diff --git a/src/back/routes/admin/pages/index.list.tsx b/src/back/routes/admin/pages/index.list.tsx
new file mode 100644
index 0000000..501de8f
--- /dev/null
+++ b/src/back/routes/admin/pages/index.list.tsx
@@ -0,0 +1,185 @@
+import type { Context } from "koa";
+import type { CollectionItem } from "sealious";
+import type { FlatTemplatable, Templatable } from "tempstream";
+import { TempstreamJSX, tempstream } from "tempstream";
+import { Pages } from "src/back/collections/collections.js";
+import html from "src/back/html.js";
+import type { ListFilterRender } from "@sealcode/sealgen";
+import {
+ SealiousItemListPage,
+ BaseListPageFields,
+ DefaultListFilters,
+} from "@sealcode/sealgen";
+import qs from "qs";
+
+import {
+ PagesCRUDCreateURL,
+ PagesCRUDEditURL,
+ PagesCRUDDeleteURL,
+ EditPageContentURL,
+} from "../../urls.js";
+
+import type { FilePointer } from "@sealcode/file-manager";
+import { imageRouter } from "src/back/image-router.js";
+
+export const actionName = "PagesCRUDList";
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const filterFields = [
+ { field: "url", ...DefaultListFilters["text"] },
+ { field: "content", ...DefaultListFilters.fallback },
+ { field: "domain", ...DefaultListFilters["text"] },
+ { field: "title", ...DefaultListFilters["text"] },
+ { field: "heading", ...DefaultListFilters["text"] },
+ { field: "description", ...DefaultListFilters["text"] },
+ { field: "imageForMetadata", ...DefaultListFilters.fallback },
+ { field: "hideNavigation", ...DefaultListFilters["boolean"] },
+] as {
+ field: keyof (typeof Pages)["fields"];
+ render?: ListFilterRender;
+ prepareValue?: (filter_value: unknown) => unknown; // set this function to change what filter value is passed to Sealious
+}[];
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const displayFields = [
+ { field: "url", label: "url" },
+ { field: "content", label: "content" },
+ { field: "domain", label: "domain" },
+ { field: "title", label: "title" },
+ { field: "heading", label: "heading" },
+ { field: "description", label: "description" },
+ {
+ field: "imageForMetadata",
+ label: "imageForMetadata",
+ format: async (value: FilePointer) => {
+ return imageRouter.image(await value.getPath(), {
+ container: { width: 45, height: 45 },
+ crop: { width: 45, height: 45 },
+ alt: "",
+ });
+ },
+ },
+ {
+ field: "hideNavigation",
+ label: "hideNavigation",
+ format: (v: boolean) => (v ? "YES" : "NO"),
+ },
+] as {
+ field: string;
+ label: string;
+ format?: (value: unknown, item: CollectionItem<typeof Pages>) => FlatTemplatable;
+}[];
+
+export default new (class PagesCRUDListPage extends SealiousItemListPage<
+ typeof Pages,
+ typeof BaseListPageFields
+> {
+ fields = BaseListPageFields;
+
+ async renderFilters(ctx: Context): Promise<FlatTemplatable> {
+ const query_params = qs.parse(ctx.search.slice(1));
+ query_params.page = "1";
+ const filter_values = await super.getFilterValues(ctx);
+ return (
+ <form>
+ {Object.entries(query_params).map(([key, value]) => {
+ if (key == "filter") {
+ return "";
+ }
+ // this is necessary to not lose any query params when the user changes the filter values
+ return <input type="hidden" name={key} value={value} />;
+ })}
+ {filterFields.map(({ field, render }) => {
+ if (!render) {
+ render = DefaultListFilters.fallback.render;
+ }
+ return (
+ render(
+ filter_values[field] || "",
+ this.collection.fields[field]
+ ) || ""
+ );
+ })}
+ <input type="submit" />
+ </form>
+ );
+ }
+
+ async getFilterValues(ctx: Context) {
+ // adding opportunity to adjust the values for a given field filter before it's sent to Sealious
+ const values = await super.getFilterValues(ctx);
+ for (const filterField of filterFields) {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const key = filterField.field as keyof typeof values;
+ if (key in values) {
+ const prepare_fn = filterField.prepareValue;
+ if (prepare_fn) {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
+ values[key] = prepare_fn(values[key]) as any;
+ }
+ }
+ }
+ return values;
+ }
+
+ async renderItem(ctx: Context, item: CollectionItem<typeof Pages>) {
+ return (
+ <tr>
+ {displayFields.map(({ field, format }) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
+ const value = item.get(field as any);
+ return <td>{format ? format(value, item) : value}</td>;
+ })}
+ <td>
+ <div class="sealious-list__actions">
+ <a href={PagesCRUDEditURL(item.id)}>Edit Metadata</a>
+ <a
+ data-turbo-method="POST"
+ data-turbo-confirm="Delete item?"
+ href={PagesCRUDDeleteURL(item.id)}
+ >
+ Delete
+ </a>
+ <a href={EditPageContentURL(item.id)}>Edit Content</a>
+ </div>
+ </td>
+ </tr>
+ );
+ }
+
+ renderListContainer(ctx: Context, content: Templatable): FlatTemplatable {
+ return (
+ <table class="sealious-list pages-crudlist-table">
+ {this.renderTableHead(ctx, displayFields)}
+ <tbody>{content}</tbody>
+ </table>
+ );
+ }
+
+ renderTableHead(
+ ctx: Context,
+ fields: { field: string; label?: string }[]
+ ): FlatTemplatable {
+ return tempstream/* HTML */ `<thead>
+ <tr>
+ ${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
+ <th>Actions</th>
+ </tr>
+ </thead>`;
+ }
+
+ async render(ctx: Context) {
+ return html({
+ ctx,
+ title: "PagesCRUDList",
+ description: "",
+ body: (
+ <div class="sealious-list-wrapper pages-crudlist--wrapper">
+ <h2>PagesCRUDList List</h2>
+ <a href={PagesCRUDCreateURL}> Create </a>
+ {super.render(ctx)}
+ </div>
+ ),
+ });
+ }
+})(Pages);
diff --git a/src/back/routes/admin/pages/shared.ts b/src/back/routes/admin/pages/shared.ts
new file mode 100644
index 0000000..8c799f5
--- /dev/null
+++ b/src/back/routes/admin/pages/shared.ts
@@ -0,0 +1,36 @@
+import { Controls, Fields } from "@sealcode/sealgen";
+import { Pages } from "../../../collections/collections.js";
+
+import { imageRouter } from "../../../image-router.js";
+import { TheFileManager } from "../../../file-manager.js";
+
+export const PagesFormFields = <const>{
+ url: new Fields.CollectionField(Pages.fields.url.required, Pages.fields.url),
+ domain: new Fields.CollectionField(Pages.fields.domain.required, Pages.fields.domain),
+ title: new Fields.CollectionField(Pages.fields.title.required, Pages.fields.title),
+ heading: new Fields.CollectionField(
+ Pages.fields.heading.required,
+ Pages.fields.heading
+ ),
+ description: new Fields.CollectionField(
+ Pages.fields.description.required,
+ Pages.fields.description
+ ),
+ imageForMetadata: new Fields.File(
+ Pages.fields.imageForMetadata.required,
+ TheFileManager
+ ),
+ hideNavigation: new Fields.Boolean(Pages.fields.hideNavigation.required),
+};
+
+export const PagesFormControls = [
+ new Controls.SimpleInput(PagesFormFields.url, { label: "url" }),
+ new Controls.SimpleInput(PagesFormFields.domain, { label: "domain" }),
+ new Controls.SimpleInput(PagesFormFields.title, { label: "title" }),
+ new Controls.SimpleInput(PagesFormFields.heading, { label: "heading" }),
+ new Controls.SimpleInput(PagesFormFields.description, { label: "description" }),
+ new Controls.Photo(PagesFormFields.imageForMetadata, imageRouter, {
+ label: "imageForMetadata",
+ }),
+ new Controls.Checkbox(PagesFormFields.hideNavigation, { label: "hideNavigation" }),
+];
diff --git a/src/back/routes/common/navbar.ts b/src/back/routes/common/navbar.ts
index 7f17b61..ec81164 100644
--- a/src/back/routes/common/navbar.ts
+++ b/src/back/routes/common/navbar.ts
@@ -1,34 +1,37 @@
import type { BaseContext } from "koa";
import type { FlatTemplatable } from "tempstream";
-import { SignInURL, LogoutURL } from "../urls.js";
+import { SignInURL, LogoutURL, PagesCRUDListURL } from "../urls.js";
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
const isLoggedIn = !!ctx.$context.session_id;
const linkData = isLoggedIn
- ? [{ text: "Logout", url: LogoutURL }]
+ ? [
+ { text: "Pages", url: PagesCRUDListURL },
+ { text: "Logout", url: LogoutURL },
+ ]
: [{ text: "Sign in", url: SignInURL }];
const linksHTML = linkData
.map((link) =>
- link.url === new URL(ctx.url, "https://a.com").pathname
+ link.url === new URL(ctx.url, "https://a.com").pathname // checking if it's the current path we're looking at
? `<li class="active"><span>${link.text}</span></li>`
: /* HTML */ `<li><a href="${link.url}">${link.text}</a></li>`
)
.join("\n");
return /* HTML */ ` <nav>
<a href="/" class="nav-logo">
<img
src="/assets/logo"
alt="${ctx.$app.manifest.name} - logo"
width="50"
height="50"
/>
${ctx.$app.manifest.name}
</a>
<ul>
${linksHTML}
</ul>
</nav>`;
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 08:20 (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034327
Default Alt Text
(17 KB)

Event Timeline