Admin improvements, editable navbar
This commit is contained in:
parent
0bf41032ad
commit
8cb11bc2ff
@ -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,
|
||||
|
9
src/back/collections/navbar-links.ts
Normal file
9
src/back/collections/navbar-links.ts
Normal file
@ -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();
|
||||
}
|
@ -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",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
36
src/back/routes/admin.page.tsx
Normal file
36
src/back/routes/admin.page.tsx
Normal file
@ -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: (
|
||||
<div>
|
||||
{[
|
||||
{ text: "Pages", href: PagesCRUDListURL },
|
||||
{ text: "Navbar", href: NavbarLinksCRUDURL },
|
||||
].map((e) => button({ ...e, variant: "accent1" }))}
|
||||
</div>
|
||||
),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
})();
|
@ -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);
|
||||
});
|
66
src/back/routes/admin/navbar/index.page.ts
Normal file
66
src/back/routes/admin/navbar/index.page.ts
Normal file
@ -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 = <const>{
|
||||
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<typeof NavbarLinks>
|
||||
) => string | Promise<string>;
|
||||
}[];
|
||||
|
||||
// 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 () => {},
|
||||
});
|
@ -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<Pages> {
|
||||
const item = await this.getItem(ctx);
|
||||
return (
|
||||
<div>
|
||||
<h1>Edit pages: {item.id}</h1>{" "}
|
||||
<a href={PagesCRUDListURL}>← back to pages </a>
|
||||
<h1>Edit page: {item.get("title")}</h1>{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -48,13 +48,14 @@ export default new (class PagesCRUDEditForm extends Form<typeof fields, void> {
|
||||
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<typeof fields, void> {
|
||||
}
|
||||
|
||||
const preparedImageForMetadata =
|
||||
data.imageForMetadata.new || data.imageForMetadata.old;
|
||||
data.imageForMetadata?.new || data.imageForMetadata?.old;
|
||||
if (!preparedImageForMetadata) {
|
||||
throw new Error("Missing field: imageForMetadata");
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export default new (class JDDCreatePreviewPage extends EditJDDField<Pages> {
|
||||
const item = await this.getItem(ctx);
|
||||
return (
|
||||
<div>
|
||||
<h1>Edit pages: {item.id}</h1>{" "}
|
||||
<h1>Edit page: {item.get("title")}</h1>{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export default new (class PagesCRUDCreateForm extends Form<typeof fields, void>
|
||||
}
|
||||
|
||||
const preparedImageForMetadata =
|
||||
data.imageForMetadata.new || data.imageForMetadata.old;
|
||||
data.imageForMetadata?.new || data.imageForMetadata?.old;
|
||||
if (!preparedImageForMetadata) {
|
||||
throw new Error("Missing field: imageForMetadata");
|
||||
}
|
||||
|
@ -3,14 +3,13 @@ import type { FlatTemplatable } from "tempstream";
|
||||
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: "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) =>
|
||||
|
@ -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: <div>Hello</div>, description: "" });
|
||||
}
|
||||
})();
|
@ -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],
|
||||
|
Loading…
x
Reference in New Issue
Block a user