ref(nav): Add ReorderableList, ReorderableLink, and Indicator to SecondaryNavigation#110824
ref(nav): Add ReorderableList, ReorderableLink, and Indicator to SecondaryNavigation#110824
Conversation
Plan for consolidating duplicated drag-and-drop primitives across dashboards, explore, and issue views secondary navigation items into shared SecondaryNavigation.ReorderableList, ReorderableLink, and Indicator components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… SecondaryNavigation Extract duplicated drag-and-drop plumbing from dashboards, explore, and issue views into three new SecondaryNavigation primitives: - ReorderableList: owns all drag state, exposes onDragEnd with the final ordered array (ref-based, no stale-state bug). Calls setInteraction internally so consumers never touch it. - ReorderableLink: drop-in for Link that auto-injects the grab handle via context, shows it on hover, and blocks navigation if a drag is in progress. - Indicator: small absolute-positioned dot matching PrimaryNavigationUnreadIndicator styling (tokens, sizing, variant prop). All three consumer components (dashboards, explore, issue views) now look structurally identical. Removed ~545 lines of duplicated styled components, useRef/useState/useEffect drag boilerplate, and sectionRef threading. Also fixes a bug in dashboards and explore where a single useDragControls() instance was shared across all items; each ReorderableListItem now calls useDragControls() independently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37c1db8 to
5fe100a
Compare
…Provider ReorderableListItem now calls useSecondaryNavigation to track drag interaction state, which requires a SecondaryNavigationContextProvider in the tree. Wrap the existing tests for ExploreSavedQueryNavigationItems and DashboardsNavigationItems with the provider so they continue to pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5ca1683 to
ec81ccf
Compare
|
Dashboards part LGTM, thanks! 👍🏻 |
…ble-secondary-nav # Conflicts: # static/app/views/navigation/secondary/components.tsx
Merge master's active state indicator CSS into MobileNavigationLink alongside the interaction state layer disable rule added on this branch. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
natemoo-re
left a comment
There was a problem hiding this comment.
Most functionality seems covered—I had a few points of visual feedback. The only one really blocking is the broken mouseover behaivor
static/app/views/navigation/secondary/sections/issues/issueViews/issueViewQueryCount.tsx
Show resolved
Hide resolved
natemoo-re
left a comment
There was a problem hiding this comment.
Don't want to block since this is a big improvement over what we currently have—merge when you're happy.
| onMutate: (queries: SavedQuery[]) => { | ||
| setApiQueryData<SavedQuery[]>(queryClient, queryKey, queries); | ||
| }, |
There was a problem hiding this comment.
Bug: The optimistic update for reordering starred queries caches SavedQuery class instances instead of the expected ReadableSavedQuery objects, causing data corruption on subsequent reads.
Severity: HIGH
Suggested Fix
Before calling setApiQueryData in the onMutate handler, convert the SavedQuery[] array back into the expected ReadableSavedQuery[] type. This ensures the data stored in the cache matches the type that consumers expect to read.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: static/app/views/explore/hooks/useReorderStarredSavedQueries.tsx#L26-L28
Potential issue: When a user reorders starred queries, the optimistic update in
`useReorderStarredSavedQueries` incorrectly caches `SavedQuery` class instances. The
React Query cache, however, expects plain `ReadableSavedQuery` objects. When the
`useGetSavedQueries` hook subsequently reads from this corrupted cache, it attempts to
construct new `SavedQuery` instances from objects that are already `SavedQuery`
instances. This "double construction" leads to incorrect data structures and breaks the
feature.
| <IconGrabbable variant="muted" /> | ||
| </StyledGrabHandle> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Grab handle click without drag triggers unwanted navigation
Medium Severity
The GrabHandle component is missing an onClick handler with stopPropagation/preventDefault. All three old implementations (issues, dashboards, explore) had explicit onClick={e => { e.stopPropagation(); e.preventDefault(); }} on the grab handle wrapper. The new GrabHandle only spreads {...listeners} from dnd-kit (which provides onPointerDown but not onClick). When a user clicks the grab handle without moving 5+ pixels (the NavigationPointerSensor activation constraint), the click event bubbles up to StyledReorderableFakeLink and calls handleNavigate(), causing unwanted page navigation.
Additional Locations (1)
| onMutate: (queries: SavedQuery[]) => { | ||
| setApiQueryData<SavedQuery[]>(queryClient, queryKey, queries); | ||
| }, |
There was a problem hiding this comment.
Bug: The optimistic update in useReorderStarredSavedQueries writes SavedQuery class instances to the cache, which expects ReadableSavedQuery plain objects, causing a potential type mismatch on refetch.
Severity: MEDIUM
Suggested Fix
Before writing to the cache in onMutate, convert the SavedQuery[] class instances back to ReadableSavedQuery[] plain objects. This can be done by adding a toJSON() or similar serialization method to the SavedQuery class and calling it on each item in the array before calling setApiQueryData. This ensures the cache data type remains consistent.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: static/app/views/explore/hooks/useReorderStarredSavedQueries.tsx#L26-L28
Potential issue: The `onMutate` callback in the `useReorderStarredSavedQueries` hook
optimistically updates the React Query cache by writing an array of `SavedQuery` class
instances. However, the `useGetSavedQueries` hook, which reads from this same cache key,
expects the cache to contain `ReadableSavedQuery` plain objects. During the brief window
before the cache is invalidated on `onSettled`, if a component refetches,
`useGetSavedQueries` will receive the wrong data type. This causes its `useMemo` to pass
an already-instantiated `SavedQuery` object to the `new SavedQuery()` constructor,
leading to incorrect data processing and potential data corruption.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| [data-reorderable-handle-slot] { | ||
| ${p => p.isDragging && p.theme.visuallyHidden} | ||
| } | ||
| `; |
There was a problem hiding this comment.
Both drag icon and project icon visible during keyboard focus
Low Severity
The CSS in StyledReorderableFakeLink has a gap in its show/hide logic. When the grab handle receives keyboard focus (:focus-visible) but the user isn't hovering, both [data-drag-icon] and [data-reorderable-handle-slot] become visible simultaneously. The first rule :not(:hover):not(:has(:focus-visible)) correctly avoids hiding the drag icon during focus, but the second rule only hides the handle slot on :hover, so the project icon remains visible too. Both elements occupy layout space in the same Flex container, causing visual doubling of the leading icon area.


Consolidates draggable nav elements from explore, issues and dashboards under common secondary nav primitives.
I will migrate away from reorder in a followup PR
Fix DE-583, DE-582, DE-590
Ref DE-578