Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 9462bab

Browse files
authored
fix(ui): make watch-list drops land visibly
A dropped task only rendered if it happened to be in the viewer's loaded task list, so drops of anything else stored a reference and showed nothing. Watch entries now capture the title at drop time (rows put it on the drag payload alongside the id), the list synthesizes a row for any watched task the query doesn't hold, and a toast confirms each add. Legacy bare-id entries are normalized on read. Generated-By: PostHog Code Task-Id: 0331ac58-0a1c-4b4e-b884-2ff7d8e71986
1 parent 4914224 commit 9462bab

3 files changed

Lines changed: 70 additions & 14 deletions

File tree

‎packages/ui/src/features/canvas/components/ChannelItemRow.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,16 @@ export function ChannelItemRow({
277277
label={<span>{item.title}</span>}
278278
isActive={isActive}
279279
onClick={() => actions.open(item)}
280-
// Tasks drag into the Command Center grid, which accepts this
281-
// payload type — same contract as the code sidebar's TaskItem.
280+
// Tasks drag into the Command Center grid and the sidebar's
281+
// watch list, which accept this payload type — same contract as
282+
// the code sidebar's TaskItem. The title rides along so a drop
283+
// target can keep a legible reference without refetching.
282284
draggable={item.kind === "task"}
283285
onDragStart={
284286
item.kind === "task"
285287
? (e) => {
286288
e.dataTransfer.setData("text/x-task-id", item.id);
289+
e.dataTransfer.setData("text/x-task-title", item.title);
287290
e.dataTransfer.effectAllowed = "copy";
288291
}
289292
: undefined

‎packages/ui/src/features/canvas/components/WatchListSection.tsx‎

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
PERSONAL_CHANNEL_NAME,
1515
useTaskChannels,
1616
} from "@posthog/ui/features/canvas/hooks/useTaskChannels";
17-
import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
17+
import {
18+
useSpacesSidebarStore,
19+
type WatchedTaskRef,
20+
} from "@posthog/ui/features/canvas/stores/spacesSidebarStore";
1821
import { usePinnedTasks } from "@posthog/ui/features/sidebar/usePinnedTasks";
1922
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
2023
import { toast } from "@posthog/ui/primitives/toast";
@@ -49,10 +52,23 @@ export function WatchListSection() {
4952
const { pinnedTaskIds, togglePin } = usePinnedTasks();
5053
const { archiveTask } = useArchiveTask({ navigateSpace: "website" });
5154

55+
// Older builds persisted bare id strings; treat them as minimal refs.
56+
const watchedRefs = useMemo<WatchedTaskRef[]>(
57+
() =>
58+
(watchList as unknown as (WatchedTaskRef | string)[]).map((entry) =>
59+
typeof entry === "string"
60+
? { id: entry, title: "Untitled task", addedAt: 0 }
61+
: entry,
62+
),
63+
[watchList],
64+
);
65+
5266
// Same item shape the space lists use, held in watch-list order (newest
53-
// watched first) rather than the builder's recency sort.
67+
// watched first) rather than the builder's recency sort. A watched task the
68+
// viewer's task list doesn't hold (someone else's, or beyond the page) still
69+
// renders, from the reference captured at drop time.
5470
const items = useMemo<ChannelItemModel[]>(() => {
55-
const watched = new Set(watchList);
71+
const watched = new Set(watchedRefs.map((entry) => entry.id));
5672
const built = buildChannelItems({
5773
dashboards: [],
5874
feedTasks: allTasks.filter((t) => watched.has(t.id)),
@@ -61,8 +77,24 @@ export function WatchListSection() {
6177
ownedBy: null,
6278
});
6379
const byId = new Map(built.map((item) => [item.id, item]));
64-
return watchList.flatMap((id) => byId.get(id) ?? []);
65-
}, [watchList, allTasks, archivedTaskIds, pinnedTaskIds]);
80+
return watchedRefs.map(
81+
(entry) =>
82+
byId.get(entry.id) ?? {
83+
key: `task:${entry.id}`,
84+
kind: "task" as const,
85+
id: entry.id,
86+
title: entry.title,
87+
ts: entry.addedAt,
88+
pinned: pinnedTaskIds.has(entry.id),
89+
rawStatus: null,
90+
authorUser: null,
91+
authorName: null,
92+
authorUuid: null,
93+
templateId: null,
94+
task: null,
95+
},
96+
);
97+
}, [watchedRefs, allTasks, archivedTaskIds, pinnedTaskIds]);
6698

6799
// Each task's space: backend channel → display name → folder channel (which
68100
// the routes need). Unmapped tasks open under #me and carry no label.
@@ -133,7 +165,14 @@ export function WatchListSection() {
133165
const taskId = e.dataTransfer.getData("text/x-task-id");
134166
if (!taskId) return;
135167
e.preventDefault();
136-
addToWatchList(taskId);
168+
// Title from the drag payload where the source provides it (channel
169+
// rows); the loaded task list covers drags from the code sidebar.
170+
const title =
171+
e.dataTransfer.getData("text/x-task-title") ||
172+
allTasks.find((t) => t.id === taskId)?.title ||
173+
"Untitled task";
174+
addToWatchList({ id: taskId, title, addedAt: Date.now() });
175+
toast.success("Added to watch list", { description: title });
137176
};
138177

139178
return (

‎packages/ui/src/features/canvas/stores/spacesSidebarStore.ts‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import { create } from "zustand";
22
import { persist } from "zustand/middleware";
33

4+
/**
5+
* A watched task, self-sufficient for rendering: the title and added-at are
6+
* captured at drop time so a reference stays legible even when the task isn't
7+
* in the viewer's loaded task list (someone else's, or beyond the page).
8+
*/
9+
export interface WatchedTaskRef {
10+
id: string;
11+
title: string;
12+
addedAt: number;
13+
}
14+
415
/**
516
* View state for the static spaces sidebar: per-space expands, and the
617
* "All spaces" list toggle. `openSections` stores explicit space expansion
@@ -21,16 +32,16 @@ interface SpacesSidebarState {
2132
/** The watch list section's fold. */
2233
openWatchList: boolean;
2334
/**
24-
* Task ids the user dragged into the watch list, newest first. Local-only
35+
* Tasks the user dragged into the watch list, newest first. Local-only
2536
* references for now — watching doesn't touch the task or its space.
2637
*/
27-
watchList: string[];
38+
watchList: WatchedTaskRef[];
2839
toggle: (channelId: string) => void;
2940
toggleAddSpace: () => void;
3041
toggleOnlyMyTasks: () => void;
3142
toggleWatchList: () => void;
3243
setSpaceOrder: (ids: string[]) => void;
33-
addToWatchList: (taskId: string) => void;
44+
addToWatchList: (ref: WatchedTaskRef) => void;
3445
removeFromWatchList: (taskId: string) => void;
3546
}
3647

@@ -59,13 +70,16 @@ export const useSpacesSidebarStore = create<SpacesSidebarState>()(
5970
toggleWatchList: () =>
6071
set((state) => ({ openWatchList: !state.openWatchList })),
6172
setSpaceOrder: (ids) => set({ spaceOrder: ids }),
62-
addToWatchList: (taskId) =>
73+
addToWatchList: (ref) =>
6374
set((state) => ({
64-
watchList: [taskId, ...state.watchList.filter((id) => id !== taskId)],
75+
watchList: [
76+
ref,
77+
...state.watchList.filter((entry) => entry.id !== ref.id),
78+
],
6579
})),
6680
removeFromWatchList: (taskId) =>
6781
set((state) => ({
68-
watchList: state.watchList.filter((id) => id !== taskId),
82+
watchList: state.watchList.filter((entry) => entry.id !== taskId),
6983
})),
7084
}),
7185
{ name: "spaces-sidebar" },

0 commit comments

Comments
 (0)