Skip to content
Merged
Show file tree
Hide file tree
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
131 changes: 92 additions & 39 deletions packages/ui/src/features/canvas/components/ChannelsList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ArchiveIcon,
CaretDownIcon,
CaretRightIcon,
CaretUpIcon,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
<span className="min-w-0 flex-1 truncate text-left">{label}</span>
{count != null && (
<Badge variant="default" className="ml-auto">
<Badge variant="default" className="ml-auto shrink-0">
{count}
</Badge>
)}
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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),
});
}
};
Comment thread
raquelmsmith marked this conversation as resolved.

const onRemove = async () => {
try {
await unfileTask(channelTaskId);
Expand All @@ -281,18 +295,20 @@ function TaskNavRow({

return (
<ContextMenu>
<ContextMenuTrigger
render={
<Box>
<NavButton
label={title}
icon={icon}
active={active}
onClick={onClick}
/>
</Box>
}
/>
<Tooltip content={title} delayDuration={600}>
<ContextMenuTrigger
render={
<Box>
<NavButton
label={title}
icon={icon}
active={active}
onClick={onClick}
/>
</Box>
}
/>
</Tooltip>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>
Expand All @@ -317,6 +333,10 @@ function TaskNavRow({
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem onClick={() => void onArchive()}>
<ArchiveIcon size={14} />
Archive
</ContextMenuItem>
<ContextMenuItem variant="destructive" onClick={() => void onRemove()}>
<XIcon size={14} />
Remove from channel
Expand All @@ -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
Expand All @@ -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. */}
<span className="flex size-[18px] shrink-0 items-center justify-center text-gray-10">
<span className="flex size-3.5 shrink-0 items-center justify-center text-gray-10">
<HashIcon size={14} className="block group-hover/chan:hidden" />
{open ? (
<CaretDownIcon
Expand Down Expand Up @@ -392,7 +420,7 @@ function ChannelSection({
})
}
/>
{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 (
Expand Down Expand Up @@ -450,41 +478,53 @@ function ChannelGroup({
isEmpty = false,
emptyHint,
className,
headerAction,
children,
}: {
label: string;
icon: ReactNode;
isEmpty?: boolean;
emptyHint?: ReactNode;
className?: string;
headerAction?: ReactNode;
children: ReactNode;
}) {
const [open, setOpen] = useState(true);

return (
<Collapsible.Root open={open} onOpenChange={setOpen} className={className}>
<Collapsible.Trigger asChild>
<button
type="button"
className="group/grp flex w-full items-center gap-2 px-2 pt-1 text-left"
>
{/* Leading icon by default; swaps to the expand/collapse caret on hover. */}
<span className="flex size-4 shrink-0 items-center justify-center text-gray-9">
<span className="block group-hover/grp:hidden">{icon}</span>
{open ? (
<CaretUpIcon size={12} className="hidden group-hover/grp:block" />
) : (
<CaretDownIcon
size={12}
className="hidden group-hover/grp:block"
/>
)}
</span>
<Text weight="medium" className="text-gray-9 text-xs tracking-wide">
{label}
</Text>
</button>
</Collapsible.Trigger>
<Box className="group/grp relative">
<Collapsible.Trigger asChild>
<button
type="button"
className="flex w-full items-center gap-2 px-2 pt-1 text-left"
>
{/* Leading icon by default; swaps to the expand/collapse caret on hover. */}
<span className="flex size-4 shrink-0 items-center justify-center text-gray-9">
<span className="block group-hover/grp:hidden">{icon}</span>
{open ? (
<CaretUpIcon
size={12}
className="hidden group-hover/grp:block"
/>
) : (
<CaretDownIcon
size={12}
className="hidden group-hover/grp:block"
/>
)}
</span>
<Text weight="medium" className="text-gray-9 text-xs tracking-wide">
{label}
</Text>
</button>
</Collapsible.Trigger>
{headerAction && (
<Box className="absolute top-0.5 right-1 opacity-0 transition-opacity group-hover/grp:opacity-100">
{headerAction}
</Box>
)}
</Box>
<Collapsible.Content>
<Flex direction="column" gap="1" pt="1" pl="3">
{isEmpty ? emptyHint : children}
Expand Down Expand Up @@ -552,6 +592,19 @@ export function ChannelsList() {
label="Channels"
icon={<HashIcon size={14} className="text-gray-9" />}
className="mt-3"
headerAction={
<Tooltip content="New channel" side="top">
<IconButton
variant="ghost"
color="gray"
size="1"
aria-label="New channel"
onClick={() => setModalOpen(true)}
>
<PlusIcon size={14} weight="bold" />
</IconButton>
</Tooltip>
}
>
{otherChannels.map((channel) => (
<ChannelSection
Expand Down
16 changes: 13 additions & 3 deletions packages/ui/src/features/command/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import { TaskIcon } from "@posthog/ui/features/sidebar/components/items/TaskIcon
import { useSidebarStore } from "@posthog/ui/features/sidebar/sidebarStore";
import { useTaskPrStatus } from "@posthog/ui/features/sidebar/useTaskPrStatus";
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
import { navigateToChannel } from "@posthog/ui/router/navigationBridge";
import {
navigateToChannel,
navigateToChannelTask,
} from "@posthog/ui/router/navigationBridge";
import { useAppView } from "@posthog/ui/router/useAppView";
import { openTask, openTaskInput } from "@posthog/ui/router/useOpenTask";
import { track } from "@posthog/ui/shell/analytics";
Expand Down Expand Up @@ -284,13 +287,20 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
action: "open-task" as CommandMenuAction,
onRun: () => {
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<CommandSection[]>(() => {
if (channels.length === 0) return [];
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/router/navigationBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/src/router/useAppView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading