diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index 96d99b81be..3f39973a7e 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -1,4 +1,5 @@ import { + ArchiveIcon, CaretDownIcon, CaretRightIcon, CaretUpIcon, @@ -33,6 +34,8 @@ import { DropdownMenuTrigger, } from "@posthog/quill"; import type { Task } from "@posthog/shared/domain-types"; +import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds"; +import { useArchiveTask } from "@posthog/ui/features/archive/useArchiveTask"; import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal"; import { RenameChannelModal } from "@posthog/ui/features/canvas/components/RenameChannelModal"; import { @@ -79,12 +82,12 @@ function NavButton({ size="sm" data-selected={active || undefined} onClick={onClick} - className="w-full justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12" + className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12" > {icon} - {label} + {label} {count != null && ( - + {count} )} @@ -224,6 +227,7 @@ function TaskNavRow({ const navigate = useNavigate(); const pathname = useRouterState({ select: (s) => s.location.pathname }); const { fileTask, unfileTask } = useChannelTaskMutations(); + const { archiveTask } = useArchiveTask(); const taskData = useChannelTaskData(task); const workspace = useWorkspace(taskId); const workspaceMode = @@ -263,6 +267,16 @@ function TaskNavRow({ } }; + const onArchive = async () => { + try { + await archiveTask({ taskId }); + } catch (error) { + toast.error("Couldn't archive task", { + description: error instanceof Error ? error.message : String(error), + }); + } + }; + const onRemove = async () => { try { await unfileTask(channelTaskId); @@ -281,18 +295,20 @@ function TaskNavRow({ return ( - - - - } - /> + + + + + } + /> + @@ -317,6 +333,10 @@ function TaskNavRow({ + void onArchive()}> + + Archive + void onRemove()}> Remove from channel @@ -337,6 +357,14 @@ function ChannelSection({ const pathname = useRouterState({ select: (s) => s.location.pathname }); const { data: tasks } = useTasks(); const { tasks: filedTasks } = useChannelTasks(channel.id); + const archivedTaskIds = useArchivedTaskIds(); + // Tasks are private to each user. A task filed by someone else won't be in + // `tasks` (it isn't shared with me), so hide it rather than rendering an + // "Untitled task" placeholder. Also drop archived tasks. + const visibleFiledTasks = filedTasks.filter( + ({ taskId }) => + !archivedTaskIds.has(taskId) && tasks?.some((t) => t.id === taskId), + ); const base = `/website/${channel.id}`; const isActive = pathname === base || pathname.startsWith(`${base}/`); // Channels start collapsed; expansion is session-only. Navigating into a @@ -360,7 +388,7 @@ function ChannelSection({ className="aria-expanded:!bg-transparent hover:aria-expanded:!bg-fill-hover w-full justify-start gap-2 pr-16" > {/* `#` by default; swaps to the expand/collapse caret on hover. */} - + {open ? ( - {filedTasks.map(({ id: channelTaskId, taskId }) => { + {visibleFiledTasks.map(({ id: channelTaskId, taskId }) => { const task = tasks?.find((t) => t.id === taskId); const title = task?.title || "Untitled task"; return ( @@ -450,6 +478,7 @@ function ChannelGroup({ isEmpty = false, emptyHint, className, + headerAction, children, }: { label: string; @@ -457,34 +486,45 @@ function ChannelGroup({ isEmpty?: boolean; emptyHint?: ReactNode; className?: string; + headerAction?: ReactNode; children: ReactNode; }) { const [open, setOpen] = useState(true); return ( - - - + + + + + {headerAction && ( + + {headerAction} + + )} + {isEmpty ? emptyHint : children} @@ -552,6 +592,19 @@ export function ChannelsList() { label="Channels" icon={} className="mt-3" + headerAction={ + + setModalOpen(true)} + > + + + + } > {otherChannels.map((channel) => ( { closeSettingsDialog(); - void openTask(task); + // Bluebird: a task filed to a channel opens in the channel- + // organized view under /website, keeping the channels chrome. + // Otherwise fall back to the /code task detail. + if (bluebirdEnabled && channel) { + navigateToChannelTask(channel.id, task.id); + } else { + void openTask(task); + } }, }; }), }, ]; - }, [tasks, taskChannelMap, closeSettingsDialog]); + }, [tasks, taskChannelMap, bluebirdEnabled, closeSettingsDialog]); const channelSections = useMemo(() => { if (channels.length === 0) return []; diff --git a/packages/ui/src/router/navigationBridge.ts b/packages/ui/src/router/navigationBridge.ts index 7664d601b7..daac44ba5e 100644 --- a/packages/ui/src/router/navigationBridge.ts +++ b/packages/ui/src/router/navigationBridge.ts @@ -39,6 +39,13 @@ export function navigateToChannel(channelId: string): void { }); } +export function navigateToChannelTask(channelId: string, taskId: string): void { + void getRouterOrNull()?.navigate({ + to: "/website/$channelId/tasks/$taskId", + params: { channelId, taskId }, + }); +} + export function navigateToFolderSettings(folderId: string): void { void getRouterOrNull()?.navigate({ to: "/folders/$folderId", diff --git a/packages/ui/src/router/useAppView.ts b/packages/ui/src/router/useAppView.ts index e74e69d224..c94a0a12e3 100644 --- a/packages/ui/src/router/useAppView.ts +++ b/packages/ui/src/router/useAppView.ts @@ -40,7 +40,11 @@ function deriveFromMatches(matches: Match[]): AppView { if (!last) return { type: "task-input" }; switch (last.routeId) { - case "/code/tasks/$taskId": { + // Both the /code task detail and the channels-space task detail render the + // same task-detail view, so consumers (active-state highlighting, archive's + // navigate-away-if-active check) treat them identically. + case "/code/tasks/$taskId": + case "/website/$channelId/tasks/$taskId": { const taskId = last.params.taskId; if (!taskId) return { type: "task-input" }; // Intentionally no `data` snapshot: consumers read live task state via