feat(canvas): collapse channels by default, hash icon, and starred channels#2683
feat(canvas): collapse channels by default, hash icon, and starred channels#2683raquelmsmith wants to merge 4 commits into
Conversation
…annels Reworks the channel list in the Channels sidebar: - Channels always start collapsed on load. Expansion is now session-only state instead of being persisted per-channel in localStorage, so opening the list always begins fully collapsed. - Each channel shows a `#` icon by default; hovering the row swaps it for the expand/collapse caret (the SidebarSection pattern). This moves the header off quill's `folder` Collapsible onto a Radix Collapsible so the trigger contents are fully controllable. - Adds user-specific starred channels, Slack-style. Starred channels surface in a "Starred" group pinned above the rest of the list. Star/unstar via the hover star button on each row or the channel "…" menu. Starred state lives in a persisted zustand UI store (canvas-starred-channels) via the host storage backend — consistent with the rest of the canvas view state, and per-device. Deleting a channel also clears its star. Note on storage: stars are persisted client-side per device rather than on the PostHog user record. This keeps the change self-contained in this repo (no PostHog/posthog backend change) and matches how channel UI state was already handled. If we want cross-device sync later, the store is the single seam to swap for a user-settings field. Generated-By: PostHog Code Task-Id: 79b30083-d838-4e23-8c62-09fc18c48bba
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
packages/ui/src/features/canvas/components/ChannelsList.tsx:517-519
Dangling "Channels" label when all channels are starred. The `<ChannelGroupLabel>Channels</ChannelGroupLabel>` is rendered unconditionally whenever any channel is starred, but if every channel is starred then `otherChannels` is empty and the label floats below the Starred group with nothing underneath it.
```suggestion
{otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>}
</>
)}
```
### Issue 2 of 3
packages/ui/src/features/canvas/starredChannelsStore.ts:11
`isStarred` is never used as a selector in any component — every callsite in `ChannelsList.tsx` inlines `s.starredIds.includes(channel.id)` directly (three separate sites). The method only appears in test assertions via `getState()`. Either replace all three inline `.includes()` usages in the component with `s.isStarred(channelId)`, or drop `isStarred` from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.
### Issue 3 of 3
packages/ui/src/features/canvas/starredChannelsStore.test.ts:8-44
House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a `it.each` table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.
Reviews (1): Last reviewed commit: "feat(canvas): collapse channels by defau..." | Re-trigger Greptile |
| <ChannelGroupLabel>Channels</ChannelGroupLabel> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
Dangling "Channels" label when all channels are starred. The
<ChannelGroupLabel>Channels</ChannelGroupLabel> is rendered unconditionally whenever any channel is starred, but if every channel is starred then otherChannels is empty and the label floats below the Starred group with nothing underneath it.
| <ChannelGroupLabel>Channels</ChannelGroupLabel> | |
| </> | |
| )} | |
| {otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>} | |
| </> | |
| )} |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/components/ChannelsList.tsx
Line: 517-519
Comment:
Dangling "Channels" label when all channels are starred. The `<ChannelGroupLabel>Channels</ChannelGroupLabel>` is rendered unconditionally whenever any channel is starred, but if every channel is starred then `otherChannels` is empty and the label floats below the Starred group with nothing underneath it.
```suggestion
{otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>}
</>
)}
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Fixed in 3f86704 — the Channels section header now only renders when otherChannels.length > 0, so it no longer dangles when every channel is starred. (The code moved from ChannelGroupLabel to a collapsible ChannelGroup.)
| // across projects, so a flat list needs no per-project scoping. | ||
| interface StarredChannelsStore { | ||
| starredIds: string[]; | ||
| isStarred: (channelId: string) => boolean; |
There was a problem hiding this comment.
isStarred is never used as a selector in any component — every callsite in ChannelsList.tsx inlines s.starredIds.includes(channel.id) directly (three separate sites). The method only appears in test assertions via getState(). Either replace all three inline .includes() usages in the component with s.isStarred(channelId), or drop isStarred from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/starredChannelsStore.ts
Line: 11
Comment:
`isStarred` is never used as a selector in any component — every callsite in `ChannelsList.tsx` inlines `s.starredIds.includes(channel.id)` directly (three separate sites). The method only appears in test assertions via `getState()`. Either replace all three inline `.includes()` usages in the component with `s.isStarred(channelId)`, or drop `isStarred` from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Obsolete — starredChannelsStore.ts was removed in the file-system shortcuts refactor (d27c908). Starred state now lives in useChannelStars (backed by desktop FS shortcuts); there is no isStarred selector duplicating .includes() anymore.
|
|
||
| it("stars a channel that is not yet starred", () => { | ||
| useStarredChannelsStore.getState().toggle("c1"); | ||
|
|
||
| expect(useStarredChannelsStore.getState().starredIds).toEqual(["c1"]); | ||
| expect(useStarredChannelsStore.getState().isStarred("c1")).toBe(true); | ||
| }); | ||
|
|
||
| it("toggles an already-starred channel back off", () => { | ||
| const { toggle } = useStarredChannelsStore.getState(); | ||
| toggle("c1"); | ||
| toggle("c1"); | ||
|
|
||
| expect(useStarredChannelsStore.getState().starredIds).toEqual([]); | ||
| expect(useStarredChannelsStore.getState().isStarred("c1")).toBe(false); | ||
| }); | ||
|
|
||
| it("preserves star order as channels are added", () => { | ||
| const { toggle } = useStarredChannelsStore.getState(); | ||
| toggle("c1"); | ||
| toggle("c2"); | ||
|
|
||
| expect(useStarredChannelsStore.getState().starredIds).toEqual(["c1", "c2"]); | ||
| }); | ||
|
|
||
| it("unstar removes a channel and is a no-op when absent", () => { | ||
| const { toggle, unstar } = useStarredChannelsStore.getState(); | ||
| toggle("c1"); | ||
| toggle("c2"); | ||
|
|
||
| unstar("c1"); | ||
| expect(useStarredChannelsStore.getState().starredIds).toEqual(["c2"]); | ||
|
|
||
| // Removing a channel that isn't starred leaves the list untouched. | ||
| unstar("missing"); | ||
| expect(useStarredChannelsStore.getState().starredIds).toEqual(["c2"]); | ||
| }); |
There was a problem hiding this comment.
House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a
it.each table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/starredChannelsStore.test.ts
Line: 8-44
Comment:
House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a `it.each` table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Obsolete — starredChannelsStore.test.ts was removed in the file-system shortcuts refactor (d27c908). The store it tested no longer exists.
…ortcuts Replaces the device-local zustand store for starred channels with the PostHog backend's per-user desktop file-system shortcuts, so stars are user-specific and sync across devices. - api-client: add desktop_file_system_shortcut methods (list/create/delete) on the "desktop" surface, hand-written like the existing desktop_file_system channel methods (these routes aren't in the generated OpenAPI client). - A channel is a folder, so its star is a folder-typed shortcut whose `ref` is the channel's raw file-system path — matching how PostHog's own project tree stars folders. Channel now carries that raw `path` for the `ref`. - useChannelStars / useChannelStarToggle: query the user's shortcuts and map ref → shortcut id, then star (POST) / unstar (DELETE) with optimistic cache updates so the sidebar re-sorts instantly. Deleting a channel also removes its shortcut so a later same-named channel doesn't inherit a stale star. - Remove starredChannelsStore and its test; add useChannelStars tests. The collapse-by-default behavior and the #/caret-on-hover header from the previous commit are unchanged. Generated-By: PostHog Code Task-Id: 79b30083-d838-4e23-8c62-09fc18c48bba
Make the channel sidebar section headers first-class collapsible groups: sentence-case labels at the channel-row font size, a leading star/hash icon that swaps to a caret on hover, and an always-visible Starred section with an empty-state prompt. Channel rows are indented under their section. Move starring into the channel "..." menu (drop the inline star button) and add an "Edit CONTEXT.md" item there, removing the CONTEXT.md row from the list. Stop highlighting expanded channels persistently — they highlight on hover only. Generated-By: PostHog Code Task-Id: 14f68ad5-6084-4693-8d06-00d6f4c32319
Addresses Greptile P1: the "Channels" section header rendered even when otherChannels was empty (all channels starred), leaving a dangling header. Generated-By: PostHog Code Task-Id: 14f68ad5-6084-4693-8d06-00d6f4c32319
What
Reworks the Channels sidebar channel list:
#icon + caret-on-hover. Each channel shows a#by default; hovering the row swaps it for the expand/collapse caret. The header moved from quill'sfolderCollapsible to a Radix Collapsible so the trigger contents are controllable (same pattern asSidebarSection).…menu. Deleting a channel clears its star.Storage: backend per-user shortcuts
Stars are persisted in the PostHog backend as per-user desktop file-system shortcuts — so they're user-specific and sync across devices (no
PostHog/posthogchange required; the capability already exists)./api/projects/{teamId}/desktop_file_system_shortcut/(theDesktopFileSystemShortcutViewSet, scoped to thedesktopsurface and the requesting user).refis the channel's raw file-system path — mirroring how PostHog's own project tree stars folders (projectTreeDataLogic).Channelnow carries that rawpath.useChannelStarslists the user's shortcuts and mapsref → shortcut id;useChannelStarTogglestars (POST) / unstars (DELETE) with optimistic cache updates so the sidebar re-sorts instantly. Deleting a channel removes its shortcut too, so a later same-named channel doesn't inherit a stale star.This replaces the earlier device-local approach (a zustand store), which has been removed.
Testing
pnpm typecheck(whole repo) — clean (22/22)biome checkon changed files — cleanuseChannelStars.test.tsx(ref-mapping, star, unstar) + existinguseChannelstests — passingDraft for local testing.
Note
I haven't yet driven the running Electron app to visually confirm the hover/caret behavior, the Starred section, and a real star/unstar round-trip against the backend. Happy to do that and attach screenshots if you'd like a visual check before merging.
🤖 Generated with Claude Code