sdlc-hub/web/src/modules/wiki/PageView.tsx
Andrey Epifancev 5a142f772c Веб: вики — правки по ревью Task 10 (#91)
Дерево не раскрывает предков повторно после ручного сворачивания, предел опроса вложения,
//host не ссылка, регистр схем page:/attachment:, тесты дерева, загрузки, плашки панели, 422/404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 03:17:13 +04:00

71 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>}
</>
);
}