Add tailored navigation onboarding popover
What does this MR do and why?
This MR adds a one-time guided tour that introduces the tailored navigation sidebar. It appears as a 2-step popover the first time a user visits a project or group page while the hide_unpinned_sidebar_items feature flag is enabled.
- Layout: anchored to the sidebar's "Customize sidebar" menu, it explains that the sidebar is built around pins and lets the user choose Compact or Full.
- Feature catalog: points out the Feature catalog in the sidebar, where users can explore and pin more features later.
The popover only shows on a docked, expanded sidebar. If the sidebar collapses, the popover hides and comes back on the same step. Closing it, or finishing the tour, dismisses it for good through the new tailored_navigation_onboarding user callout. It works with the keyboard: focus moves into the popover when it opens (unless the user has already moved focus to something else), Esc closes it, and focus returns to the control the step points at.
Existing users keep the pins they already have. The first time a user loads a sidebar with the flag enabled, any missing default pins are added after their saved group and project pins. The merge is saved the first time the user changes their pins, recorded by the tailored_navigation_default_pins_merged callout.
How the default pins merge avoids writes on page loads
The sidebar shows the merged pins from the first page load, but page loads never write. The merge is only saved when the user changes their pins:
- Page load: the helper merges the defaults in memory and renders them through
pinned_items, so the EEgroup_epic_list/group_issue_listdedupe still applies. Nothing else reads the saved pins. - Pin changes:
Users::PinsController#updatealready writes. Before applying the change, it claims the callout with an insert that skips existing rows, so the merge is saved at most once, then saves the merged pins. Later page loads don't merge again, so a default the user removed stays removed. - Users who never change their pins never trigger a write.
- Panels saved as empty get the defaults. With unpinned items hidden, an empty panel would otherwise show no navigation.
- Read-only databases (Geo secondaries, maintenance mode) show the merged pins, like the primary, but never save them.
- Users without saved pins already fall back to the defaults, so no preference is written for them.
- Rollback: turning the flag off shows users their original saved pins, unless they changed pins during the rollout.
References
Work item: Feedback : Feature library + tailored navigation (#604008 - closed) • Austin Regnery
Screenshots or screen recordings
| New | Existing |
|---|---|
How to set up and validate locally
-
Enable the feature flag in a Rails console:
Feature.enable(:hide_unpinned_sidebar_items). -
On a desktop-width window with the sidebar expanded, visit a project in a group, for example http://127.0.0.1:3000/flightjs/Flight.
-
Confirm the popover appears next to the "Customize sidebar" menu and shows Step 1 of 2.
-
Switch between Compact and Full, then confirm the sidebar updates without a reload. Reload and confirm the choice persists.
-
Select Next. Confirm the popover points at Feature catalog and the Feature catalog shimmer plays.
-
Select Feature catalog. Confirm the popover closes, the catalog opens, and the popover doesn't return after a reload.
-
Reset the dismissal (see below), then collapse the sidebar to icons only. Confirm the popover hides, and reappears on the same step when you expand the sidebar again.
-
With the keyboard, confirm:
- Focus moves into the popover when it opens, and Tab reaches its controls.
- Esc closes it and returns focus to the control the step points at: Customize sidebar on step 1, Feature catalog on step 2.
- If you tab to a link before the popover appears, focus stays on that link.
-
To check the one-time default pins merge, give the user custom project pins and reset the callouts (see below):
User.find_by_username('root').update!(pinned_nav_items: { 'project' => %w[pipelines] })Reload the project page. The sidebar shows the merged pins straight away, but they're only saved once you change a pin. Confirm the sidebar shows Pipelines first, followed by the missing default pins (Project overview, Members, Issues, Branches, Merge requests). Unpin one of the defaults, reload, and confirm it stays unpinned: the merge only happens once.
To reset the dismissal and the default pins merge, run:
User.find_by_username('root').callouts.where(feature_name: %w[tailored_navigation_onboarding tailored_navigation_default_pins_merged]).destroy_allTo replay the Feature catalog shimmer, run:
User.find_by_username('root').callouts.where(feature_name: 'feature_library_shimmer_seen').destroy_allDatabase
Both writes run at most once per user, in Users::PinsController#update, the first time the user changes their pins with hide_unpinned_sidebar_items enabled. They happen inside one Users::Callout.transaction on the main database, never during a page GET, and are skipped on read-only databases. Users who never change their pins never write.
Claim the callout (Users::Pins::MergeDefaultsService#claim_callout)
INSERT INTO "user_callouts" ("user_id","feature_name","dismissed_at")
VALUES (<user_id>, 139, '<now>')
ON CONFLICT ("user_id","feature_name") DO NOTHING RETURNING "id"Conflict arbiter: index_user_callouts_on_user_id_and_feature_name (existing unique index).
- New row, cold cache: 36.5 ms, mostly I/O and the
usersFK check (plan) - New row, warm cache: 4.5 ms (plan)
- Existing row (lost race): 0.4 ms, no row inserted, FK trigger not fired (plan)
Save merged pins (Users::Pins::MergeDefaultsService#save_merged_pins)
Only runs when the merged pins differ from the saved pins. Users without saved pins don't write.
UPDATE "user_preferences"
SET "updated_at" = '<now>', "pinned_nav_items" = '<json>'
WHERE "user_preferences"."id" = <preference_id>Index Scan on user_preferences_pkey, 1 row: 23.6 ms, 22.2 ms of it cold I/O (plan)
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.