Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360681
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rPLAY Sealious playground
Attached
Detach File
Event Timeline
Log In to Comment