Skip to content

ref(nav): Add ReorderableList, ReorderableLink, and Indicator to SecondaryNavigation#110824

Open
JonasBa wants to merge 33 commits intomasterfrom
jb/ref/nav-reorderable-secondary-nav
Open

ref(nav): Add ReorderableList, ReorderableLink, and Indicator to SecondaryNavigation#110824
JonasBa wants to merge 33 commits intomasterfrom
jb/ref/nav-reorderable-secondary-nav

Conversation

@JonasBa
Copy link
Member

@JonasBa JonasBa commented Mar 16, 2026

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

JonasBa and others added 3 commits March 16, 2026 16:31
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>
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Mar 16, 2026
@JonasBa JonasBa force-pushed the jb/ref/nav-reorderable-secondary-nav branch from 37c1db8 to 5fe100a Compare March 16, 2026 23:37
@JonasBa JonasBa marked this pull request as ready for review March 16, 2026 23:39
@JonasBa JonasBa requested a review from a team as a code owner March 16, 2026 23:39
…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>
@JonasBa JonasBa force-pushed the jb/ref/nav-reorderable-secondary-nav branch from 5ca1683 to ec81ccf Compare March 17, 2026 01:55
@gggritso
Copy link
Member

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>
Copy link
Member

@natemoo-re natemoo-re left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most functionality seems covered—I had a few points of visual feedback. The only one really blocking is the broken mouseover behaivor

Copy link
Member

@natemoo-re natemoo-re left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't want to block since this is a big improvement over what we currently have—merge when you're happy.

Comment on lines +26 to +28
onMutate: (queries: SavedQuery[]) => {
setApiQueryData<SavedQuery[]>(queryClient, queryKey, queries);
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Comment on lines +26 to +28
onMutate: (queries: SavedQuery[]) => {
setApiQueryData<SavedQuery[]>(queryClient, queryKey, queries);
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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}
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants