From 8cb11bc2ff8a5a17f6b99bf5fdef0a46e12afe32 Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Sun, 2 Mar 2025 19:33:50 +0100 Subject: [PATCH] Admin improvements, editable navbar --- src/back/collections/collections.ts | 3 + src/back/collections/navbar-links.ts | 9 +++ src/back/collections/pages.ts | 21 +++++- src/back/routes/admin.page.tsx | 36 ++++++++++ .../routes/{index.test.ts => admin.test.ts} | 14 ++-- src/back/routes/admin/navbar/index.page.ts | 66 +++++++++++++++++++ .../admin/pages/[id]/content.jdd-editor.tsx | 4 +- src/back/routes/admin/pages/[id]/edit.form.ts | 5 +- .../admin/pages/[id]/edit.jdd-editor.tsx | 2 +- src/back/routes/admin/pages/create.form.ts | 2 +- src/back/routes/common/navbar.ts | 15 ++--- src/back/routes/index.page.tsx | 17 ----- src/back/routes/middlewares/customUrlView.tsx | 2 + 13 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 src/back/collections/navbar-links.ts create mode 100644 src/back/routes/admin.page.tsx rename src/back/routes/{index.test.ts => admin.test.ts} (79%) create mode 100644 src/back/routes/admin/navbar/index.page.ts delete mode 100644 src/back/routes/index.page.tsx diff --git a/src/back/collections/collections.ts b/src/back/collections/collections.ts index 13e8317..5a23f97 100644 --- a/src/back/collections/collections.ts +++ b/src/back/collections/collections.ts @@ -4,6 +4,7 @@ import { App } from "sealious"; import _Globals from "./globals.js"; import _GroupsToUsers from "./groups-to-users.js"; import _Groups from "./groups.js"; +import _NavbarLinks from "./navbar-links.js"; import _Pages from "./pages.js"; import _PasswordResetIntents from "./password-reset-intents.js"; import _Redirects from "./redirects.js"; @@ -13,6 +14,7 @@ import _Users from "./users.js"; export const Globals = new _Globals(); export const GroupsToUsers = new _GroupsToUsers(); export const Groups = new _Groups(); +export const NavbarLinks = new _NavbarLinks(); export const Pages = new _Pages(); export const PasswordResetIntents = new _PasswordResetIntents(); export const Redirects = new _Redirects(); @@ -24,6 +26,7 @@ export const collections = { globals: Globals, "groups-to-users": GroupsToUsers, groups: Groups, + "navbar-links": NavbarLinks, pages: Pages, "password-reset-intents": PasswordResetIntents, redirects: Redirects, diff --git a/src/back/collections/navbar-links.ts b/src/back/collections/navbar-links.ts new file mode 100644 index 0000000..2d521b9 --- /dev/null +++ b/src/back/collections/navbar-links.ts @@ -0,0 +1,9 @@ +import { Collection, FieldTypes, Policies } from "sealious"; + +export default class NavbarLinks extends Collection { + fields = { + label: new FieldTypes.Text(), + href: new FieldTypes.Text(), + }; + defaultPolicy = new Policies.Public(); +} diff --git a/src/back/collections/pages.ts b/src/back/collections/pages.ts index 1116d8f..1dfa295 100644 --- a/src/back/collections/pages.ts +++ b/src/back/collections/pages.ts @@ -1,5 +1,7 @@ -import { Collection, FieldTypes, Policies } from "sealious"; +import { App, Collection, FieldTypes, Policies } from "sealious"; import { CRUDRoles } from "../policy-types/roles.js"; +import assert from "assert"; +import TheApp from "../app.js"; export default class Pages extends Collection { fields = { @@ -9,8 +11,23 @@ export default class Pages extends Collection { title: new FieldTypes.Text(), heading: new FieldTypes.Text(), description: new FieldTypes.Text(), - imageForMetadata: new FieldTypes.Image().setRequired(true), + imageForMetadata: new FieldTypes.Image(), hideNavigation: new FieldTypes.Boolean(), }; policies = CRUDRoles(["pages"], { show: new Policies.Public() }); + + async init(app: App, name: string) { + assert(app instanceof TheApp); + await super.init(app, name); + app.on("started", async () => { + const all_pages = await app.collections.pages.suList().fetch(); + if (all_pages.empty) { + await app.collections.pages.suCreate({ + url: "/", + content: [], + title: "Homepage", + }); + } + }); + } } diff --git a/src/back/routes/admin.page.tsx b/src/back/routes/admin.page.tsx new file mode 100644 index 0000000..4c0c6a1 --- /dev/null +++ b/src/back/routes/admin.page.tsx @@ -0,0 +1,36 @@ +import type { Context } from "koa"; +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"; + +export const actionName = "Admin"; + +export default new (class AdminPage extends Page { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async canAccess(_: Context) { + return { canAccess: true, message: "" }; + } + + async render(ctx: Context) { + if (!ctx.$context.user_id) { + ctx.redirect(LoginURL); + ctx.status = 303; + return ""; + } + return html({ + ctx: ctx, + title: "Admin", + body: ( +
+ {[ + { text: "Pages", href: PagesCRUDListURL }, + { text: "Navbar", href: NavbarLinksCRUDURL }, + ].map((e) => button({ ...e, variant: "accent1" }))} +
+ ), + description: "", + }); + } +})(); diff --git a/src/back/routes/index.test.ts b/src/back/routes/admin.test.ts similarity index 79% rename from src/back/routes/index.test.ts rename to src/back/routes/admin.test.ts index 7b840ab..4fe6fe3 100644 --- a/src/back/routes/index.test.ts +++ b/src/back/routes/admin.test.ts @@ -1,23 +1,23 @@ import { withProdApp } from "../test_utils/with-prod-app.js"; import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js"; -import { HomeURL } from "./urls.js"; +import { AdminURL } from "./urls.js"; import { getBrowser } from "../test_utils/browser-creator.js"; import type { Browser, BrowserContext, Page } from "@playwright/test"; -describe("Home webhint", () => { +describe("Admin webhint", () => { it("doesn't crash", async function () { return withProdApp(async ({ base_url, rest_api }) => { - await rest_api.get(HomeURL); - await webhintURL(base_url + HomeURL); + await rest_api.get(AdminURL); + await webhintURL(base_url + AdminURL); // alternatively you can use webhintHTML for faster but less precise scans // or for scanning responses of requests that use some form of authorization: - // const response = await rest_api.get(HomeURL); + // const response = await rest_api.get(AdminURL); // await webhintHTML(response); }); }).timeout(VERY_LONG_TEST_TIMEOUT); }); -describe("Home", () => { +describe("Admin", () => { let page: Page; let browser: Browser; let context: BrowserContext; @@ -34,7 +34,7 @@ describe("Home", () => { it("works as expected", async function () { return withProdApp(async ({ base_url }) => { - await page.goto(base_url + HomeURL); + await page.goto(base_url + AdminURL); }); }).timeout(VERY_LONG_TEST_TIMEOUT); }); diff --git a/src/back/routes/admin/navbar/index.page.ts b/src/back/routes/admin/navbar/index.page.ts new file mode 100644 index 0000000..1ef2e59 --- /dev/null +++ b/src/back/routes/admin/navbar/index.page.ts @@ -0,0 +1,66 @@ +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: "NavbarLinks list", + 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/pages/[id]/content.jdd-editor.tsx b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx index f30fa0a..f82a44d 100644 --- a/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx +++ b/src/back/routes/admin/pages/[id]/content.jdd-editor.tsx @@ -7,6 +7,7 @@ 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"; +import { PagesCRUDListURL } from "src/back/routes/urls.js"; export const actionName = "EditPageContent"; @@ -23,7 +24,8 @@ export default new (class JDDCreatePreviewPage extends EditJDDField { const item = await this.getItem(ctx); return (
-

