Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 92 additions & 15 deletions packages/ui/src/features/canvas/components/ChannelsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ import {
useChannelTaskMutations,
useChannelTasks,
} from "@posthog/ui/features/canvas/hooks/useChannelTasks";
import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards";
import {
useDashboardMutations,
useDashboards,
} from "@posthog/ui/features/canvas/hooks/useDashboards";
import { TaskIcon } from "@posthog/ui/features/sidebar/components/items/TaskIcon";
import { useTaskPrStatus } from "@posthog/ui/features/sidebar/useTaskPrStatus";
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
import { useWorkspace } from "@posthog/ui/features/workspace/useWorkspace";
import { toast } from "@posthog/ui/primitives/toast";
import { Box, Flex, Text, Tooltip } from "@radix-ui/themes";
import { AlertDialog, Box, Flex, Text, Tooltip } from "@radix-ui/themes";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { type ReactNode, useEffect, useState } from "react";
import { hostClient } from "../hostClient";
Expand Down Expand Up @@ -248,7 +251,8 @@ function ChildRow({
);
}

// A single saved canvas under a channel — navigates to its detail view.
// A single saved canvas under a channel — navigates to its detail view, with a
// right-click menu to delete it.
function DashboardRow({
channelId,
dashboard,
Expand All @@ -259,19 +263,92 @@ function DashboardRow({
active: boolean;
}) {
const navigate = useNavigate();
return (
<ChildRow
icon={iconForTemplate(dashboard.templateId)}
title={dashboard.name}
subtitle={`updated ${relativeTime(dashboard.updatedAt)}`}
active={active}
onClick={() =>
navigate({
to: "/website/$channelId/dashboards/$dashboardId",
params: { channelId, dashboardId: dashboard.id },
})
const pathname = useRouterState({ select: (s) => s.location.pathname });
const { deleteDashboard, isDeleting } = useDashboardMutations();
const [confirmOpen, setConfirmOpen] = useState(false);

const onDelete = async () => {
try {
await deleteDashboard(dashboard.id);
// Deleting destroys the canvas, including any child routes under it, so
// match the whole subtree (mirrors ChannelMenu.onDelete).
if (
pathname.startsWith(`/website/${channelId}/dashboards/${dashboard.id}`)
) {
void navigate({
to: "/website/$channelId",
params: { channelId },
});
}
/>
} catch (error) {
toast.error("Couldn't delete canvas", {
description: error instanceof Error ? error.message : String(error),
});
}
};
Comment on lines +270 to +288

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No confirmation before permanent canvas deletion

deleteDashboard fires immediately when the menu item is clicked — there is no "Are you sure?" step. Unlike removing a task from a channel (which only unfiles it), this permanently destroys the canvas and its spec. A mis-click on a long-form canvas is not recoverable. ChannelMenu.onDelete has the same gap, but there the channel-level deletion is at least visually more prominent; the inline list item here is easier to trigger by accident.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/components/ChannelsList.tsx
Line: 269-283

Comment:
**No confirmation before permanent canvas deletion**

`deleteDashboard` fires immediately when the menu item is clicked — there is no "Are you sure?" step. Unlike removing a task from a channel (which only unfiles it), this permanently destroys the canvas and its spec. A mis-click on a long-form canvas is not recoverable. `ChannelMenu.onDelete` has the same gap, but there the channel-level deletion is at least visually more prominent; the inline list item here is easier to trigger by accident.

How can I resolve this? If you propose a fix, please make it concise.


return (
<>
<ContextMenu>
<Tooltip content={dashboard.name} delayDuration={600}>
<ContextMenuTrigger
render={
<Box>
<ChildRow
icon={iconForTemplate(dashboard.templateId)}
title={dashboard.name}
subtitle={`updated ${relativeTime(dashboard.updatedAt)}`}
active={active}
onClick={() =>
navigate({
to: "/website/$channelId/dashboards/$dashboardId",
params: { channelId, dashboardId: dashboard.id },
})
}
/>
</Box>
}
/>
</Tooltip>
<ContextMenuContent>
<ContextMenuItem
variant="destructive"
disabled={isDeleting}
onClick={() => setConfirmOpen(true)}
>
<TrashIcon size={14} />
Delete
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

<AlertDialog.Root open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialog.Content maxWidth="420px" size="2">
<AlertDialog.Title size="3">Delete canvas</AlertDialog.Title>
<AlertDialog.Description size="1">
"{dashboard.name}" will be permanently deleted. This can't be
undone.
</AlertDialog.Description>
<Flex justify="end" gap="2" mt="4">
<AlertDialog.Cancel>
<Button variant="outline" size="sm">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button
variant="destructive"
size="sm"
disabled={isDeleting}
onClick={() => void onDelete()}
>
Delete
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
</>
);
}

Expand Down
Loading