diff --git a/package-lock.json b/package-lock.json
index b4b05b0..de55747 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,7 +17,7 @@
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.7.1",
"@sealcode/jdd-editor": "^0.1.19",
- "@sealcode/sealgen": "^0.17.24",
+ "@sealcode/sealgen": "^0.17.26",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0",
@@ -1004,9 +1004,9 @@
"integrity": "sha512-pDsGlk2KokQkwzsJDBUWJFDRpEoxxth6TMQGDJyCTmWnd1Vn+cQb5moXDKaf7cXnWb9Y6QtdNX/fPzM/3RH2Cg=="
},
"node_modules/@sealcode/sealgen": {
- "version": "0.17.24",
- "resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.17.24.tgz",
- "integrity": "sha512-xj3BTX2b3uFaO0Fj71inNY282Y96JiiCDFJF4dCmgWQu+yOLlzaVKF7p4bSi1CoNN9eHsvwXUEYKoRfvz8Q9GQ==",
+ "version": "0.17.26",
+ "resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.17.26.tgz",
+ "integrity": "sha512-eehUUh+d3XW8F41d18q9tBuYEg9P21IfvovxFiM/p609OaplcZ4pUD+pma7zu/cMr+Xh85aw/gQUox5C9Obp4A==",
"dependencies": {
"@koa/router": "^12.0.1",
"@sealcode/file-manager": "^1.0.2",
@@ -1017,6 +1017,7 @@
"esbuild": "^0.20.0",
"escape-goat": "^4.0.0",
"google-fonts-helper": "^3.4.1",
+ "husky": "^9.1.7",
"is-what": "^4.1.16",
"js-convert-case": "^4.2.0",
"json5": "^2.2.3",
@@ -5131,7 +5132,6 @@
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
- "dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
diff --git a/package.json b/package.json
index b22bde3..e9c51fa 100644
--- a/package.json
+++ b/package.json
@@ -82,7 +82,7 @@
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.7.1",
"@sealcode/jdd-editor": "^0.1.19",
- "@sealcode/sealgen": "^0.17.24",
+ "@sealcode/sealgen": "^0.17.26",
"@sealcode/simplemde": "^1.12.1",
"@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0",
diff --git a/src/back/routes/admin.page.tsx b/src/back/routes/admin.page.tsx
index 4c0c6a1..c328c9c 100644
--- a/src/back/routes/admin.page.tsx
+++ b/src/back/routes/admin.page.tsx
@@ -3,7 +3,7 @@ import { TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import html from "src/back/html.js";
import { button } from "../elements/button.js";
-import { LoginURL, NavbarLinksCRUDURL, PagesCRUDListURL } from "./urls.js";
+import { LoginURL, NavbarLinksCRUDListURL, PagesCRUDListURL } from "./urls.js";
export const actionName = "Admin";
@@ -24,9 +24,10 @@ export default new (class AdminPage extends Page {
title: "Admin",
body: (
+
Admin panel
{[
- { text: "Pages", href: PagesCRUDListURL },
- { text: "Navbar", href: NavbarLinksCRUDURL },
+ { text: "Edit Pages", href: PagesCRUDListURL },
+ { text: "Edit Navbar", href: NavbarLinksCRUDListURL },
].map((e) => button({ ...e, variant: "accent1" }))}
),
diff --git a/src/back/routes/admin/navbar/[id]/delete.page.tsx b/src/back/routes/admin/navbar/[id]/delete.page.tsx
new file mode 100644
index 0000000..1567002
--- /dev/null
+++ b/src/back/routes/admin/navbar/[id]/delete.page.tsx
@@ -0,0 +1,28 @@
+import type { Context } from "koa";
+import { Mountable } from "@sealcode/sealgen";
+import type Router from "@koa/router";
+
+import { NavbarLinks } from "../../../../collections/collections.js";
+
+import { NavbarLinksCRUDListURL } from "../../../urls.js";
+
+export const actionName = "NavbarLinksCRUDDelete";
+
+export default new (class NavbarLinksCRUDDeleteRedirect extends Mountable {
+ canAccess = async (ctx: Context) => {
+ const policy = NavbarLinks.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["navbar-links"].removeByID(
+ ctx.$context,
+ ctx.params.id!
+ );
+ ctx.status = 302;
+ ctx.redirect(NavbarLinksCRUDListURL);
+ });
+ }
+})();
diff --git a/src/back/routes/admin/navbar/[id]/edit.form.ts b/src/back/routes/admin/navbar/[id]/edit.form.ts
new file mode 100644
index 0000000..b3f49c8
--- /dev/null
+++ b/src/back/routes/admin/navbar/[id]/edit.form.ts
@@ -0,0 +1,105 @@
+import type { Context } from "koa";
+import type { FormData } from "@sealcode/sealgen";
+import { Form, Controls, fieldsToShape } from "@sealcode/sealgen";
+import html from "../../../../html.js";
+
+import { NavbarLinksFormFields, NavbarLinksFormControls } from "../shared.js";
+import { NavbarLinks } from "../../../../collections/collections.js";
+import { NavbarLinksCRUDListURL } from "../../../urls.js";
+import { tempstream } from "tempstream";
+
+import { withFallback } from "@sealcode/sealgen";
+
+export const actionName = "NavbarLinksCRUDEdit";
+
+const fields = {
+ ...NavbarLinksFormFields,
+};
+
+export const NavbarLinksCRUDEditShape = fieldsToShape(fields);
+
+export default new (class NavbarLinksCRUDEditForm extends Form {
+ defaultSuccessMessage = "Formularz wypełniony poprawnie";
+ fields = fields;
+
+ controls = [new Controls.FormHeader("Edit NavbarLinks"), ...NavbarLinksFormControls];
+
+ async getID(ctx: Context): Promise {
+ // trying to automatically guess which param is the one that represents the ID
+ // of the resource to edit.
+ // If sealgen got it wrong, just return the param name that should be used here instead
+ 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["navbar-links"]
+ .list(ctx.$context)
+ .ids([id])
+ .attach({})
+ .fetch();
+
+ if (!item) {
+ throw new Error("Item with given id not found: " + id);
+ }
+
+ return {
+ label: withFallback(item.get("label"), ""),
+ href: withFallback(item.get("href"), ""),
+ };
+ }
+
+ async onSubmit(ctx: Context) {
+ const data = await this.getParsedValues(ctx);
+ const id = await this.getID(ctx);
+ const {
+ items: [item],
+ } = await ctx.$app.collections["navbar-links"]
+ .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");
+ }
+
+ item.setMultiple({
+ label: data["label"] != null ? data["label"] : "",
+ href: data["href"] != null ? data["href"] : "",
+ });
+ await item.save(ctx.$context);
+ }
+
+ canAccess = async (ctx: Context) => {
+ const policy = NavbarLinks.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 NavbarLinks",
+ body: tempstream/* HTML */ ` `,
+ description: "",
+ css_clumps: ["admin-forms"],
+ });
+ }
+})();
diff --git a/src/back/routes/admin/navbar/create.form.ts b/src/back/routes/admin/navbar/create.form.ts
new file mode 100644
index 0000000..7154a92
--- /dev/null
+++ b/src/back/routes/admin/navbar/create.form.ts
@@ -0,0 +1,63 @@
+import type { Context } from "koa";
+import type { FormData } from "@sealcode/sealgen";
+import { Form, Controls, fieldsToShape } from "@sealcode/sealgen";
+import html from "../../../html.js";
+
+import { NavbarLinksFormFields, NavbarLinksFormControls } from "./shared.js";
+
+import { NavbarLinks } from "../../../collections/collections.js";
+
+import { NavbarLinksCRUDListURL } from "../../urls.js";
+
+import { tempstream } from "tempstream";
+
+export const actionName = "NavbarLinksCRUDCreate";
+
+const fields = {
+ ...NavbarLinksFormFields,
+};
+
+export const NavbarLinksCRUDCreateShape = fieldsToShape(fields);
+
+export default new (class NavbarLinksCRUDCreateForm extends Form {
+ defaultSuccessMessage = "Formularz wypełniony poprawnie";
+ fields = fields;
+
+ controls = [
+ new Controls.FormHeader("Create NavbarLinks"),
+ ...NavbarLinksFormControls,
+ ];
+
+ async onSubmit(ctx: Context) {
+ const data = await this.getParsedValues(ctx);
+ if (!data) {
+ throw new Error("Error when parsing the form values");
+ }
+
+ await ctx.$app.collections["navbar-links"].create(ctx.$context, {
+ label: data["label"] != null ? data["label"] : "",
+ href: data["href"] != null ? data["href"] : "",
+ });
+ }
+
+ canAccess = async (ctx: Context) => {
+ const policy = NavbarLinks.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 navbar-links",
+ body: tempstream/* HTML */ ` `,
+ description: "",
+ css_clumps: ["admin-forms"],
+ });
+ }
+})();
diff --git a/src/back/routes/admin/navbar/index.list.tsx b/src/back/routes/admin/navbar/index.list.tsx
new file mode 100644
index 0000000..a92dc2a
--- /dev/null
+++ b/src/back/routes/admin/navbar/index.list.tsx
@@ -0,0 +1,159 @@
+import type { Context } from "koa";
+import type { CollectionItem } from "sealious";
+import type { FlatTemplatable, Templatable } from "tempstream";
+import { TempstreamJSX, tempstream } from "tempstream";
+import { NavbarLinks } 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 {
+ NavbarLinksCRUDCreateURL,
+ NavbarLinksCRUDEditURL,
+ NavbarLinksCRUDDeleteURL,
+ AdminURL,
+} from "../../urls.js";
+
+export const actionName = "NavbarLinksCRUDList";
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+const filterFields = [
+ { field: "label", ...DefaultListFilters["text"] },
+ { field: "href", ...DefaultListFilters["text"] },
+] as {
+ field: keyof (typeof NavbarLinks)["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: "label", label: "label" },
+ { field: "href", label: "href" },
+] as {
+ field: string;
+ label: string;
+ format?: (
+ value: unknown,
+ item: CollectionItem
+ ) => FlatTemplatable;
+}[];
+
+export default new (class NavbarLinksCRUDListPage extends SealiousItemListPage<
+ typeof NavbarLinks,
+ typeof BaseListPageFields
+> {
+ fields = BaseListPageFields;
+
+ async renderFilters(ctx: Context): Promise {
+ const query_params = qs.parse(ctx.search.slice(1));
+ query_params.page = "1";
+ const filter_values = await super.getFilterValues(ctx);
+ return (
+
+ );
+ }
+
+ 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) {
+ return (
+
+ {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 {format ? format(value, item) : value} | ;
+ })}
+
+
+ |
+
+ );
+ }
+
+ renderListContainer(ctx: Context, content: Templatable): FlatTemplatable {
+ return (
+
+ {this.renderTableHead(ctx, displayFields)}
+ {content}
+
+ );
+ }
+
+ renderTableHead(
+ ctx: Context,
+ fields: { field: string; label?: string }[]
+ ): FlatTemplatable {
+ return tempstream/* HTML */ `
+
+ ${fields.map(({ label, field }) => this.renderHeading(ctx, field, label))}
+ Actions |
+
+ `;
+ }
+
+ async render(ctx: Context) {
+ return html({
+ ctx,
+ title: "Navbar",
+ description: "",
+ body: (
+
+ ),
+ });
+ }
+})(NavbarLinks);
diff --git a/src/back/routes/admin/navbar/index.page.ts b/src/back/routes/admin/navbar/index.page.ts
deleted file mode 100644
index 1b7b38a..0000000
--- a/src/back/routes/admin/navbar/index.page.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { CRUD } from "@sealcode/crud-ui";
-import type { CollectionItem } from "sealious";
-import type { ListFilterRender } from "@sealcode/sealgen";
-import { Controls, DefaultListFilters, Fields } from "@sealcode/sealgen";
-import { NavbarLinks } from "src/back/collections/collections.js";
-import html from "src/back/html.js";
-
-export const actionName = "NavbarLinksCRUD";
-
-const edit_fields = {
- label: new Fields.CollectionField(
- NavbarLinks.fields.label.required,
- NavbarLinks.fields.label
- ),
- href: new Fields.CollectionField(
- NavbarLinks.fields.href.required,
- NavbarLinks.fields.href
- ),
-};
-
-const edit_controls = [
- new Controls.SimpleInput(edit_fields.label, { label: "label" }),
- new Controls.SimpleInput(edit_fields.href, { label: "href" }),
-];
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-const fields_for_display = [
- { field: "label", label: "label" },
- { field: "href", label: "href" },
-] as {
- field: keyof (typeof NavbarLinks)["fields"];
- label: string;
- format?: (
- value: unknown,
- item: CollectionItem
- ) => string | Promise;
-}[];
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-const fields_for_filters = [
- { field: "label", ...DefaultListFilters["text"] },
- { field: "href", ...DefaultListFilters["text"] },
-] as {
- field: keyof (typeof NavbarLinks)["fields"];
- render?: ListFilterRender;
- prepareValue?: (filter_value: unknown) => unknown; // set this function to change what filter value is passed to Sealious
-}[];
-
-export default new CRUD({
- collection: NavbarLinks,
- nice_collection_name: "NavbarLinks",
- fields_for_display,
- fields_for_filters,
- html,
- list_title: "Navbar Links",
- edit_title: "Edit",
- edit_button_text: "Edit",
- delete_button_text: "Delete",
- back_to_list_button_text: "← Back to NavbarLinks list",
- edit_fields,
- edit_controls,
- form_value_to_sealious_value: {},
- sealious_value_to_form_value: {},
- post_edit: async () => {},
- post_create: async () => {},
-});
diff --git a/src/back/routes/admin/navbar/shared.ts b/src/back/routes/admin/navbar/shared.ts
new file mode 100644
index 0000000..a8d6acf
--- /dev/null
+++ b/src/back/routes/admin/navbar/shared.ts
@@ -0,0 +1,18 @@
+import { Controls, Fields } from "@sealcode/sealgen";
+import { NavbarLinks } from "../../../collections/collections.js";
+
+export const NavbarLinksFormFields = {
+ label: new Fields.CollectionField(
+ NavbarLinks.fields.label.required,
+ NavbarLinks.fields.label
+ ),
+ href: new Fields.CollectionField(
+ NavbarLinks.fields.href.required,
+ NavbarLinks.fields.href
+ ),
+};
+
+export const NavbarLinksFormControls = [
+ new Controls.SimpleInput(NavbarLinksFormFields.label, { label: "label" }),
+ new Controls.SimpleInput(NavbarLinksFormFields.href, { label: "href" }),
+];
diff --git a/src/back/routes/admin/pages/[id]/edit.form.ts b/src/back/routes/admin/pages/[id]/edit.form.ts
index b07061a..385f8b3 100644
--- a/src/back/routes/admin/pages/[id]/edit.form.ts
+++ b/src/back/routes/admin/pages/[id]/edit.form.ts
@@ -81,9 +81,6 @@ export default new (class PagesCRUDEditForm extends Form {
const preparedImageForMetadata =
data.imageForMetadata?.new || data.imageForMetadata?.old;
- if (!preparedImageForMetadata) {
- throw new Error("Missing field: imageForMetadata");
- }
item.setMultiple({
url: data["url"],
@@ -92,7 +89,7 @@ export default new (class PagesCRUDEditForm extends Form {
title: data["title"] != null ? data["title"] : "",
heading: data["heading"] != null ? data["heading"] : "",
description: data["description"] != null ? data["description"] : "",
- imageForMetadata: preparedImageForMetadata,
+ imageForMetadata: preparedImageForMetadata || undefined,
hideNavigation: !!data.hideNavigation != null ? !!data.hideNavigation : false,
});
await item.save(ctx.$context);
diff --git a/src/back/routes/admin/pages/create.form.ts b/src/back/routes/admin/pages/create.form.ts
index 6fed1e3..c0b6825 100644
--- a/src/back/routes/admin/pages/create.form.ts
+++ b/src/back/routes/admin/pages/create.form.ts
@@ -33,9 +33,6 @@ export default new (class PagesCRUDCreateForm extends Form
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"],
@@ -44,7 +41,7 @@ export default new (class PagesCRUDCreateForm extends Form
title: data["title"] != null ? data["title"] : "",
heading: data["heading"] != null ? data["heading"] : "",
description: data["description"] != null ? data["description"] : "",
- imageForMetadata: preparedImageForMetadata,
+ imageForMetadata: preparedImageForMetadata || undefined,
hideNavigation: !!data.hideNavigation != null ? !!data.hideNavigation : false,
});
}
diff --git a/src/back/routes/admin/pages/shared.ts b/src/back/routes/admin/pages/shared.ts
index 8c799f5..0ef10bf 100644
--- a/src/back/routes/admin/pages/shared.ts
+++ b/src/back/routes/admin/pages/shared.ts
@@ -16,10 +16,7 @@ export const PagesFormFields = {
Pages.fields.description.required,
Pages.fields.description
),
- imageForMetadata: new Fields.File(
- Pages.fields.imageForMetadata.required,
- TheFileManager
- ),
+ imageForMetadata: new Fields.File(false, TheFileManager),
hideNavigation: new Fields.Boolean(Pages.fields.hideNavigation.required),
};
diff --git a/src/back/routes/common/navbar.ts b/src/back/routes/common/navbar.ts
index 29797aa..853ce3f 100644
--- a/src/back/routes/common/navbar.ts
+++ b/src/back/routes/common/navbar.ts
@@ -1,6 +1,7 @@
import type { BaseContext } from "koa";
import { button } from "src/back/elements/button.js";
-import { tempstream, type FlatTemplatable } from "tempstream";
+import { tempstream } from "tempstream";
+import type { FlatTemplatable } from "tempstream";
export async function default_navbar(ctx: BaseContext): Promise {
const { items: navbar_items } = await ctx.$app.collections["navbar-links"]