Edit pages: {item.id}

{" "} + ← back to pages +

Edit page: {item.get("title")}

{" "}
); } diff --git a/src/back/routes/admin/pages/[id]/edit.form.ts b/src/back/routes/admin/pages/[id]/edit.form.ts index cce5c7a..9d7d043 100644 --- a/src/back/routes/admin/pages/[id]/edit.form.ts +++ b/src/back/routes/admin/pages/[id]/edit.form.ts @@ -48,13 +48,14 @@ export default new (class PagesCRUDEditForm extends Form { throw new Error("Item with given id not found: " + id); } + const imageForMetadata = item.get("imageForMetadata"); 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") }, + imageForMetadata: imageForMetadata ? { old: imageForMetadata } : "", hideNavigation: withFallback( item.get("hideNavigation"), String(item.get("hideNavigation")), @@ -79,7 +80,7 @@ export default new (class PagesCRUDEditForm extends Form { } const preparedImageForMetadata = - data.imageForMetadata.new || data.imageForMetadata.old; + data.imageForMetadata?.new || data.imageForMetadata?.old; if (!preparedImageForMetadata) { throw new Error("Missing field: imageForMetadata"); } diff --git a/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx b/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx index 88df822..4a92e59 100644 --- a/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx +++ b/src/back/routes/admin/pages/[id]/edit.jdd-editor.tsx @@ -23,7 +23,7 @@ export default new (class JDDCreatePreviewPage extends EditJDDField { const item = await this.getItem(ctx); return (
-

Edit pages: {item.id}

{" "} +

Edit page: {item.get("title")}

{" "}
); } diff --git a/src/back/routes/admin/pages/create.form.ts b/src/back/routes/admin/pages/create.form.ts index 01de0fa..fd085bb 100644 --- a/src/back/routes/admin/pages/create.form.ts +++ b/src/back/routes/admin/pages/create.form.ts @@ -32,7 +32,7 @@ export default new (class PagesCRUDCreateForm extends Form } const preparedImageForMetadata = - data.imageForMetadata.new || data.imageForMetadata.old; + data.imageForMetadata?.new || data.imageForMetadata?.old; if (!preparedImageForMetadata) { throw new Error("Missing field: imageForMetadata"); } diff --git a/src/back/routes/common/navbar.ts b/src/back/routes/common/navbar.ts index ec81164..91be1e5 100644 --- a/src/back/routes/common/navbar.ts +++ b/src/back/routes/common/navbar.ts @@ -3,14 +3,13 @@ import type { FlatTemplatable } from "tempstream"; import { SignInURL, LogoutURL, PagesCRUDListURL } from "../urls.js"; export async function default_navbar(ctx: BaseContext): Promise { - const isLoggedIn = !!ctx.$context.session_id; - - const linkData = isLoggedIn - ? [ - { text: "Pages", url: PagesCRUDListURL }, - { text: "Logout", url: LogoutURL }, - ] - : [{ text: "Sign in", url: SignInURL }]; + const { items: navbar_items } = await ctx.$app.collections["navbar-links"] + .list(ctx.$context) + .fetch(); + const linkData = navbar_items.map((e) => ({ + text: e.get("label"), + url: e.get("href"), + })); const linksHTML = linkData .map((link) => diff --git a/src/back/routes/index.page.tsx b/src/back/routes/index.page.tsx deleted file mode 100644 index 3d176ea..0000000 --- a/src/back/routes/index.page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Context } from "koa"; -import { TempstreamJSX } from "tempstream"; -import { Page } from "@sealcode/sealgen"; -import html from "../html.js"; - -export const actionName = "Home"; - -export default new (class HomePage extends Page { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async canAccess(_: Context) { - return { canAccess: true, message: "" }; - } - - async render(ctx: Context) { - return html({ ctx: ctx, title: "Home", body:
Hello
, description: "" }); - } -})(); diff --git a/src/back/routes/middlewares/customUrlView.tsx b/src/back/routes/middlewares/customUrlView.tsx index 8974d3e..6bcfe0a 100644 --- a/src/back/routes/middlewares/customUrlView.tsx +++ b/src/back/routes/middlewares/customUrlView.tsx @@ -20,7 +20,9 @@ interface ContextState { export const customUrlView = (app: TheApp) => async (ctx: Context & ContextState, next: Next) => { try { + console.log("CUSTOM URL VIEW"); if (ctx.body) return; + console.log("doesn't have body"); const main_domain = new URL(BASE_URL).hostname; const { items: [page],