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