Skip to content

feat(nav): nest the account rows under a Profile group in the sidebar (G31) - #94

Merged
Adron merged 1 commit into
devfrom
feat/sidebar-profile-group-g31
Sep 17, 2026
Merged

Adron merged 1 commit into
devfrom
feat/sidebar-profile-group-g31

Conversation

@Adron

@Adron Adron commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Closes #45 (G31). Sequenced after #44 (My Profile) and #47 (Integrations), as the issue asked, so the group has no dead rows.

The sidebar was ten flat peers with no account grouping, where the web keeps Settings and Integrations inside the profile menu.

The group

Profile ▸ My Profile · Organizations · Connections · Settings · Integrations.

The seven reading surfaces stay top level. Grouping Messages or Lists under an account menu would be the web's information architecture applied where it does not fit.

The HIG constraint, honoured

⌘, and File ▸ Settings are untouched. The sidebar rows use SettingsLink — an additional entrance to the same scene, three doors into one window, never a relocation of the panes into the detail column.

Landing on the right tab

SettingsLink opens the Settings window but cannot address a tab. So SettingsRootView's TabView gains a selection bound to @AppStorage, and the sidebar row stores its target before the link opens the scene.

Two things follow:

  • The Settings row targets Preferences explicitly rather than "whatever was open last" — a row labelled Settings that opens Crash Reporting reads as a bug.
  • SettingsTab's raw values are persisted, so renaming a case would silently send an existing user to a different pane on next launch. There is a test pinning them.

The requirement with teeth

"a deep link into Organizations cannot leave the user staring at a collapsed group"

All thirteen selection = … assignments in MainWindowView — menu commands, notification routing, search routing, share-link resolution — now go through one select(_:) that opens the group first when the target lives inside it.

Expanding is deliberately one-way: a deep link to a top-level row does not collapse the group. The user's choice to keep it open is not a menu command's to undo.

MainWindowView.profileGroupSections is the single list that feeds both the group's contents and select(_:), so a row cannot move into the group without deep links learning about it.

Persistence

@AppStorage("sidebar.profileGroup.expanded"), defaulting to expanded — the three rows that moved in were top-level yesterday, and collapsing them by default would look like they had been removed.

Tests

Per the project's view-layer rule the SwiftUI tree is not rendered. What is tested is the part with real logic and a real failure mode: the group's membership, that the partition orphans no section, and that the persisted SettingsTab identities round-trip.

Verification

  • xcodebuild build → ** BUILD SUCCEEDED **
  • xcodebuild test (App) → Executed 973 tests, with 0 failures · ** TEST SUCCEEDED ** (was 968)
  • swift test InterlinedDomain → Executed 1012 tests, with 0 failures
  • swift test InterlinedPersistence → Executed 140 tests, with 0 failures
  • Decision 0003 (anchored) → zero hits

Acceptance

  • ✅ ⌘, and File ▸ Settings behave identically to today.
  • ✅ No Settings pane is reachable only from the sidebar, and none only from ⌘,.
  • ✅ Every deep link still lands, expanding the group when needed.
  • ✅ Expanded state survives a relaunch.

Note on stacking

Branches from dev. The Integrations tab it links to arrives with #93 — until that merges, the Integrations row opens Settings on Linked accounts, which is the honest fallback rather than a dead tab.

🤖 Generated with Claude Code

https://claude.ai/code/session_016gSWb3scYobtxLJioV1qF9

The sidebar was ten flat peers with no account grouping, where the web keeps
Settings and Integrations inside the profile menu. My Profile, Organizations,
Connections, Settings and Integrations now live under a Profile disclosure
group; the seven reading surfaces stay top level, because grouping Messages or
Lists under an account menu would be the web's information architecture applied
where it does not fit.

Settings stays its own window. The sidebar rows use SettingsLink, so they are an
additional entrance to the same scene rather than a relocation of the panes into
the detail column — macOS convention is that Settings is a window, and re-homing
it would break both that expectation and the keyboard shortcut people already
have.

SettingsLink opens the window but cannot address a tab, so SettingsRootView's
TabView gains a selection bound to @AppStorage and the sidebar row stores its
target before opening. The Settings row targets Preferences explicitly rather
than whatever was open last: a row labelled Settings that opens Crash Reporting
reads as a bug. SettingsTab's raw values are persisted, so a rename would
silently send an existing user to a different pane — there is a test pinning
them.

The requirement with teeth was that a deep link into a collapsed group must not
leave the user staring at nothing. All thirteen `selection = …` assignments —
menu commands, notification routing, search routing, share-link resolution — now
go through one `select(_:)` that opens the group first when the target is inside
it. Expanding is one-way: a deep link to a top-level row does not collapse the
group, because the user's choice to keep it open is not a menu command's to
undo. One `profileGroupSections` list feeds both the group's contents and that
function, so a row cannot move into the group without deep links learning about
it.

The expanded state persists and defaults to open: the three rows that moved in
were top-level yesterday, and collapsing them by default would look like they
had been removed.

Refs #45

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gSWb3scYobtxLJioV1qF9
@Adron
Adron merged commit 57c9307 into dev Sep 17, 2026
8 checks passed
@Adron
Adron deleted the feat/sidebar-profile-group-g31 branch September 17, 2026 03:24
Adron added a commit that referenced this pull request Sep 17, 2026
Four files conflicted. Three were textual overlaps where both sides add distinct
members; the fourth needed more than a mechanical resolution.

AppTests/Support/StubUserService.swift was the only file git could not merge
itself. Both sides add members to the same type — this branch's profile-settings
stubs and #93's identity stubs — so the resolution is to keep both. Rebuilt from
dev's version with this branch's four additions reapplied, rather than by
concatenating the conflict hunks: the hunk boundaries crossed a closing brace, so
a naive keep-both dropped the `}` of `enqueueRequestPasswordReset(failure:)` and
nested the rest of the type inside it. It compiled far enough to look plausible
and failed with "Attribute 'private' can only be used in a non-local scope" 300
lines later.

UserDTO.swift, UserService.swift and SettingsRootView.swift auto-merged, and the
first two are genuinely fine — both sides only added. SettingsRootView's
auto-merge was textually clean and semantically wrong, which is the interesting
part of this merge.

PR #94 introduced `SettingsTab` and converted the pane into a
`TabView(selection:)`, tagging every tab it knew about. It was written against a
dev that predated both the Integrations tab (#93) and the Profile tab (this
branch), so neither got a tag. Git had no reason to flag that — no line
conflicts — but under `TabView(selection:)` an untagged tab cannot be selected
programmatically at all. Both are tagged here, and the enum gains the two cases.

The same gap had a second consequence: the sidebar's Integrations row set
`settingsTab = .linkedAccounts`, which was the honest best available when no
Integrations tab existed and is simply wrong now that one does. A row labelled
Integrations opening Linked accounts is the kind of thing that survives review
because it compiles. Retargeted.

The tab-count assertion becomes a full set. A count was already there and did not
catch this — the count was right, the tabs were unreachable — so it now pins the
identities, which at least fails loudly when a pane arrives without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant