Дерево не раскрывает предков повторно после ручного сворачивания, предел опроса вложения, //host не ссылка, регистр схем page:/attachment:, тесты дерева, загрузки, плашки панели, 422/404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
4.1 KiB
TypeScript
71 lines
4.1 KiB
TypeScript
import { useMemo, type ComponentProps, type ReactNode } from "react";
|
||
import Markdown, { type Components } from "react-markdown";
|
||
import { Link } from "react-router";
|
||
import remarkGfm from "remark-gfm";
|
||
import { Notice, IconExternal, IconPaperclip } from "../../ui/index.ts";
|
||
import { fmtStamp } from "../jira/board/model/util.ts";
|
||
import { useWikiPage } from "./api/queries.ts";
|
||
import { useWikiDownload } from "./download.ts";
|
||
import { wikiUrlTransform } from "./urlTransform.ts";
|
||
|
||
const PLUGINS = [remarkGfm];
|
||
const safeUrl = (u: string | null) => (u && /^https?:\/\//i.test(u.trim()) ? u : null);
|
||
// копия начинается заголовком «# название» — он повторяет заголовок страницы и в тексте не нужен
|
||
function withoutTitle(md: string, title: string | null): string {
|
||
const m = /^# ([^\n]*)\n+/.exec(md);
|
||
return m && title !== null && m[1].trim() === title.split(/\s+/).filter(Boolean).join(" ") ? md.slice(m[0].length) : md;
|
||
}
|
||
const decode = (s: string) => { try { return decodeURIComponent(s); } catch { return s; } };
|
||
|
||
// страница вики: заголовок, дата синхронизации, «открыть в Eva» и Markdown (таблицы GFM, без сырого HTML)
|
||
export function PageView({ project, code, hrefOf }: { project: string; code: string; hrefOf(code: string): string }) {
|
||
const page = useWikiPage(project, code);
|
||
const dl = useWikiDownload(project, code);
|
||
|
||
const components = useMemo<Components>(() => ({
|
||
a({ href, children }: ComponentProps<"a">) {
|
||
if (!href) return <>{children}</>;
|
||
if (/^page:/i.test(href)) return <Link to={hrefOf(decode(href.slice(5)))}>{children}</Link>;
|
||
if (/^attachment:/i.test(href)) return <AttachmentLink id={decode(href.slice(11))} dl={dl}>{children}</AttachmentLink>;
|
||
if (/^https?:/i.test(href)) return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>;
|
||
return <a href={href}>{children}</a>;
|
||
},
|
||
// картинки по внешним адресам не загружаются (трекинг, чужие ресурсы): виден только текст
|
||
img: ({ alt }: ComponentProps<"img">) => <span className="wiki-img">[изображение{alt ? `: ${alt}` : ""}]</span>,
|
||
table: ({ children }: ComponentProps<"table">) => <div className="wiki-table"><table>{children}</table></div>,
|
||
}), [hrefOf, dl]);
|
||
|
||
if (page.isPending) return <p className="wiki-hint">Загрузка страницы…</p>;
|
||
if (page.isError) return <Notice tone="error">{page.error.message}</Notice>;
|
||
const d = page.data;
|
||
const eva = safeUrl(d.url);
|
||
return (
|
||
<article className="wiki-page" aria-labelledby="wiki-page-title">
|
||
<header className="wiki-page__head">
|
||
<h2 id="wiki-page-title" className="wiki-page__title">{d.title || d.code}</h2>
|
||
<p className="wiki-page__meta">
|
||
<span className="wiki-page__code">{d.code}</span>
|
||
{d.synced_at && <span>синхронизировано {fmtStamp(d.synced_at)}</span>}
|
||
{eva && <a href={eva} target="_blank" rel="noopener noreferrer" className="wiki-page__eva"><IconExternal size={16} />открыть в Eva</a>}
|
||
</p>
|
||
</header>
|
||
<div className="wiki-md">
|
||
<Markdown remarkPlugins={PLUGINS} urlTransform={wikiUrlTransform} components={components}>{withoutTitle(d.markdown, d.title)}</Markdown>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|
||
|
||
// ссылка attachment:<id>: запрос на скачивание, ожидание, сохранение файла; ошибка — текстом рядом
|
||
function AttachmentLink({ id, dl, children }: { id: string; dl: ReturnType<typeof useWikiDownload>; children?: ReactNode }) {
|
||
const { busy, error } = dl.status(id);
|
||
return (
|
||
<>
|
||
<button type="button" className="wiki-att" disabled={busy} onClick={() => dl.start(id)}>
|
||
<IconPaperclip size={16} />{children}{busy && <span className="wiki-att__busy"> Загружается…</span>}
|
||
</button>
|
||
{error && <span className="wiki-att__error" role="alert"> {error}</span>}
|
||
</>
|
||
);
|
||
}
|