feat(workspaces): pin workspaces and widen the switcher to six rows - #6397
Conversation
Show up to six workspaces in the switcher instead of three, keeping the search input from six onward so it appears exactly when the list fills. Pin workspaces to the top of the switcher via the existing row context menu. Pins are per-user and global, so they live on the user's settings row rather than in `pinned_item`, which scopes every row to one workspace. They ride along on the /api/workspaces payload the switcher already loads, so the server prefetch hydrates them and pinned-first ordering never re-sorts after hydration. Drop the seat/workspace-migration disclosure copy from both invitation accept surfaces. The accept-time disclosure tokens are unchanged, so the server still verifies the outcome hasn't shifted since the page loaded.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview The switcher list grows from 3 to 6 visible rows ( Invitation accept UI no longer shows membership/seat or workspace-migration disclosure text on the Client pin toggles use Reviewed by Cursor Bugbot for commit 9c2759f. Configure here. |
Greptile SummaryThe PR adds per-user workspace pinning, pinned-first switcher ordering, and a six-row searchable workspace menu while removing invitation disclosure copy.
Confidence Score: 5/5The PR appears safe to merge. The previously reported concurrent-write and intermediate-reconciliation failures are addressed by serialized mutations, deferred final invalidation, and regression coverage, so no blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/hooks/queries/workspace.ts | Adds workspace-pin query selection, optimistic cache updates, serialized writes, rollback behavior, and deferred final reconciliation. |
| apps/sim/hooks/queries/workspace.test.tsx | Covers pin and unpin requests, same-workspace serialization, deferred reconciliation, idempotent responses, and rollback behavior. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts | Integrates pinned workspace state into pinned-first, recency-preserving switcher ordering. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx | Adds pin controls and indicators while expanding the searchable workspace menu to six rows. |
| apps/sim/lib/workspaces/list.ts | Adds the viewer’s pinned workspace IDs to workspace-list payload construction. |
| packages/db/schema.ts | Extends the pinned-resource type to support workspace pins. |
| apps/sim/app/invite/[id]/invite.tsx | Removes membership and workspace-migration disclosure text from the invitation page. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal.tsx | Removes invitation disclosure text from the pending-invitations modal. |
Sequence Diagram
sequenceDiagram
participant U as User
participant UI as Workspace Switcher
participant Q as Query Cache
participant API as Pinned Item API
participant DB as Database
U->>UI: Toggle workspace pin
UI->>Q: Optimistically update pinnedWorkspaceIds
UI->>API: Send serialized pin/unpin request
API->>DB: Insert or delete workspace pin
DB-->>API: Completed
API-->>UI: Mutation settled
alt More toggles queued
UI->>UI: Defer reconciliation
else Queue empty
UI->>Q: Invalidate workspace lists
end
Reviews (6): Last reviewed commit: "fix(workspaces): count outstanding pin t..." | Re-trigger Greptile
Each write carries the whole pin list, so two overlapping requests that the network delivered out of order left the earlier click as the stored state. Chain them instead, and hold reconciliation until the last queued write settles — refetching between two writes rendered the server's intermediate state and bounced the row out of the pinned group and back.
|
@cursor review |
…ettings Workspace pins were a jsonb array on the settings row, replaced wholesale on every toggle. That shape is what forced the write serialization in 53ee94f: two overlapping toggles each sent the entire list, so the one that landed last won regardless of which the user clicked last. pinned_item is the canonical pinning table and its resource_type is plain text precisely so kinds can be added without a migration, so `workspace` joins it as a sixth kind. A pin is now one row: pinning inserts, unpinning deletes, and two toggles touch different rows and cannot overwrite each other. The serialization, the outstanding-write counter, the settings column, and its migration all go away, and deleting a workspace now cascades its pins. Reads stay on the /api/workspaces payload — the switcher needs the pins *of* every workspace, not the pins *inside* one — so the sidebar prefetch still hydrates them and pinned-first ordering is correct on first paint.
…plays Splitting pins into rows removed the lost-update race between *different* workspaces but not the one on a single row: pin then unpin the same workspace and the DELETE could overtake its INSERT, delete nothing, and leave the workspace pinned. A mutation scope serializes them; TanStack runs onMutate before the scope gate, so the optimistic update is still immediate. Both duplicate-click replays now resolve to their end state rather than erroring — a repeat pin answers 409, a repeat unpin 404, and each means the row is already how the caller wants it. Rollback undoes its own toggle instead of restoring a snapshot, so a sibling toggle's optimistic state survives. Also: cap the switcher to the height Radix measured, since six rows can push the footer actions off a short viewport with nothing able to scroll to them; drop a dead pinned-item invalidation and a redundant ref; return the pin set from the hook to match usePinnedIds; and exclude workspace pins from the unscoped pinned-items listing, where they would read as a resource inside themselves.
|
Heads up for reviewers: the storage design changed after the first round, so the resolved thread above describes an approach that is no longer in the diff. What changed. Pins were a jsonb array on So Reads still come off the Also fixed since round one, from a follow-up audit:
Full suite green (20775 tests). Not browser-verified yet — the short-viewport clamp in particular is reasoned, not observed. |
|
@cursor review |
The mutation scope serializes the writes, so an earlier toggle settles while a later one is still waiting its turn. Invalidating there refetched the server's intermediate state and bounced the row out of the pinned group and back before the last write had even left the client.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3dc924d. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 839a6f3. Configure here.
`hooks/queries/workspace.ts` has no 'use client' directive because server code imports `workspaceKeys` during SSR, so the `useRef` counter added in 3dc924d broke the production build — caught by CI, not by typecheck or tests. `isMutating` answers the same question without a hook: `onSettled` runs before the mutation leaves `pending`, so it counts itself, and anything above one means a later toggle is still queued behind the scope.
839a6f3 to
9c2759f
Compare
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9c2759f. Configure here.
Summary
settings.pinned_workspace_idsrather thanpinned_item(every row there is scoped to one workspace). They ride along on the/api/workspacespayload the switcher already loads, so the server prefetch hydrates them and pinned-first ordering never re-sorts after hydration — no extra request, no flashjoinPreviewUnavailablefield it fed. Accept-time disclosure tokens are unchanged, so the server still verifies the outcome hasn't shifted since page loadType of Change
Testing
Migration verified by running the full chain against a scratch database (285/285 applied, column lands as
jsonb NOT NULL DEFAULT '[]') and round-tripping a value.drizzle-kit generatereports no remaining schema changes. Full suite green (20431 tests). Not yet browser-tested.Checklist