Files
kandev/apps/web/components/settings/canvas-host-desktop-actions.tsx
T
2026-09-23 20:51:55 +01:00

331 lines
10 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import {
IconDatabase,
IconEdit,
IconListDetails,
IconShare3,
IconSparkles,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { Button } from "@kandev/ui/button";
import { DropdownMenuItem } from "@kandev/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { PanelHeaderOverflowMenu } from "@/components/task/panel-primitives";
import { canvasCanEnableWorkspaceData, type Canvas } from "@/lib/api/domains/canvas-api";
export function canvasLockHelp(canvas: Canvas, t: (key: string) => string): string {
return canvas.status === "archived"
? t("canvases:archivedCanvasActionHelp")
: t("canvases:disabledCanvasActionHelp");
}
export function canvasPromotionHelp(canvas: Canvas, t: (key: string) => string): string {
if (canvas.status === "archived" || canvas.status === "disabled") {
return canvasLockHelp(canvas, t);
}
if (canvas.scope_kind === "task" && canvas.active_release_status === "valid") {
return t("canvases:promoteCanvasHelp");
}
return t("canvases:promoteCanvasUnavailable");
}
function CanvasDesktopActionTooltip({
description,
disabled,
testId,
children,
}: {
description: string;
disabled: boolean;
testId: string;
children: ReactNode;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className={disabled ? "cursor-not-allowed" : undefined}
data-testid={testId}
tabIndex={disabled ? 0 : undefined}
>
{children}
</span>
</TooltipTrigger>
<TooltipContent>{description}</TooltipContent>
</Tooltip>
);
}
export function CanvasDesktopActions({
canvas,
editing,
onEdit,
onPromote,
onReleases,
onShare,
onEnableWorkspaceData,
}: {
canvas: Canvas;
editing: boolean;
onEdit: () => void;
onPromote: () => void;
onReleases: () => void;
onShare: () => void;
onEnableWorkspaceData?: () => void;
}) {
const { t } = useTranslation();
const lifecycleLocked = canvas.status === "archived" || canvas.status === "disabled";
const promoteAvailable = canvas.scope_kind === "task" && canvas.active_release_status === "valid";
const promoteDisabled = lifecycleLocked || !promoteAvailable;
const promoteDescription = canvasPromotionHelp(canvas, t);
return (
<div className="flex items-center gap-2">
{canvas.scope_kind === "workspace" && (
<CanvasDesktopActionTooltip
description={lifecycleLocked ? canvasLockHelp(canvas, t) : t("canvases:editCanvasHelp")}
disabled={editing || lifecycleLocked}
testId="canvas-action-edit-tooltip-trigger"
>
<Button
variant="outline"
size="sm"
className="cursor-pointer"
disabled={editing || lifecycleLocked}
onClick={onEdit}
>
<IconEdit className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:editCanvas")}
</Button>
</CanvasDesktopActionTooltip>
)}
<CanvasDesktopActionTooltip
description={t("canvases:releasesAndPermissionsHelp")}
disabled={false}
testId="canvas-action-releases-tooltip-trigger"
>
<Button variant="outline" size="sm" className="cursor-pointer" onClick={onReleases}>
<IconListDetails className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:releasesAndPermissions")}
</Button>
</CanvasDesktopActionTooltip>
<CanvasDesktopActionTooltip
description={t("canvases:shareCanvasDescription")}
disabled={false}
testId="canvas-action-share-tooltip-trigger"
>
<Button variant="outline" size="sm" className="cursor-pointer" onClick={onShare}>
<IconShare3 className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:shareCanvas")}
</Button>
</CanvasDesktopActionTooltip>
{onEnableWorkspaceData && canvasCanEnableWorkspaceData(canvas) && (
<CanvasDesktopActionTooltip
description={t("canvases:enableWorkspaceDataHelp")}
disabled={false}
testId="canvas-action-enable-workspace-data-tooltip-trigger"
>
<Button
variant="outline"
size="sm"
className="cursor-pointer"
onClick={onEnableWorkspaceData}
data-testid="canvas-action-enable-workspace-data"
>
<IconDatabase className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:enableWorkspaceData")}
</Button>
</CanvasDesktopActionTooltip>
)}
{canvas.scope_kind === "task" && (
<CanvasDesktopActionTooltip
description={promoteDescription}
disabled={promoteDisabled}
testId="canvas-action-promote-tooltip-trigger"
>
<Button
size="sm"
className="cursor-pointer"
disabled={promoteDisabled}
onClick={onPromote}
>
<IconSparkles className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:promoteCanvas")}
</Button>
</CanvasDesktopActionTooltip>
)}
</div>
);
}
type CanvasDesktopOverflowActionsProps = {
canvas: Canvas;
editing: boolean;
onEdit: () => void;
onPromote: () => void;
onReleases: () => void;
onShare: () => void;
onEnableWorkspaceData?: () => void;
omitPrimaryAction?: boolean;
};
function CanvasWorkspaceDataMenuAction({
canvas,
onEnableWorkspaceData,
t,
}: {
canvas: Canvas;
onEnableWorkspaceData?: () => void;
t: (key: string) => string;
}) {
if (!onEnableWorkspaceData || !canvasCanEnableWorkspaceData(canvas)) return null;
return (
<DropdownMenuItem
className="cursor-pointer gap-2"
onSelect={onEnableWorkspaceData}
data-testid="canvas-action-enable-workspace-data"
>
<IconDatabase className="size-4" />
{t("canvases:enableWorkspaceData")}
</DropdownMenuItem>
);
}
export function CanvasDesktopPrimaryAction({
canvas,
editing,
onEdit,
onPromote,
}: Pick<CanvasDesktopOverflowActionsProps, "canvas" | "editing" | "onEdit" | "onPromote">) {
const { t } = useTranslation();
const lifecycleLocked = canvas.status === "archived" || canvas.status === "disabled";
if (canvas.scope_kind === "workspace") {
const editDisabled = editing || lifecycleLocked;
return (
<CanvasDesktopActionTooltip
description={lifecycleLocked ? canvasLockHelp(canvas, t) : t("canvases:editCanvasHelp")}
disabled={editDisabled}
testId="canvas-action-edit-tooltip-trigger"
>
<Button
variant="outline"
size="sm"
className="cursor-pointer"
disabled={editDisabled}
onClick={onEdit}
>
<IconEdit className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:editCanvas")}
</Button>
</CanvasDesktopActionTooltip>
);
}
const promoteAvailable = canvas.active_release_status === "valid";
const promoteDisabled = lifecycleLocked || !promoteAvailable;
const promoteDescription = canvasPromotionHelp(canvas, t);
return (
<CanvasDesktopActionTooltip
description={promoteDescription}
disabled={promoteDisabled}
testId="canvas-action-promote-tooltip-trigger"
>
<Button size="sm" className="cursor-pointer" disabled={promoteDisabled} onClick={onPromote}>
<IconSparkles className="mr-1.5 h-3.5 w-3.5" />
{t("canvases:promoteCanvas")}
</Button>
</CanvasDesktopActionTooltip>
);
}
function CanvasDesktopOverflowMenuItemsContent({
canvas,
editing,
onEdit,
onPromote,
onReleases,
onShare,
onEnableWorkspaceData,
omitPrimaryAction,
t,
}: CanvasDesktopOverflowActionsProps & { t: (key: string) => string }) {
const lifecycleLocked = canvas.status === "archived" || canvas.status === "disabled";
const promoteAvailable = canvas.scope_kind === "task" && canvas.active_release_status === "valid";
const promoteDisabled = lifecycleLocked || !promoteAvailable;
const promoteDescription = canvasPromotionHelp(canvas, t);
const editDescription = lifecycleLocked
? canvasLockHelp(canvas, t)
: t("canvases:editCanvasHelp");
const editDisabled = editing || lifecycleLocked;
const editHelpId = `canvas-edit-overflow-help-${canvas.id}`;
const promoteHelpId = `canvas-promote-overflow-help-${canvas.id}`;
return (
<>
{canvas.scope_kind === "workspace" && !omitPrimaryAction && (
<DropdownMenuItem
className="cursor-pointer gap-2"
disabled={editDisabled}
title={editDescription}
aria-describedby={editDisabled ? editHelpId : undefined}
onSelect={onEdit}
>
<IconEdit className="size-4" />
{t("canvases:editCanvas")}
{editDisabled && (
<span id={editHelpId} className="sr-only">
{editDescription}
</span>
)}
</DropdownMenuItem>
)}
<DropdownMenuItem className="cursor-pointer gap-2" onSelect={onReleases}>
<IconListDetails className="size-4" />
{t("canvases:releasesAndPermissions")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer gap-2" onSelect={onShare}>
<IconShare3 className="size-4" />
{t("canvases:shareCanvas")}
</DropdownMenuItem>
<CanvasWorkspaceDataMenuAction
canvas={canvas}
onEnableWorkspaceData={onEnableWorkspaceData}
t={t}
/>
{canvas.scope_kind === "task" && !omitPrimaryAction && (
<DropdownMenuItem
className="cursor-pointer gap-2"
disabled={promoteDisabled}
title={promoteDescription}
aria-describedby={promoteDisabled ? promoteHelpId : undefined}
onSelect={onPromote}
>
<IconSparkles className="size-4" />
{t("canvases:promoteCanvas")}
{promoteDisabled && (
<span id={promoteHelpId} className="sr-only">
{promoteDescription}
</span>
)}
</DropdownMenuItem>
)}
</>
);
}
export function CanvasDesktopOverflowMenuItems(props: CanvasDesktopOverflowActionsProps) {
const { t } = useTranslation();
return <CanvasDesktopOverflowMenuItemsContent {...props} t={t} />;
}
export function CanvasDesktopOverflowActions(props: CanvasDesktopOverflowActionsProps) {
const { t } = useTranslation();
return (
<PanelHeaderOverflowMenu label={t("canvases:canvasActions")}>
<CanvasDesktopOverflowMenuItemsContent {...props} t={t} />
</PanelHeaderOverflowMenu>
);
}