Skip to content

Commit 79d8a7d

Browse files
authored
fix(bunch o stuff): suggestion fade, task cap, archive route (#2794)
1 parent c9df89f commit 79d8a7d

3 files changed

Lines changed: 106 additions & 49 deletions

File tree

packages/ui/src/features/archive/useArchiveTask.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function makeCacheWriter(
7474
function makeOrchestrationDeps(
7575
queryClient: QueryClient,
7676
keys: ArchiveCacheKeys,
77-
options?: { skipNavigate?: boolean },
77+
options?: { skipNavigate?: boolean; navigateSpace?: "code" | "website" },
7878
): ArchiveOrchestrationDeps {
7979
const hostClient = resolveService<HostTrpcClient>(HOST_TRPC_CLIENT);
8080
return {
@@ -91,7 +91,9 @@ function makeOrchestrationDeps(
9191
if (options?.skipNavigate) return;
9292
const view = getAppViewSnapshot();
9393
if (view.type === "task-detail" && view.taskId === taskId) {
94-
openTaskInput();
94+
openTaskInput(
95+
options?.navigateSpace ? { space: options.navigateSpace } : undefined,
96+
);
9597
}
9698
},
9799
snapshotTerminalStates: (taskId) =>
@@ -147,7 +149,11 @@ export async function archiveTaskImperative(
147149
taskId: string,
148150
queryClient: QueryClient,
149151
keys: ArchiveCacheKeys,
150-
options?: { skipNavigate?: boolean; optimistic?: boolean },
152+
options?: {
153+
skipNavigate?: boolean;
154+
optimistic?: boolean;
155+
navigateSpace?: "code" | "website";
156+
},
151157
): Promise<void> {
152158
await archiveTask(
153159
taskId,
@@ -173,7 +179,12 @@ export async function archiveTasksImperative(
173179
);
174180
}
175181

176-
export function useArchiveTask() {
182+
export function useArchiveTask(options?: {
183+
// Which new-task screen to land on if the archived task is the active view.
184+
// Defaults to Code; the bluebird/channels nav passes "website" so archiving
185+
// from there returns to the website new-task screen instead.
186+
navigateSpace?: "code" | "website";
187+
}) {
177188
const queryClient = useQueryClient();
178189
const keys = useArchiveCacheKeys();
179190
const { restore } = useUnarchiveTask();
@@ -183,6 +194,7 @@ export function useArchiveTask() {
183194
// is confirmed, rather than removing it instantly and rolling back on error.
184195
await archiveTaskImperative(taskId, queryClient, keys, {
185196
optimistic: false,
197+
navigateSpace: options?.navigateSpace,
186198
});
187199
const toastId = `archive-undo-${taskId}`;
188200
toast.success("Task archived", {

packages/ui/src/features/canvas/components/ChannelsList.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ import { useNavigate, useRouterState } from "@tanstack/react-router";
7070
import { type ReactNode, useEffect, useState } from "react";
7171
import { hostClient } from "../hostClient";
7272

73+
// Cap how many tasks each channel shows by default; the rest hide behind a
74+
// "View more" button so a busy channel doesn't dominate the sidebar.
75+
const MAX_VISIBLE_TASKS_PER_CHANNEL = 5;
76+
7377
// A canvas's leading icon, chosen from its template so the tree reads at a
7478
// glance: bar chart for dashboards, line chart for web-analytics, plain file for
7579
// blank canvases.
@@ -376,7 +380,9 @@ function TaskRow({
376380
const navigate = useNavigate();
377381
const pathname = useRouterState({ select: (s) => s.location.pathname });
378382
const { fileTask, unfileTask } = useChannelTaskMutations();
379-
const { archiveTask } = useArchiveTask();
383+
// Archiving from the bluebird/channels nav should return to the website
384+
// new-task screen, not the Code one.
385+
const { archiveTask } = useArchiveTask({ navigateSpace: "website" });
380386
const taskData = useChannelTaskData(task);
381387
const workspace = useWorkspace(taskId);
382388
const workspaceMode =
@@ -524,9 +530,17 @@ function ChannelSection({
524530
const [open, setOpen] = useState(isActive);
525531
// Lifted so the hover button group stays visible while the menu is open.
526532
const [menuOpen, setMenuOpen] = useState(false);
533+
// Only the first few tasks per channel show by default; "View more" reveals
534+
// another batch each click so a busy channel doesn't flood the sidebar.
535+
const [taskLimit, setTaskLimit] = useState(MAX_VISIBLE_TASKS_PER_CHANNEL);
527536
useEffect(() => {
528537
if (isActive) setOpen(true);
529538
}, [isActive]);
539+
// Toggle expansion; collapsing also resets back to the first batch of tasks.
540+
const toggleOpen = () => {
541+
setOpen((o) => !o);
542+
if (open) setTaskLimit(MAX_VISIBLE_TASKS_PER_CHANNEL);
543+
};
530544

531545
// Lazy: a channel's canvases and filed tasks are only fetched once it's
532546
// expanded, so the tree doesn't fire one query per channel on mount.
@@ -539,6 +553,13 @@ function ChannelSection({
539553
({ taskId }) =>
540554
!archivedTaskIds.has(taskId) && tasks?.some((t) => t.id === taskId),
541555
);
556+
const displayedFiledTasks = visibleFiledTasks.slice(0, taskLimit);
557+
const hiddenTaskCount = visibleFiledTasks.length - displayedFiledTasks.length;
558+
// Reveal one more batch, capped at the remaining count.
559+
const nextBatchCount = Math.min(
560+
hiddenTaskCount,
561+
MAX_VISIBLE_TASKS_PER_CHANNEL,
562+
);
542563

543564
return (
544565
<Box className="group/chan relative">
@@ -550,7 +571,7 @@ function ChannelSection({
550571
<Button
551572
variant="default"
552573
size="default"
553-
onClick={() => setOpen((o) => !o)}
574+
onClick={toggleOpen}
554575
aria-expanded={open}
555576
className="w-full min-w-0 flex-1 justify-start gap-2 aria-expanded:bg-transparent"
556577
>
@@ -618,7 +639,7 @@ function ChannelSection({
618639
active={pathname === `${base}/dashboards/${d.id}`}
619640
/>
620641
))}
621-
{visibleFiledTasks.map(({ id: channelTaskId, taskId }) => {
642+
{displayedFiledTasks.map(({ id: channelTaskId, taskId }) => {
622643
const task = tasks?.find((t) => t.id === taskId);
623644
const title = task?.title || "Untitled task";
624645
return (
@@ -640,6 +661,21 @@ function ChannelSection({
640661
/>
641662
);
642663
})}
664+
{hiddenTaskCount > 0 && (
665+
<Button
666+
variant="default"
667+
size="default"
668+
onClick={() =>
669+
setTaskLimit((n) => n + MAX_VISIBLE_TASKS_PER_CHANNEL)
670+
}
671+
className="w-full min-w-0 justify-start gap-2 text-[13px] text-gray-10"
672+
>
673+
<span className="inline-flex size-[14px] shrink-0 items-center justify-center">
674+
<CaretDownIcon size={12} />
675+
</span>
676+
View {nextBatchCount} more
677+
</Button>
678+
)}
643679
</Flex>
644680
)}
645681
</Box>

packages/ui/src/features/task-detail/components/TaskInput.tsx

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { navigateToInbox } from "@posthog/ui/router/navigationBridge";
1010
import { useAppView } from "@posthog/ui/router/useAppView";
1111
import { Flex, Text, Tooltip } from "@radix-ui/themes";
1212
import { useQuery } from "@tanstack/react-query";
13+
import { AnimatePresence, motion } from "framer-motion";
1314
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1415
import { useConnectivity } from "../../../hooks/useConnectivity";
1516
import { DotPatternBackground } from "../../../primitives/DotPatternBackground";
@@ -688,12 +689,10 @@ export function TaskInput({
688689
style={{
689690
// Raise the input when the suggestion cards are shown so the longer
690691
// list below it isn't squished against the bottom of the viewport.
691-
// Tied to the same condition as the cards (which hide once the
692-
// editor has content) so the input recenters as the user types.
693-
top:
694-
suggestions && suggestions.length > 0 && editorIsEmpty
695-
? "38%"
696-
: "50%",
692+
// Note: this is NOT tied to `editorIsEmpty` — the input keeps its
693+
// position as the user types so the box doesn't jump down when the
694+
// suggestions fade out (and back in when the prompt is cleared).
695+
top: suggestions && suggestions.length > 0 ? "38%" : "50%",
697696
transform: "translate(-50%, -50%)",
698697
}}
699698
className="absolute left-1/2 z-[1] flex w-[calc(100%-2rem)] max-w-[600px] flex-col gap-2"
@@ -942,43 +941,53 @@ export function TaskInput({
942941
</Flex>
943942
<div className="absolute top-full right-0 left-0 z-10">
944943
{suggestions ? (
945-
suggestions.length > 0 &&
946-
editorIsEmpty && (
947-
<div className="mt-6 flex flex-col gap-2">
948-
<Text
949-
size="1"
950-
weight="medium"
951-
className="px-2.5 text-(--gray-11)"
944+
<AnimatePresence>
945+
{suggestions.length > 0 && editorIsEmpty && (
946+
<motion.div
947+
key="suggestions"
948+
initial={{ opacity: 0 }}
949+
animate={{ opacity: 1 }}
950+
exit={{ opacity: 0 }}
951+
transition={{ duration: 0.2, ease: "easeInOut" }}
952+
className="mt-6 flex flex-col gap-2"
952953
>
953-
Suggestions
954-
</Text>
955-
<div className="grid grid-cols-2 gap-2">
956-
{suggestions.map((suggestion) => (
957-
<SuggestedPromptCard
958-
key={suggestion.label}
959-
suggestion={suggestion}
960-
onSelect={() => {
961-
// Use pending content (not setContent) so the
962-
// multi-line template — intro + "User input:" fill-in
963-
// lines — keeps its line breaks; focuses at the end.
964-
useDraftStore
965-
.getState()
966-
.actions.setPendingContent(sessionId, {
967-
segments: [
968-
{ type: "text", text: suggestion.prompt },
969-
],
970-
});
971-
// Bug/feature suggestions start in plan mode; the
972-
// analysis ones start in auto mode.
973-
if (isValidConfigValue(modeOption, suggestion.mode)) {
974-
setConfigOption(modeOption.id, suggestion.mode);
975-
}
976-
}}
977-
/>
978-
))}
979-
</div>
980-
</div>
981-
)
954+
<Text
955+
size="1"
956+
weight="medium"
957+
className="px-2.5 text-(--gray-11)"
958+
>
959+
Suggestions
960+
</Text>
961+
<div className="grid grid-cols-2 gap-2">
962+
{suggestions.map((suggestion) => (
963+
<SuggestedPromptCard
964+
key={suggestion.label}
965+
suggestion={suggestion}
966+
onSelect={() => {
967+
// Use pending content (not setContent) so the
968+
// multi-line template — intro + "User input:" fill-in
969+
// lines — keeps its line breaks; focuses at the end.
970+
useDraftStore
971+
.getState()
972+
.actions.setPendingContent(sessionId, {
973+
segments: [
974+
{ type: "text", text: suggestion.prompt },
975+
],
976+
});
977+
// Bug/feature suggestions start in plan mode; the
978+
// analysis ones start in auto mode.
979+
if (
980+
isValidConfigValue(modeOption, suggestion.mode)
981+
) {
982+
setConfigOption(modeOption.id, suggestion.mode);
983+
}
984+
}}
985+
/>
986+
))}
987+
</div>
988+
</motion.div>
989+
)}
990+
</AnimatePresence>
982991
) : (
983992
<SuggestedTasksPanel />
984993
)}

0 commit comments

Comments
 (0)