Skip to content

Commit aff8287

Browse files
committed
more fake composer + reference to tasks
1 parent 3473704 commit aff8287

3 files changed

Lines changed: 183 additions & 83 deletions

File tree

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

Lines changed: 8 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LinkIcon, RobotIcon, UserIcon } from "@phosphor-icons/react";
1+
import { RobotIcon, UserIcon } from "@phosphor-icons/react";
22
import {
33
Button,
44
Dialog,
@@ -8,13 +8,6 @@ import {
88
DialogFooter,
99
DialogHeader,
1010
DialogTitle,
11-
DropdownMenu,
12-
DropdownMenuContent,
13-
DropdownMenuItem,
14-
DropdownMenuSub,
15-
DropdownMenuSubContent,
16-
DropdownMenuSubTrigger,
17-
DropdownMenuTrigger,
1811
Field,
1912
FieldLabel,
2013
Input,
@@ -23,8 +16,8 @@ import {
2316
} from "@posthog/quill";
2417
import { DemoMessageItem } from "@posthog/ui/features/canvas/components/ChannelFeedView";
2518
import { MentionComposer } from "@posthog/ui/features/canvas/components/MentionComposer";
19+
import { ReferencePicker } from "@posthog/ui/features/canvas/components/ReferencePicker";
2620
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
27-
import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards";
2821
import { useOrgMembers } from "@posthog/ui/features/canvas/hooks/useOrgMembers";
2922
import { useDemoFeedStore } from "@posthog/ui/features/canvas/stores/demoFeedStore";
3023
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
@@ -52,7 +45,6 @@ export function InsertFakeMessageDialog({
5245
}) {
5346
const { channels } = useChannels();
5447
const channelName = channels.find((c) => c.id === channelId)?.name;
55-
const { dashboards } = useDashboards(channelId);
5648
const { members } = useOrgMembers();
5749
const addDemoMessage = useDemoFeedStore((s) => s.add);
5850
// The dialog portals to <body>, outside the app's Radix <Theme> — re-establish
@@ -179,80 +171,15 @@ export function InsertFakeMessageDialog({
179171
</Field>
180172

181173
{/* Message body: a real thread composer, so @ opens the same member
182-
picker as a thread. The reference menu drops in canvas / context
183-
names as text. */}
174+
picker as a thread. The reference picker inserts canvas / channel /
175+
task / CONTEXT.md deep links. */}
184176
<Field>
185177
<div className="flex items-center justify-between">
186178
<FieldLabel>Message</FieldLabel>
187-
<DropdownMenu>
188-
<DropdownMenuTrigger
189-
render={
190-
<Button variant="outline" size="xs">
191-
<LinkIcon size={13} />
192-
Insert reference
193-
</Button>
194-
}
195-
/>
196-
<DropdownMenuContent align="end" className="w-56">
197-
<DropdownMenuSub>
198-
<DropdownMenuSubTrigger>Canvas</DropdownMenuSubTrigger>
199-
<DropdownMenuSubContent
200-
side="right"
201-
sideOffset={4}
202-
className="max-h-72 w-56 overflow-y-auto"
203-
>
204-
{dashboards.length === 0 ? (
205-
<DropdownMenuItem disabled>
206-
No canvases yet
207-
</DropdownMenuItem>
208-
) : (
209-
dashboards.map((d) => (
210-
<DropdownMenuItem
211-
key={d.id}
212-
onClick={() =>
213-
insertReference(
214-
d.name,
215-
`/website/${channelId}/dashboards/${d.id}`,
216-
)
217-
}
218-
>
219-
{d.name}
220-
</DropdownMenuItem>
221-
))
222-
)}
223-
</DropdownMenuSubContent>
224-
</DropdownMenuSub>
225-
<DropdownMenuSub>
226-
<DropdownMenuSubTrigger>Channels</DropdownMenuSubTrigger>
227-
<DropdownMenuSubContent
228-
side="right"
229-
sideOffset={4}
230-
className="max-h-72 w-56 overflow-y-auto"
231-
>
232-
{channels.map((c) => (
233-
<DropdownMenuItem
234-
key={c.id}
235-
onClick={() =>
236-
insertReference(`#${c.name}`, `/website/${c.id}`)
237-
}
238-
>
239-
#{c.name}
240-
</DropdownMenuItem>
241-
))}
242-
</DropdownMenuSubContent>
243-
</DropdownMenuSub>
244-
<DropdownMenuItem
245-
onClick={() =>
246-
insertReference(
247-
"CONTEXT.md",
248-
`/website/${channelId}/context`,
249-
)
250-
}
251-
>
252-
This channel's CONTEXT.md
253-
</DropdownMenuItem>
254-
</DropdownMenuContent>
255-
</DropdownMenu>
179+
<ReferencePicker
180+
channelId={channelId}
181+
onInsert={insertReference}
182+
/>
256183
</div>
257184
<div className="rounded-md border border-border px-2 py-1">
258185
<MentionComposer

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { splitMentionSegments } from "@posthog/shared";
2+
import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon";
23
import {
34
splitLinkSegments,
45
splitRichLinkSegments,
56
} from "@posthog/ui/features/canvas/utils/linkify";
7+
import {
8+
getCachedTask,
9+
taskDetailQuery,
10+
} from "@posthog/ui/features/tasks/queries";
611
import { Text } from "@radix-ui/themes";
12+
import { useQuery } from "@tanstack/react-query";
713
import { useRouter } from "@tanstack/react-router";
814
import { Fragment, useMemo } from "react";
915
import "./mention-chip.css";
@@ -15,6 +21,14 @@ import {
1521
SquircleDashed,
1622
} from "lucide-react";
1723

24+
// The task-list status icon for a `…/tasks/<id>` link — live, like the sidebar
25+
// and feed cards (generating spinner, PR/cloud state). Falls back to the code
26+
// glyph while the task loads or if it isn't cached.
27+
function TaskLinkIcon({ taskId }: { taskId: string }) {
28+
const { data } = useQuery({ ...taskDetailQuery(taskId), staleTime: 30_000 });
29+
return <TaskTabIcon task={data ?? getCachedTask(taskId)} size={12} />;
30+
}
31+
1832
type RenderSegment =
1933
| { type: "text"; text: string }
2034
| { type: "link"; text: string; href: string }
@@ -110,6 +124,7 @@ export function MentionText({
110124
// An in-app route (`/…`) navigates through the router (opening a
111125
// canvas, context, …) instead of the OS browser.
112126
if (segment.href.startsWith("/")) {
127+
const taskId = segment.href.match(/\/tasks\/([^/?#]+)/)?.[1];
113128
const { Icon, label } = internalLinkMeta(
114129
segment.href,
115130
segment.text,
@@ -124,8 +139,14 @@ export function MentionText({
124139
}}
125140
className={linkClass}
126141
>
127-
<Icon size={12} className="mr-0.5 inline-block align-[-1px]" />
128-
{label}
142+
<span className="mr-0.5 inline-block align-[-1px]">
143+
{taskId ? (
144+
<TaskLinkIcon taskId={taskId} />
145+
) : (
146+
<Icon size={12} />
147+
)}
148+
</span>
149+
{taskId ? segment.text : label}
129150
</a>
130151
);
131152
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { ArrowSquareOutIcon, LinkIcon } from "@phosphor-icons/react";
2+
import {
3+
Button,
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuGroup,
7+
DropdownMenuItem,
8+
DropdownMenuLabel,
9+
DropdownMenuSeparator,
10+
DropdownMenuSub,
11+
DropdownMenuSubContent,
12+
DropdownMenuSubTrigger,
13+
DropdownMenuTrigger,
14+
} from "@posthog/quill";
15+
import type { Task } from "@posthog/shared/domain-types";
16+
import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon";
17+
import { useChannelFeed } from "@posthog/ui/features/canvas/hooks/useChannelFeed";
18+
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
19+
import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards";
20+
import { useBackendChannel } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
21+
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
22+
import { File, Shapes, SquircleDashed } from "lucide-react";
23+
24+
// A menu for inserting an in-app reference (canvas / channel / task / this
25+
// channel's CONTEXT.md) as a deep link. Canvas / Channels / Tasks are submenus;
26+
// CONTEXT.md is a direct item.
27+
export function ReferencePicker({
28+
channelId,
29+
onInsert,
30+
}: {
31+
channelId: string;
32+
onInsert: (label: string, href: string) => void;
33+
}) {
34+
const { channels } = useChannels();
35+
const channelName = channels.find((c) => c.id === channelId)?.name;
36+
const { channel: backendChannel } = useBackendChannel(channelName);
37+
const { dashboards } = useDashboards(channelId);
38+
const { tasks: channelTasks } = useChannelFeed(backendChannel?.id);
39+
const { data: allTasks = [] } = useTasks();
40+
41+
// Every task is referenceable: this channel's tasks first, then the rest.
42+
const channelTaskIds = new Set(channelTasks.map((t) => t.id));
43+
const otherTasks = allTasks.filter((t) => !channelTaskIds.has(t.id));
44+
45+
const taskItem = (t: Task) => (
46+
<DropdownMenuItem
47+
key={t.id}
48+
onClick={() =>
49+
onInsert(
50+
t.title || "Untitled task",
51+
`/website/${channelId}/tasks/${t.id}`,
52+
)
53+
}
54+
>
55+
<TaskTabIcon task={t} size={14} />
56+
<span className="truncate">{t.title || "Untitled task"}</span>
57+
</DropdownMenuItem>
58+
);
59+
60+
return (
61+
<DropdownMenu>
62+
<DropdownMenuTrigger
63+
render={
64+
<Button variant="outline" size="xs">
65+
<LinkIcon size={13} />
66+
Insert reference
67+
</Button>
68+
}
69+
/>
70+
<DropdownMenuContent align="end" className="w-56">
71+
<DropdownMenuSub>
72+
<DropdownMenuSubTrigger>
73+
<Shapes size={14} className="text-muted-foreground" />
74+
Canvas
75+
</DropdownMenuSubTrigger>
76+
<DropdownMenuSubContent side="right" sideOffset={4} className="w-56">
77+
{dashboards.length === 0 ? (
78+
<DropdownMenuItem disabled>No canvases yet</DropdownMenuItem>
79+
) : (
80+
dashboards.map((d) => (
81+
<DropdownMenuItem
82+
key={d.id}
83+
onClick={() =>
84+
onInsert(d.name, `/website/${channelId}/dashboards/${d.id}`)
85+
}
86+
>
87+
<Shapes size={14} className="text-muted-foreground" />
88+
{d.name}
89+
</DropdownMenuItem>
90+
))
91+
)}
92+
</DropdownMenuSubContent>
93+
</DropdownMenuSub>
94+
95+
<DropdownMenuSub>
96+
<DropdownMenuSubTrigger>
97+
<SquircleDashed size={14} className="text-muted-foreground" />
98+
Channels
99+
</DropdownMenuSubTrigger>
100+
<DropdownMenuSubContent side="right" sideOffset={4} className="w-56">
101+
{channels.map((c) => (
102+
<DropdownMenuItem
103+
key={c.id}
104+
onClick={() => onInsert(`#${c.name}`, `/website/${c.id}`)}
105+
>
106+
<SquircleDashed size={14} className="text-muted-foreground" />
107+
{c.name}
108+
</DropdownMenuItem>
109+
))}
110+
</DropdownMenuSubContent>
111+
</DropdownMenuSub>
112+
113+
<DropdownMenuSub>
114+
<DropdownMenuSubTrigger>
115+
<ArrowSquareOutIcon size={14} className="text-muted-foreground" />
116+
Tasks
117+
</DropdownMenuSubTrigger>
118+
<DropdownMenuSubContent side="right" sideOffset={4} className="w-64">
119+
{channelTasks.length === 0 && otherTasks.length === 0 ? (
120+
<DropdownMenuItem disabled>No tasks yet</DropdownMenuItem>
121+
) : (
122+
<>
123+
{channelTasks.length > 0 && (
124+
<DropdownMenuGroup>
125+
<DropdownMenuLabel>This channel</DropdownMenuLabel>
126+
{channelTasks.map(taskItem)}
127+
</DropdownMenuGroup>
128+
)}
129+
{otherTasks.length > 0 && (
130+
<DropdownMenuGroup>
131+
{channelTasks.length > 0 && <DropdownMenuSeparator />}
132+
<DropdownMenuLabel>Other tasks</DropdownMenuLabel>
133+
{otherTasks.map(taskItem)}
134+
</DropdownMenuGroup>
135+
)}
136+
</>
137+
)}
138+
</DropdownMenuSubContent>
139+
</DropdownMenuSub>
140+
141+
<DropdownMenuItem
142+
onClick={() =>
143+
onInsert("CONTEXT.md", `/website/${channelId}/context`)
144+
}
145+
>
146+
<File size={14} className="text-muted-foreground" />
147+
This channel's CONTEXT.md
148+
</DropdownMenuItem>
149+
</DropdownMenuContent>
150+
</DropdownMenu>
151+
);
152+
}

0 commit comments

Comments
 (0)