1- import {
2- CaretDown ,
3- CheckIcon ,
4- CubeFocusIcon ,
5- LockSimpleIcon ,
6- } from "@phosphor-icons/react" ;
1+ import { CaretDown , Check } from "@phosphor-icons/react" ;
72import {
83 Button ,
94 DropdownMenu ,
105 DropdownMenuContent ,
116 DropdownMenuItem ,
7+ DropdownMenuSeparator ,
128 DropdownMenuTrigger ,
139} from "@posthog/quill" ;
14- import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels" ;
10+ import { channelGlyph } from "@posthog/ui/features/canvas/components/channelGlyph" ;
11+ import { useChannelStars } from "@posthog/ui/features/canvas/hooks/useChannelStars" ;
12+ import {
13+ type Channel ,
14+ useChannels ,
15+ } from "@posthog/ui/features/canvas/hooks/useChannels" ;
1516import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels" ;
17+ import { useSpacesSidebarStore } from "@posthog/ui/features/canvas/stores/spacesSidebarStore" ;
1618import { useMemo } from "react" ;
1719
18- function spaceIcon ( name : string | undefined ) {
19- return name === PERSONAL_CHANNEL_NAME ? (
20- < LockSimpleIcon size = { 14 } weight = "regular" />
21- ) : (
22- < CubeFocusIcon size = { 14 } weight = "regular" />
23- ) ;
24- }
25-
2620/**
2721 * Which space a new task files into — a chip for the composer's selector row,
28- * drawn exactly like WorkspaceModeSelect ("Cloud"/"Local") beside it.
29- * Personal space first, the rest alphabetical.
22+ * drawn exactly like WorkspaceModeSelect ("Cloud"/"Local") beside it. The menu
23+ * leads with the starred spaces in their sidebar order (#me first), a
24+ * separator, then everything else alphabetically — items in the flyout
25+ * vocabulary the switchers use: a leading check well, then the space's own
26+ * glyph (only #me carries one), then the name.
3027 */
3128export function SpaceSelect ( {
3229 value,
@@ -36,26 +33,68 @@ export function SpaceSelect({
3633 onChange : ( channelId : string ) => void ;
3734} ) {
3835 const { channels } = useChannels ( ) ;
36+ const { starredRefToShortcutId } = useChannelStars ( ) ;
37+ const spaceOrder = useSpacesSidebarStore ( ( s ) => s . spaceOrder ) ;
3938 const current = channels . find ( ( c ) => c . id === value ) ;
4039
41- const options = useMemo (
42- ( ) =>
43- [ ...channels ] . sort ( ( a , b ) => {
44- if ( a . name === PERSONAL_CHANNEL_NAME ) return - 1 ;
45- if ( b . name === PERSONAL_CHANNEL_NAME ) return 1 ;
46- return a . name . localeCompare ( b . name ) ;
47- } ) ,
48- [ channels ] ,
49- ) ;
40+ const { starred, rest } = useMemo ( ( ) => {
41+ const me = channels . filter ( ( c ) => c . name === PERSONAL_CHANNEL_NAME ) ;
42+ const rank = new Map ( spaceOrder . map ( ( id , index ) => [ id , index ] ) ) ;
43+ const starredList = channels
44+ . filter (
45+ ( c ) =>
46+ c . name !== PERSONAL_CHANNEL_NAME &&
47+ starredRefToShortcutId . has ( c . path ) ,
48+ )
49+ . sort (
50+ ( a , b ) =>
51+ ( rank . get ( a . id ) ?? Number . MAX_SAFE_INTEGER ) -
52+ ( rank . get ( b . id ) ?? Number . MAX_SAFE_INTEGER ) ,
53+ ) ;
54+ const starredIds = new Set ( starredList . map ( ( c ) => c . id ) ) ;
55+ return {
56+ starred : [ ...me , ...starredList ] ,
57+ rest : channels
58+ . filter (
59+ ( c ) => c . name !== PERSONAL_CHANNEL_NAME && ! starredIds . has ( c . id ) ,
60+ )
61+ . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ,
62+ } ;
63+ } , [ channels , starredRefToShortcutId , spaceOrder ] ) ;
64+
65+ const triggerGlyph = channelGlyph ( current ?. name , { size : 14 , space : true } ) ;
66+
67+ const renderItem = ( space : Channel ) => {
68+ const glyph = channelGlyph ( space . name , { size : 14 , space : true } ) ;
69+ return (
70+ < DropdownMenuItem
71+ key = { space . id }
72+ className = "justify-start"
73+ onClick = { ( ) => {
74+ if ( space . id !== value ) onChange ( space . id ) ;
75+ } }
76+ >
77+ < span className = "flex w-4 shrink-0 items-center justify-center" >
78+ { space . id === value && < Check size = { 14 } className = "text-accent-11" /> }
79+ </ span >
80+ { glyph && (
81+ < span className = "flex shrink-0 items-center text-muted-foreground" >
82+ { glyph }
83+ </ span >
84+ ) }
85+ < span className = "min-w-0 truncate text-[13px]" > { space . name } </ span >
86+ </ DropdownMenuItem >
87+ ) ;
88+ } ;
5089
5190 return (
5291 < DropdownMenu >
5392 < DropdownMenuTrigger
5493 render = {
5594 < Button type = "button" variant = "outline" size = "sm" aria-label = "Space" >
56- < span className = "text-muted-foreground" >
57- { spaceIcon ( current ?. name ) }
58- </ span >
95+ { triggerGlyph && (
96+ < span className = "text-muted-foreground" > { triggerGlyph } </ span >
97+ ) }
5998 { current ?. name ?? "Space" }
6099 < CaretDown
61100 size = { 10 }
@@ -65,21 +104,15 @@ export function SpaceSelect({
65104 </ Button >
66105 }
67106 />
68- < DropdownMenuContent align = "start" side = "bottom" sideOffset = { 6 } >
69- { options . map ( ( space ) => (
70- < DropdownMenuItem
71- key = { space . id }
72- onClick = { ( ) => {
73- if ( space . id !== value ) onChange ( space . id ) ;
74- } }
75- >
76- < span className = "text-muted-foreground" >
77- { spaceIcon ( space . name ) }
78- </ span >
79- < span className = "min-w-0 flex-1 truncate" > { space . name } </ span >
80- { space . id === value && < CheckIcon size = { 14 } /> }
81- </ DropdownMenuItem >
82- ) ) }
107+ < DropdownMenuContent
108+ align = "start"
109+ side = "bottom"
110+ sideOffset = { 6 }
111+ className = "w-auto min-w-[200px]"
112+ >
113+ { starred . map ( renderItem ) }
114+ { starred . length > 0 && rest . length > 0 && < DropdownMenuSeparator /> }
115+ { rest . map ( renderItem ) }
83116 </ DropdownMenuContent >
84117 </ DropdownMenu >
85118 ) ;
0 commit comments