5452a4b9ce
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
IconCircleCheckFilled,
|
|
IconCircleXFilled,
|
|
IconClock,
|
|
IconGitMerge,
|
|
IconLoader2,
|
|
IconPointFilled,
|
|
} from "@tabler/icons-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { cn } from "@/lib/utils";
|
|
import { isMRReadyToMerge, mrChipStatus } from "./mr-task-icon";
|
|
import type { MRChipStatus } from "./mr-task-icon";
|
|
import type { ChipAutomation } from "./mr-status-chip-selection";
|
|
import type { TaskMR } from "@/lib/types/gitlab";
|
|
|
|
export const MR_CHIP_TRIGGER_CLASS =
|
|
"cursor-pointer inline-flex items-center gap-1 rounded-md px-1 py-0.5 text-xs";
|
|
|
|
const STATUS_LABEL_KEY: Record<MRChipStatus, string> = {
|
|
merged: "gitlab:mrChipStatusMerged",
|
|
closed: "gitlab:mrChipStatusClosed",
|
|
failed: "gitlab:mrChipStatusFailed",
|
|
draft: "gitlab:mrChipStatusDraft",
|
|
ready: "gitlab:mrChipStatusReady",
|
|
awaiting_approval: "gitlab:mrChipStatusAwaitingApproval",
|
|
running: "gitlab:mrChipStatusRunning",
|
|
neutral: "gitlab:mrChipStatusNeutral",
|
|
};
|
|
|
|
export function MRStatusChipGlyph({ status }: { status: MRChipStatus }) {
|
|
const cls = "h-3.5 w-3.5";
|
|
const testProps = { "data-testid": "mr-status-glyph", "data-status": status } as const;
|
|
switch (status) {
|
|
case "merged":
|
|
return (
|
|
<IconGitMerge {...testProps} className={cn(cls, "text-purple-500")} aria-hidden="true" />
|
|
);
|
|
case "failed":
|
|
return (
|
|
<IconCircleXFilled {...testProps} className={cn(cls, "text-red-500")} aria-hidden="true" />
|
|
);
|
|
case "ready":
|
|
return (
|
|
<IconCircleCheckFilled
|
|
{...testProps}
|
|
className={cn(cls, "text-emerald-400")}
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
case "awaiting_approval":
|
|
return <IconClock {...testProps} className={cn(cls, "text-sky-400")} aria-hidden="true" />;
|
|
case "running":
|
|
return (
|
|
<IconLoader2
|
|
{...testProps}
|
|
className={cn(cls, "text-yellow-500 animate-spin [animation-duration:3s]")}
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
default:
|
|
// closed, draft, neutral all render the same muted dot.
|
|
return (
|
|
<IconPointFilled
|
|
{...testProps}
|
|
className={cn(cls, "text-muted-foreground")}
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
// The aria-label describes the acted-on MR, so its status word is the
|
|
// acted-on MR's OWN status (mrChipStatus(actedOnMR)) — it deliberately does
|
|
// NOT read the live-tracking `liveStatus` that drives `data-status` and the
|
|
// glyph. Diverging on that point would rename the label mid-interaction to a
|
|
// status that belongs to a different MR than the one the disclosure is
|
|
// actually showing/acting on (spec: Accessibility, "The aria-label describes
|
|
// the acted-on MR").
|
|
function useMRChipAriaLabel(openCount: number, actedOnMR: TaskMR) {
|
|
const { t } = useTranslation();
|
|
const status = t(STATUS_LABEL_KEY[mrChipStatus(actedOnMR)]);
|
|
if (openCount > 1) return t("gitlab:mrChipAriaLabelMulti", { count: openCount, status });
|
|
return t("gitlab:mrChipAriaLabelSingle", { mriid: actedOnMR.mr_iid, status });
|
|
}
|
|
|
|
export type MRChipTriggerAttrs = {
|
|
"data-testid": "mr-status-chip";
|
|
"data-status": MRChipStatus;
|
|
"data-mr-count": number;
|
|
"data-mr-iid": number;
|
|
"data-mr-state": string;
|
|
"data-mr-ready-to-merge": "true" | "false";
|
|
"data-selection-frozen": "true" | "false";
|
|
"aria-label": string;
|
|
className: string;
|
|
};
|
|
|
|
export function useMRChipTriggerAttrs({
|
|
openCount,
|
|
liveStatus,
|
|
actedOnMR,
|
|
frozen,
|
|
}: {
|
|
openCount: number;
|
|
liveStatus: MRChipStatus;
|
|
actedOnMR: TaskMR;
|
|
frozen: boolean;
|
|
}): MRChipTriggerAttrs {
|
|
return {
|
|
"data-testid": "mr-status-chip",
|
|
"data-status": liveStatus,
|
|
"data-mr-count": openCount,
|
|
"data-mr-iid": actedOnMR.mr_iid,
|
|
"data-mr-state": actedOnMR.state,
|
|
"data-mr-ready-to-merge": isMRReadyToMerge(actedOnMR) ? "true" : "false",
|
|
"data-selection-frozen": frozen ? "true" : "false",
|
|
"aria-label": useMRChipAriaLabel(openCount, actedOnMR),
|
|
className: MR_CHIP_TRIGGER_CLASS,
|
|
};
|
|
}
|
|
|
|
function AutomationFlagBadges({ automation }: { automation: ChipAutomation }) {
|
|
const { t } = useTranslation();
|
|
if (!automation.autoFixEnabled && !automation.autoMergeEnabled) return null;
|
|
const round = automation.autoFixRound;
|
|
return (
|
|
<>
|
|
{automation.autoFixEnabled && round && (
|
|
<span
|
|
data-testid="mr-status-auto-fix-chip"
|
|
data-auto-fix-exhausted={round.exhausted ? "true" : "false"}
|
|
className={cn(
|
|
"rounded-sm px-1 py-0.5 text-[9px] font-medium leading-none",
|
|
round.exhausted
|
|
? "bg-yellow-500/15 text-yellow-500"
|
|
: "bg-emerald-500/15 text-emerald-500",
|
|
)}
|
|
>
|
|
{t("gitlab:mrChipAutoFix")} {round.current}/{round.max}
|
|
</span>
|
|
)}
|
|
{automation.autoMergeEnabled && (
|
|
<span
|
|
data-testid="mr-status-auto-merge-chip"
|
|
className="rounded-sm bg-sky-500/15 px-1 py-0.5 text-[9px] font-medium leading-none text-sky-500"
|
|
>
|
|
{t("gitlab:mrChipAutoMerge")}
|
|
</span>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function MRStatusChipTriggerContent({
|
|
openCount,
|
|
liveStatus,
|
|
automation,
|
|
}: {
|
|
openCount: number;
|
|
liveStatus: MRChipStatus;
|
|
automation: ChipAutomation;
|
|
}) {
|
|
return (
|
|
<>
|
|
<IconGitMerge className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
|
|
<MRStatusChipGlyph status={liveStatus} />
|
|
{openCount > 1 && (
|
|
<span className="text-[9px] font-semibold leading-none tabular-nums">{openCount}</span>
|
|
)}
|
|
<AutomationFlagBadges automation={automation} />
|
|
</>
|
|
);
|
|
}
|