Andrey Epifancev 2cb454af8b refactor(web): модуль jira и оболочка из списка модулей (#87)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 23:05:02 +04:00

57 lines
3.2 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 type { IssueAttachment } from "../../../../api/types.ts";
import { fmtStamp } from "../../board/model/util.ts";
import { Badge, Button, IconDownload, IconExternal, IconFile, IconImage, IconSheet } from "../../../../ui/index.ts";
import { fileKind, fmtSize, isTooBig, jiraAttachmentUrl, TOO_BIG_HINT } from "./format.ts";
import type { useAttachmentDownload } from "./useAttachmentDownload.ts";
import "./attachments.css";
export type Download = ReturnType<typeof useAttachmentDownload>;
const KIND_ICON = { image: IconImage, sheet: IconSheet, pdf: IconFile, other: IconFile };
const KIND_LABEL = { image: "картинка", sheet: "таблица", pdf: "PDF", other: "файл" };
// новые сверху: по времени Jira, затем по числовому id
export function newestFirst(items: IssueAttachment[]): IssueAttachment[] {
const t = (a: IssueAttachment) => (a.created ? Date.parse(a.created) || 0 : 0);
return [...items].sort((a, b) => t(b) - t(a) || (Number(b.attachment_id) || 0) - (Number(a.attachment_id) || 0));
}
// раздел «Вложения (N)»: значок по типу, имя, размер, «автор · дата», отметка «у хаба», кнопка скачивания
export function AttachmentList({ items, dl }: { items: IssueAttachment[]; dl: Download }) {
return (
<section className="attachments" data-testid="attachments">
<div className="sec-head"><h3>Вложения ({items.length})</h3></div>
<ul className="att-list">
{newestFirst(items).map((a) => <Row key={a.attachment_id} a={a} dl={dl} />)}
</ul>
</section>
);
}
function Row({ a, dl }: { a: IssueAttachment; dl: Download }) {
const kind = fileKind(a.mime_type, a.filename);
const Icon = KIND_ICON[kind];
const { busy, error: local } = dl.status(a.attachment_id);
// ошибка: своя (последняя попытка в этой карточке) или команда, закончившаяся failed до открытия карточки
const error = busy ? null : local ?? (a.state === "failed" ? a.error ?? "не удалось скачать вложение" : null);
const who = [a.author, a.created ? fmtStamp(a.created) : null].filter(Boolean).join(" · ");
return (
<li className="att-row">
<span className="att-icon" role="img" aria-label={KIND_LABEL[kind]}><Icon /></span>
<div className="att-main">
<span className="att-name">{a.filename}</span>
<span className="att-meta">
{fmtSize(a.size)}{who && ` · ${who}`}
{a.state === "ready" && <>{" "}<Badge tone="success">у хаба</Badge></>}
</span>
{error && <p className="att-error" role="alert">{error}</p>}
</div>
{isTooBig(a.size)
? <a className="ui-btn ui-btn--secondary ui-btn--md" href={jiraAttachmentUrl(a.attachment_id, a.filename)} target="_blank" rel="noopener noreferrer" title={TOO_BIG_HINT}><IconExternal size={16} />Открыть в Jira</a>
: <Button icon={busy ? undefined : <IconDownload size={16} />} loading={busy} onClick={() => dl.start(a)}>
{busy ? "Загружается…" : error ? "Повторить" : "Скачать"}
</Button>}
</li>
);
}