57 lines
3.2 KiB
TypeScript
57 lines
3.2 KiB
TypeScript
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>
|
||
);
|
||
}
|