- ApiKeyCreationModal: required Alias input (reuse alias = renew, with banner); align roles to backend allowlist (guest -> user, default user); fix copy bug (generatedKey.key). - MarkdownContent + PatchCards: show author / created / last modified (+ by whom); formatDateTime helper (null -> "—"). Branched off fix/buildconfig-cachebust (carries the contenthash + BuildConfig fix already deployed to prod). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
5.2 KiB
JavaScript
118 lines
5.2 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import {Link, useParams} from "react-router-dom";
|
|
import "katex/dist/katex.min.css";
|
|
import "./MarkdownContent.css";
|
|
import { Settings2, Pencil, User, Clock, History } from "lucide-react";
|
|
import { formatDateTime } from "../../lib/utils";
|
|
import MarkdownView from "./MarkdownView";
|
|
import PatchCards from "./PatchCards";
|
|
import PermissionGuard from "../PermissionGuard";
|
|
import {useMarkdown} from "../../utils/queries/markdown-queries";
|
|
import {usePath} from "../../utils/queries/path-queries";
|
|
import {useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
|
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
|
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
|
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
|
import { parseMarkdownContent } from "../../utils/safe-json";
|
|
import { Button } from "../ui/button";
|
|
import { Spinner } from "../ui/misc";
|
|
|
|
const MarkdownContent = () => {
|
|
const { strId } = useParams();
|
|
const id = Number(strId);
|
|
const [indexTitle, setIndexTitle] = useState(null);
|
|
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
|
|
const {data: markdown, isLoading, error} = useMarkdown(id);
|
|
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
|
|
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
|
|
const {data: templateSetting, isFetching: isTemplateSettingFetching} = useMarkdownTemplateSetting(setting?.template_setting_id);
|
|
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(templateSetting?.template_id);
|
|
|
|
useEffect(() => {
|
|
if(markdown && markdown.title === "index" && path){
|
|
setIndexTitle(path.id === 1 ? "Home" : path.name);
|
|
}
|
|
}, [markdown, path]);
|
|
|
|
|
|
const notReady = isLoading || isPathFetching || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
|
|
|
|
if (notReady) {
|
|
return (
|
|
<div className="flex justify-center py-20">
|
|
<Spinner label="Loading content" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="rounded-lg border border-destructive/40 bg-destructive/10 px-4 py-3 font-mono text-sm text-destructive">
|
|
Error: {error.message || "Failed to load content"}
|
|
</div>
|
|
);
|
|
}
|
|
if (markdown.isMessage) {
|
|
return (
|
|
<div className="rounded-lg border border-primary/30 bg-primary/10 px-5 py-4">
|
|
<h4 className="mb-1 font-mono text-base font-semibold text-primary">
|
|
{markdown.title}
|
|
</h4>
|
|
<p className="text-sm text-foreground/80">{markdown.content}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<article>
|
|
<div className="mb-6 flex items-start justify-between gap-4 border-b border-border pb-4">
|
|
<h1 className="font-mono text-3xl font-bold tracking-tight text-foreground">
|
|
{markdown.title === "index" ? indexTitle : markdown.title}
|
|
</h1>
|
|
<PermissionGuard rolesRequired={['admin']}>
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setSettingModalOpen(true)}
|
|
>
|
|
<Settings2 className="h-4 w-4" /> Settings
|
|
</Button>
|
|
<Button asChild size="sm">
|
|
<Link to={`/markdown/edit/${id}`}>
|
|
<Pencil className="h-4 w-4" /> Edit
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</PermissionGuard>
|
|
</div>
|
|
<div className="-mt-3 mb-6 flex flex-wrap items-center gap-x-5 gap-y-1 font-mono text-xs text-muted-foreground">
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<User className="h-3.5 w-3.5 text-secondary" />
|
|
{markdown.author || "—"}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<Clock className="h-3.5 w-3.5" />
|
|
created {formatDateTime(markdown.created_at)}
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<History className="h-3.5 w-3.5" />
|
|
updated {formatDateTime(markdown.updated_at)}
|
|
{markdown.last_modified_by
|
|
? ` by ${markdown.last_modified_by}`
|
|
: ""}
|
|
</span>
|
|
</div>
|
|
<MarkdownView content={parseMarkdownContent(markdown.content)} template={template}/>
|
|
<PatchCards markdownId={id} />
|
|
<MarkdownSettingModal
|
|
isOpen={isSettingModalOpen}
|
|
markdown={markdown}
|
|
onClose={() => setSettingModalOpen(false)}
|
|
/>
|
|
</article>
|
|
);
|
|
};
|
|
|
|
export default MarkdownContent;
|