59 lines
3.7 KiB
TypeScript
59 lines
3.7 KiB
TypeScript
import { memo, type PointerEvent } from "react";
|
||
import { Link } from "react-router";
|
||
import { BOARD_CARD_STATE } from "../../issues/modal.ts";
|
||
import { CW, JIRA_URL, KIND, PRIO } from "../constants.ts";
|
||
import { lineClamp, type Rect, type RelBox } from "../layout/layout.ts";
|
||
import { fmtDate, shortName, unassigned } from "../model/util.ts";
|
||
import type { VisNode } from "../types.ts";
|
||
|
||
export interface CardProps {
|
||
node: VisNode;
|
||
box?: Rect; // нет — карточка слоя измерения (ширина CW, высота по содержимому)
|
||
rel?: RelBox;
|
||
multiSprint: boolean;
|
||
sprintSel: Set<string>;
|
||
onPointerDown?: (ev: PointerEvent<HTMLDivElement>, key: string) => void;
|
||
onAutoSize?: (key: string) => void;
|
||
}
|
||
|
||
// T11.1: при перетаскивании одной карточки place() создаёт новые box у всех — сравниваем box по значению
|
||
export function cardPropsEqual(a: CardProps, b: CardProps): boolean {
|
||
const sameBox = a.box === b.box || (!!a.box && !!b.box && a.box.x === b.box.x && a.box.y === b.box.y && a.box.w === b.box.w && a.box.h === b.box.h);
|
||
return sameBox && a.node === b.node && a.rel === b.rel && a.multiSprint === b.multiSprint && a.sprintSel === b.sprintSel &&
|
||
a.onPointerDown === b.onPointerDown && a.onAutoSize === b.onAutoSize;
|
||
}
|
||
|
||
export const Card = memo(function Card({ node: n, box, rel, multiSprint, sprintSel, onPointerDown, onAutoSize }: CardProps) {
|
||
const cls = "card" + (n.dim || n.stub ? " out" : "") + (n.crit ? " crit" : "") + (n.focus ? " focus" : "");
|
||
const style = box ? { left: box.x, top: box.y, width: box.w, height: box.h } : { width: CW };
|
||
const showTag = !n.stub && (multiSprint || n.dim); // метка спринта — если спринтов на доске больше одного или это сосед
|
||
return (
|
||
<div className={cls} title={n.title} style={style} data-key={n.key}
|
||
onPointerDown={onPointerDown ? (e) => onPointerDown(e, n.key) : undefined}>
|
||
<div className="head">
|
||
<a className="key" href={`${JIRA_URL}/browse/${n.key}`} target="_blank" rel="noopener">{n.key}</a>
|
||
{!n.stub && <Link className="open-card" to={`/issues/${n.key}`} state={BOARD_CARD_STATE} title="Открыть карточку задачи"
|
||
aria-label={`Открыть карточку ${n.key}`}>ⓘ</Link>}
|
||
{showTag && (n.sprint
|
||
? <span className={"sprint-tag" + (sprintSel.has(n.sprint) ? " sel" : "")} title={n.sprint}>{n.sprint.replace(/^AWPCA_/, "")}</span>
|
||
: <span className="sprint-tag none">без спринта</span>)}
|
||
</div>
|
||
<div className="title" style={{ WebkitLineClamp: box && rel ? lineClamp(rel) : "unset" }}>{n.title}</div>
|
||
<div className="meta">
|
||
<span className="kind"><i style={{ background: `var(--k-${KIND[n.kind] || "org"})` }} />{n.kind}</span>
|
||
<span className="prio" style={{ color: `var(--p-${PRIO[n.prio] || "mid"})` }}>{n.prio}</span>
|
||
</div>
|
||
{(n.status || n.due) && (
|
||
<div className="state">
|
||
{n.status && <span className="status" title={n.status}>{n.status}</span>}
|
||
{n.due && <span className="due">до {fmtDate(n.due)}</span>}
|
||
</div>
|
||
)}
|
||
{!n.stub && <div className={"who" + (unassigned(n.who) ? " none" : "")} title={n.who}>{shortName(n.who)}</div>}
|
||
<div className="rsz" title="Потянуть — изменить размер; двойной щелчок — размер по содержимому"
|
||
onDoubleClick={onAutoSize ? () => onAutoSize(n.key) : undefined} />
|
||
<div className="grip t" title="Тянуть за рамку" /><div className="grip b" /><div className="grip l" /><div className="grip r" />
|
||
</div>
|
||
);
|
||
}, cardPropsEqual);
|