import { type RefObject, useLayoutEffect, useRef, useState } from "react";
import { ApiError } from "../api/client.ts";
import { useClosedStatuses } from "../api/closed.ts";
import { useIssue, useSettings } from "../api/queries.ts";
import type { IssueComment, IssueDetail } from "../api/types.ts";
import { JIRA_URL } from "../board/constants.ts";
import { fmtDate, fmtStamp } from "../board/model/util.ts";
import { groupLinks, isOverdue, todayISO } from "./card.ts";
import { useCommentsHidden } from "./commentsPref.ts";
import { IssueLink } from "./IssueLink.tsx";
import { Badge, Button, IconButton, IconClose, IconExternal, Notice, Panel } from "../ui/index.ts";
import { Notes } from "./Notes.tsx";
import { dueOf } from "./options.ts";
import { WikiText } from "./wiki/WikiText.tsx";
import "./issues.css";
const DESC_LINES = 8;
export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClose: () => void }) {
const issue = useIssue(key);
const settings = useSettings();
const closed = useClosedStatuses();
const close = } onClick={onClose} />;
if (issue.isPending) {
return
{key}{close}
загрузка {key}…
;
}
if (issue.error instanceof ApiError && issue.error.status === 404) {
return (
Закрыть}>Задачи {key} нет в зеркале хаба.
);
}
if (issue.isError && !issue.data) {
return (
>}>
{issue.error.message}
);
}
const i = issue.data!;
const due = dueOf(i);
const facts = [
i.assignee && {i.assignee},
due && срок {fmtDate(due)}{isOverdue(due, i.status, closed, todayISO()) && <> просрочено>},
i.sprint && {i.sprint},
i.epic_key && эпик: {i.epic_summary ?? i.epic_key},
].filter(Boolean);
return (
);
}
function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record }) {
const groups = groupLinks(issue, linkTypes);
return (
Связи
{!groups.length && связей нет
}
{groups.map(([id, title, links]) => (
{title}
{links.map((l, n) => (
-
{l.tag && <>{l.tag}{" "}>}
{" "}
{l.summary === null && l.status === null
? нет в зеркале
: <>{l.summary}{l.status && <>{" "}{l.status}>}>}
))}
))}
);
}
// переключатель блока «исходник» / «текст»; выбор живёт до размонтирования карточки (design wiki-render #7)
function SourceToggle({ source, onToggle }: { source: boolean; onToggle: () => void }) {
return ;
}
function Description({ text, projectKey }: { text: string; projectKey?: string }) {
const ref = useRef(null);
const [open, setOpen] = useState(false);
const [source, setSource] = useState(false);
const [overflow, setOverflow] = useState(false);
useLayoutEffect(() => {
const el = ref.current;
if (!el || open) return;
const measure = () => setOverflow(el.scrollHeight > el.clientHeight + 1);
measure();
// карточка из кэша монтируется до showModal (размеры нулевые), окно меняет ширину, догружаются шрифты — перемер (C-1)
if (typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(measure);
ro.observe(el);
if (el.firstElementChild) ro.observe(el.firstElementChild);
return () => ro.disconnect();
}, [text, open, source]);
// в рендере «длинное» — только по измерению: пустые строки исходника высоты не дают (design #8)
const long = (source && text.split("\n").length > DESC_LINES) || overflow;
const cls = "description" + (open ? "" : " clamped");
return (
Описание
setSource(!source)} />
{source
? } className={cls}>{text}
: } className={cls}>
}
{(long || open) && }
);
}
function Comment({ c, projectKey }: { c: IssueComment; projectKey?: string }) {
const [source, setSource] = useState(false);
return (
{c.author ?? "—"}{c.created ? `, ${fmtStamp(c.created)}` : ""}
setSource(!source)} />
{source
? {c.body}
:
}
);
}
function Comments({ items, projectKey }: { items: IssueComment[]; projectKey?: string }) {
const [hidden, setHidden] = useCommentsHidden();
return (
Комментарии ({items.length})
{items.length > 0 && }
{!items.length && комментариев нет
}
{items.length > 0 && !hidden && (
)}
);
}
function OtherFields({ issue: i }: { issue: IssueDetail }) {
const rows: [string, string | null][] = [
["Тип", i.issue_type], ["Приоритет", i.priority], ["Автор", i.reporter], ["Метки", i.labels.join(", ") || null],
["Плановое начало", i.planned_start], ["Обновлена в Jira", i.jira_updated ? fmtStamp(i.jira_updated) : null],
["Сосед по связям", i.neighbor ? "да" : null],
];
return (
Прочие поля
{rows.filter(([, v]) => v).map(([k, v]) => - {k}
- {v}
)}
);
}
function Commands({ issue: i }: { issue: IssueDetail }) {
if (!i.commands.length) {
return Команды моста
команд по задаче не было
;
}
return (
Команды моста ({i.commands.length})
| Команда | Пакет | Тип | Статус | Завершена | Ошибка |
{i.commands.map((c) => (
| #{c.id} | #{c.batch_id} | {c.type} | {c.status} |
{c.finished_at ? fmtStamp(c.finished_at) : ""} | {c.error} |
))}
);
}