tabbed url, refresh stays on tab, refactor tabbed footer logic across components#2980
tabbed url, refresh stays on tab, refactor tabbed footer logic across components#2980danoswaltCL wants to merge 2 commits intodevfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request refactors tab management across the application by consolidating tabbing logic into a shared common-tabbed-section-card-footer component. The PR adds URL query parameter support (?tab=index) to persist tab selection across page refreshes, and removes three component-specific footer implementations (for segments, feature flags, and experiments) in favor of the unified approach.
Changes:
- Moved tab state management into
CommonTabbedSectionCardFooterComponentwith URL query parameter persistence - Updated authentication service to preserve query parameters (including tab) during login redirects
- Eliminated duplicate footer components across segments, feature flags, and experiments features
Reviewed changes
Copilot reviewed 17 out of 20 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| common-tabbed-section-card-footer.component.ts | Added URL query parameter reading/writing, initialization logic, and change detection for tab persistence |
| segment-overview-details-section-card.component.ts | Migrated from custom footer to common footer, added dynamic tabLabels based on segment type |
| segment-overview-details-section-card.component.html | Replaced custom footer with common footer component |
| segment-overview-details-footer/* | Removed obsolete custom footer component files |
| feature-flag-overview-details-section-card.component.ts | Migrated from custom footer to common footer, added static tabLabels array |
| feature-flag-overview-details-section-card.component.html | Replaced custom footer with common footer component |
| feature-flag-overview-details-footer/* | Removed obsolete custom footer component files |
| feature-flag-details-page-content.component.ts | Changed from Observable-based tab state to primitive activeTabIndex |
| feature-flag-details-page-content.component.html | Updated template to use primitive instead of async pipe for tab conditionals |
| experiment-overview-details-section-card.component.ts | Migrated from custom footer to common footer, added static tabLabels array |
| experiment-overview-details-section-card.component.html | Replaced custom footer with common footer component |
| experiment-overview-details-footer/* | Removed obsolete custom footer component files |
| auth.effects.ts | Changed from navigate() to navigateByUrl() to preserve query parameters in redirects |
| auth.service.ts | Updated to include query string in redirect URL preservation |
Comments suppressed due to low confidence (1)
frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts:27
- The component documentation should be updated to reflect the new URL query parameter behavior. The example usage should mention that the component now automatically reads and writes a 'tab' query parameter to/from the URL, and that this affects page refresh behavior. This is important information for developers using this component.
/**
* The `app-common-tabbed-section-card-footer` component provides a common footer component for tabbed sections.
* It allows the user to switch between different tabs and emits the selected tab index when changed.
*
* Example usage:
*
* ```html
* <app-common-tabbed-section-card-footer
* [tabLabels]="[{label: 'Tab 1'}, {label: 'Tab 2', disabled: true}]"
* (selectedTabChange)="onSelectedTabChange($event)">
* </app-common-tabbed-section-card-footer>
* ```
*/
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...t/segment-overview-details-section-card/segment-overview-details-section-card.component.html
Outdated
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Show resolved
Hide resolved
...ent/segment-overview-details-section-card/segment-overview-details-section-card.component.ts
Show resolved
Hide resolved
...etails-page/feature-flag-details-page-content/feature-flag-details-page-content.component.ts
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Outdated
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Outdated
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Outdated
Show resolved
Hide resolved
.../components/common-tabbed-section-card-footer/common-tabbed-section-card-footer.component.ts
Outdated
Show resolved
Hide resolved
…oter logic to standardize across components
8bf06c7 to
89d5ee2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 27 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const tabWithinRange = selectedTab >= 0 && selectedTab < this.tabLabels.length; | ||
|
|
||
| if (!tabWithinRange) { | ||
| console.warn('Tab index from url is out of range'); | ||
| return; | ||
| } | ||
|
|
||
| this.selectedIndex = selectedTab; |
There was a problem hiding this comment.
When a disabled tab is specified in the URL query parameter, the component doesn't validate whether the selected tab is disabled. This could result in selecting and displaying a disabled tab, which violates the intended behavior. Consider adding validation to check if the tab at the selected index is disabled, and if so, default to the first non-disabled tab or tab index 0.
| this.selectedTabChange.emit(event.index); | ||
| this.router.navigate([], { | ||
| relativeTo: this.route, | ||
| queryParams: { tab: event.index }, | ||
| queryParamsHandling: 'merge', | ||
| replaceUrl: true, | ||
| }); |
There was a problem hiding this comment.
The navigation to update the query parameter happens after the event is emitted. If the parent component's event handler performs navigation or modifies the route, there could be a race condition or conflicting navigation commands. Consider performing the router navigation before emitting the event, or ensure that parent components don't navigate in response to this event.
| this.selectedTabChange.emit(event.index); | |
| this.router.navigate([], { | |
| relativeTo: this.route, | |
| queryParams: { tab: event.index }, | |
| queryParamsHandling: 'merge', | |
| replaceUrl: true, | |
| }); | |
| this.router.navigate([], { | |
| relativeTo: this.route, | |
| queryParams: { tab: event.index }, | |
| queryParamsHandling: 'merge', | |
| replaceUrl: true, | |
| }); | |
| this.selectedTabChange.emit(event.index); |
| setSelectedTabFromQueryParams() { | ||
| const tab = this.route.snapshot.queryParamMap.get('tab'); | ||
|
|
||
| if (tab) { | ||
| const selectedTab = parseInt(tab, 10); | ||
|
|
||
| if (isNaN(selectedTab)) { | ||
| console.warn('Tab index from url is not a number'); | ||
| return; | ||
| } | ||
|
|
||
| const tabWithinRange = selectedTab >= 0 && selectedTab < this.tabLabels.length; | ||
|
|
||
| if (!tabWithinRange) { | ||
| console.warn('Tab index from url is out of range'); | ||
| return; | ||
| } | ||
|
|
||
| this.selectedIndex = selectedTab; |
There was a problem hiding this comment.
The validation logic checks tabLabels.length but this array may be empty during initialization when tabLabels is provided via async pipe (e.g., in segment-overview-details-section-card where tabLabels$ depends on segment$ data). If the component initializes with an empty array and then tabLabels are set later, a valid tab index from the URL would be incorrectly rejected. Consider implementing ngOnChanges to re-validate the tab index when tabLabels changes, or using RxJS to subscribe to tabLabels changes and update the selected tab accordingly.
Adds
&tab=to url when selecting a "common-tabbed-footer"-based tab.Refresh will use the query param to open on that tab.
There was a lot of variance in how different components did tabbing, I found that I could tuck all tabbing logic into the common-tabbed-footer itself to standardize it.
PLUS because the feature components no longer need to manage tabbing I could eliminate a bunch of obsolete code 👍 👍 👍 👍 👍 👍