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 _Globals from "./globals.js";
|
||||||
import _GroupsToUsers from "./groups-to-users.js";
|
import _GroupsToUsers from "./groups-to-users.js";
|
||||||
import _Groups from "./groups.js";
|
import _Groups from "./groups.js";
|
||||||
|
import _NavbarLinks from "./navbar-links.js";
|
||||||
import _Pages from "./pages.js";
|
import _Pages from "./pages.js";
|
||||||
import _PasswordResetIntents from "./password-reset-intents.js";
|
import _PasswordResetIntents from "./password-reset-intents.js";
|
||||||
import _Redirects from "./redirects.js";
|
import _Redirects from "./redirects.js";
|
||||||
@ -13,6 +14,7 @@ import _Users from "./users.js";
|
|||||||
export const Globals = new _Globals();
|
export const Globals = new _Globals();
|
||||||
export const GroupsToUsers = new _GroupsToUsers();
|
export const GroupsToUsers = new _GroupsToUsers();
|
||||||
export const Groups = new _Groups();
|
export const Groups = new _Groups();
|
||||||
|
export const NavbarLinks = new _NavbarLinks();
|
||||||
export const Pages = new _Pages();
|
export const Pages = new _Pages();
|
||||||
export const PasswordResetIntents = new _PasswordResetIntents();
|
export const PasswordResetIntents = new _PasswordResetIntents();
|
||||||
export const Redirects = new _Redirects();
|
export const Redirects = new _Redirects();
|
||||||
@ -24,6 +26,7 @@ export const collections = {
|
|||||||
globals: Globals,
|
globals: Globals,
|
||||||
"groups-to-users": GroupsToUsers,
|
"groups-to-users": GroupsToUsers,
|
||||||
groups: Groups,
|
groups: Groups,
|
||||||
|
"navbar-links": NavbarLinks,
|
||||||
pages: Pages,
|
pages: Pages,
|
||||||
"password-reset-intents": PasswordResetIntents,
|
"password-reset-intents": PasswordResetIntents,
|
||||||
redirects: Redirects,
|
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 { CRUDRoles } from "../policy-types/roles.js";
|
||||||
|
import assert from "assert";
|
||||||
|
import TheApp from "../app.js";
|
||||||
|
|
||||||
export default class Pages extends Collection {
|
export default class Pages extends Collection {
|
||||||
fields = {
|
fields = {
|
||||||
@ -9,8 +11,23 @@ export default class Pages extends Collection {
|
|||||||
title: new FieldTypes.Text(),
|
title: new FieldTypes.Text(),
|
||||||
heading: new FieldTypes.Text(),
|
heading: new FieldTypes.Text(),
|
||||||
description: new FieldTypes.Text(),
|
description: new FieldTypes.Text(),
|
||||||
imageForMetadata: new FieldTypes.Image().setRequired(true),
|
imageForMetadata: new FieldTypes.Image(),
|
||||||
hideNavigation: new FieldTypes.Boolean(),
|
hideNavigation: new FieldTypes.Boolean(),
|
||||||
};
|
};
|
||||||
policies = CRUDRoles(["pages"], { show: new Policies.Public() });
|
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 { withProdApp } from "../test_utils/with-prod-app.js";
|
||||||
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.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 { getBrowser } from "../test_utils/browser-creator.js";
|
||||||
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
import type { Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
|
||||||
describe("Home webhint", () => {
|
describe("Admin webhint", () => {
|
||||||
it("doesn't crash", async function () {
|
it("doesn't crash", async function () {
|
||||||
return withProdApp(async ({ base_url, rest_api }) => {
|
return withProdApp(async ({ base_url, rest_api }) => {
|
||||||
await rest_api.get(HomeURL);
|
await rest_api.get(AdminURL);
|
||||||
await webhintURL(base_url + HomeURL);
|
await webhintURL(base_url + AdminURL);
|
||||||
// alternatively you can use webhintHTML for faster but less precise scans
|
// alternatively you can use webhintHTML for faster but less precise scans
|
||||||
// or for scanning responses of requests that use some form of authorization:
|
// 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);
|
// await webhintHTML(response);
|
||||||
});
|
});
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Home", () => {
|
describe("Admin", () => {
|
||||||
let page: Page;
|
let page: Page;
|
||||||
let browser: Browser;
|
let browser: Browser;
|
||||||
let context: BrowserContext;
|
let context: BrowserContext;
|
||||||
@ -34,7 +34,7 @@ describe("Home", () => {
|
|||||||
|
|
||||||
it("works as expected", async function () {
|
it("works as expected", async function () {
|
||||||
return withProdApp(async ({ base_url }) => {
|
return withProdApp(async ({ base_url }) => {
|
||||||
await page.goto(base_url + HomeURL);
|
await page.goto(base_url + AdminURL);
|
||||||
});
|
});
|
||||||
}).timeout(VERY_LONG_TEST_TIMEOUT);
|
}).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 { registry } from "src/back/jdd-components/registry.js";
|
||||||
import { makeJDDContext } from "src/back/jdd-context.js";
|
import { makeJDDContext } from "src/back/jdd-context.js";
|
||||||
import { defaultHead } from "src/back/defaultHead.js";
|
import { defaultHead } from "src/back/defaultHead.js";
|
||||||
|
import { PagesCRUDListURL } from "src/back/routes/urls.js";
|
||||||
|
|
||||||
export const actionName = "EditPageContent";
|
export const actionName = "EditPageContent";
|
||||||
|
|
||||||
@ -23,7 +24,8 @@ export default new (class JDDCreatePreviewPage extends EditJDDField<Pages> {
|
|||||||
const item = await this.getItem(ctx);
|
const item = await this.getItem(ctx);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Edit pages: {item.id}</h1>{" "}
|
<a href={PagesCRUDListURL}>← back to pages </a>
|
||||||
|
<h1>Edit page: {item.get("title")}</h1>{" "}
|
||||||
</div>
|
</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);
|
throw new Error("Item with given id not found: " + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageForMetadata = item.get("imageForMetadata");
|
||||||
return {
|
return {
|
||||||
url: item.get("url"),
|
url: item.get("url"),
|
||||||
domain: withFallback(item.get("domain"), ""),
|
domain: withFallback(item.get("domain"), ""),
|
||||||
title: withFallback(item.get("title"), ""),
|
title: withFallback(item.get("title"), ""),
|
||||||
heading: withFallback(item.get("heading"), ""),
|
heading: withFallback(item.get("heading"), ""),
|
||||||
description: withFallback(item.get("description"), ""),
|
description: withFallback(item.get("description"), ""),
|
||||||
imageForMetadata: { old: item.get("imageForMetadata") },
|
imageForMetadata: imageForMetadata ? { old: imageForMetadata } : "",
|
||||||
hideNavigation: withFallback(
|
hideNavigation: withFallback(
|
||||||
item.get("hideNavigation"),
|
item.get("hideNavigation"),
|
||||||
String(item.get("hideNavigation")),
|
String(item.get("hideNavigation")),
|
||||||
@ -79,7 +80,7 @@ export default new (class PagesCRUDEditForm extends Form<typeof fields, void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preparedImageForMetadata =
|
const preparedImageForMetadata =
|
||||||
data.imageForMetadata.new || data.imageForMetadata.old;
|
data.imageForMetadata?.new || data.imageForMetadata?.old;
|
||||||
if (!preparedImageForMetadata) {
|
if (!preparedImageForMetadata) {
|
||||||
throw new Error("Missing field: imageForMetadata");
|
throw new Error("Missing field: imageForMetadata");
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default new (class JDDCreatePreviewPage extends EditJDDField<Pages> {
|
|||||||
const item = await this.getItem(ctx);
|
const item = await this.getItem(ctx);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Edit pages: {item.id}</h1>{" "}
|
<h1>Edit page: {item.get("title")}</h1>{" "}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export default new (class PagesCRUDCreateForm extends Form<typeof fields, void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const preparedImageForMetadata =
|
const preparedImageForMetadata =
|
||||||
data.imageForMetadata.new || data.imageForMetadata.old;
|
data.imageForMetadata?.new || data.imageForMetadata?.old;
|
||||||
if (!preparedImageForMetadata) {
|
if (!preparedImageForMetadata) {
|
||||||
throw new Error("Missing field: imageForMetadata");
|
throw new Error("Missing field: imageForMetadata");
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,13 @@ import type { FlatTemplatable } from "tempstream";
|
|||||||
import { SignInURL, LogoutURL, PagesCRUDListURL } from "../urls.js";
|
import { SignInURL, LogoutURL, PagesCRUDListURL } from "../urls.js";
|
||||||
|
|
||||||
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
|
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
|
||||||
const isLoggedIn = !!ctx.$context.session_id;
|
const { items: navbar_items } = await ctx.$app.collections["navbar-links"]
|
||||||
|
.list(ctx.$context)
|
||||||
const linkData = isLoggedIn
|
.fetch();
|
||||||
? [
|
const linkData = navbar_items.map((e) => ({
|
||||||
{ text: "Pages", url: PagesCRUDListURL },
|
text: e.get("label"),
|
||||||
{ text: "Logout", url: LogoutURL },
|
url: e.get("href"),
|
||||||
]
|
}));
|
||||||
: [{ text: "Sign in", url: SignInURL }];
|
|
||||||
|
|
||||||
const linksHTML = linkData
|
const linksHTML = linkData
|
||||||
.map((link) =>
|
.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 =
|
export const customUrlView =
|
||||||
(app: TheApp) => async (ctx: Context & ContextState, next: Next) => {
|
(app: TheApp) => async (ctx: Context & ContextState, next: Next) => {
|
||||||
try {
|
try {
|
||||||
|
console.log("CUSTOM URL VIEW");
|
||||||
if (ctx.body) return;
|
if (ctx.body) return;
|
||||||
|
console.log("doesn't have body");
|
||||||
const main_domain = new URL(BASE_URL).hostname;
|
const main_domain = new URL(BASE_URL).hostname;
|
||||||
const {
|
const {
|
||||||
items: [page],
|
items: [page],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user