From cef75b4e3f85911cc6d9e271d8318b1057bc34d9 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Fri, 10 Jul 2026 00:20:22 -0400 Subject: [PATCH 1/5] Make decision points navigate to filtered experiments --- ...ecision-points-section-card.component.html | 1 + ...sion-points-section-card.component.spec.ts | 67 +++++++++++++++++++ ...-decision-points-section-card.component.ts | 12 +++- ...iment-decision-points-table.component.html | 8 ++- ...iment-decision-points-table.component.scss | 12 ++++ ...nt-decision-points-table.component.spec.ts | 48 +++++++++++++ ...eriment-decision-points-table.component.ts | 5 ++ 7 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts create mode 100644 packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.html index 8fe33fcf77..254c8c7523 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.html @@ -35,6 +35,7 @@ [actionsTooltip]="restrictionTooltip" [experimentState]="vm.experiment.state" (rowAction)="onRowAction($event, vm.experiment.id, appContext)" + (decisionPointClick)="onDecisionPointClick($event)" > } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts new file mode 100644 index 0000000000..a85a792a0e --- /dev/null +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts @@ -0,0 +1,67 @@ +import type { Router } from '@angular/router'; +import { EXPERIMENT_SEARCH_KEY } from 'upgrade_types'; +import type { ExperimentDecisionPoint } from '../../../../../../../core/experiments/store/experiments.model'; +import type { ExperimentService } from '../../../../../../../core/experiments/experiments.service'; +import type { AuthService } from '../../../../../../../core/auth/auth.service'; +import type { DialogService } from '../../../../../../../shared/services/common-dialog.service'; +import type { DecisionPointHelperService } from '../../../../../../../core/experiments/decision-point-helper.service'; + +jest.mock( + '@shared-component-lib', + () => ({ + CommonSectionCardActionButtonsComponent: class CommonSectionCardActionButtonsComponent {}, + CommonSectionCardComponent: class CommonSectionCardComponent {}, + CommonSectionCardTitleHeaderComponent: class CommonSectionCardTitleHeaderComponent {}, + }), + { virtual: true } +); + +jest.mock('../../../../../../../shared/services/common-dialog.service', () => ({ + DialogService: class DialogService {}, +})); + +import { ExperimentDecisionPointsSectionCardComponent } from './experiment-decision-points-section-card.component'; + +describe('ExperimentDecisionPointsSectionCardComponent', () => { + let component: ExperimentDecisionPointsSectionCardComponent; + let experimentService: jest.Mocked>; + let router: jest.Mocked>; + + const decisionPoint = { + id: 'decision-point-1', + site: 'lesson-stream', + target: 'question-hint', + description: '', + order: 1, + createdAt: '', + updatedAt: '', + versionNumber: 1, + excludeIfReached: false, + } as ExperimentDecisionPoint; + + beforeEach(() => { + experimentService = { + setSearchKey: jest.fn(), + setSearchString: jest.fn(), + }; + router = { + navigate: jest.fn(), + }; + + component = new ExperimentDecisionPointsSectionCardComponent( + experimentService as unknown as ExperimentService, + {} as AuthService, + {} as DialogService, + {} as DecisionPointHelperService, + router as unknown as Router + ); + }); + + it('should filter experiments by clicked decision point and navigate to the experiments root page', () => { + component.onDecisionPointClick(decisionPoint); + + expect(experimentService.setSearchKey).toHaveBeenCalledWith(EXPERIMENT_SEARCH_KEY.DECISION_POINT); + expect(experimentService.setSearchString).toHaveBeenCalledWith('lesson-stream (question-hint)'); + expect(router.navigate).toHaveBeenCalledWith(['/home']); + }); +}); diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts index 7b7ffcaba7..40e8750247 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts @@ -21,6 +21,9 @@ import { import { UserPermission } from '../../../../../../../core/auth/store/auth.models'; import { AuthService } from '../../../../../../../core/auth/auth.service'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; +import { Router } from '@angular/router'; +import { EXPERIMENT_SEARCH_KEY } from 'upgrade_types'; +import { formatDecisionPointDisplay } from '../../../../experiment-decision-point.utils'; @Component({ selector: 'app-experiment-decision-points-section-card', @@ -46,7 +49,8 @@ export class ExperimentDecisionPointsSectionCardComponent implements OnInit { readonly experimentService: ExperimentService, private readonly authService: AuthService, private readonly dialogService: DialogService, - private readonly decisionPointHelperService: DecisionPointHelperService + private readonly decisionPointHelperService: DecisionPointHelperService, + private readonly router: Router ) {} ngOnInit() { @@ -89,6 +93,12 @@ export class ExperimentDecisionPointsSectionCardComponent implements OnInit { this.dialogService.openEditDecisionPointModal(decisionPoint, experimentId, context); } + onDecisionPointClick(decisionPoint: ExperimentDecisionPoint): void { + this.experimentService.setSearchKey(EXPERIMENT_SEARCH_KEY.DECISION_POINT); + this.experimentService.setSearchString(formatDecisionPointDisplay(decisionPoint)); + this.router.navigate(['/home']); + } + onDeleteDecisionPoint(decisionPoint: ExperimentDecisionPoint): void { const decisionPointDisplayName = `${decisionPoint.site}; ${decisionPoint.target}`; diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html index 3eb97767e7..04cf8fa1b2 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html @@ -17,13 +17,15 @@ {{ DECISION_POINT_TRANSLATION_KEYS.DECISION_POINT | translate }} - {{ getDecisionPoint(decisionPoint) }} - + diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss index 8b800e4006..0214533449 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss @@ -65,6 +65,18 @@ .decision-point-column { width: 70%; padding-left: 32px; + + .decision-point-link { + border: 0; + background: transparent; + color: var(--black-2); + cursor: pointer; + font: inherit; + max-width: 100%; + padding: 0; + text-align: left; + text-decoration: underline; + } } .exclude-if-reached-column { diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts new file mode 100644 index 0000000000..79d8851204 --- /dev/null +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { of } from 'rxjs'; +import { ExperimentDecisionPointsTableComponent } from './experiment-decision-points-table.component'; +import type { ExperimentDecisionPoint } from '../../../../../../../../core/experiments/store/experiments.model'; + +describe('ExperimentDecisionPointsTableComponent', () => { + let component: ExperimentDecisionPointsTableComponent; + let fixture: ComponentFixture; + + const decisionPoint = { + id: 'decision-point-1', + site: 'lesson-stream', + target: 'question-hint', + description: '', + order: 1, + createdAt: '', + updatedAt: '', + versionNumber: 1, + excludeIfReached: false, + } as ExperimentDecisionPoint; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExperimentDecisionPointsTableComponent, NoopAnimationsModule, TranslateModule.forRoot()], + }).compileComponents(); + + fixture = TestBed.createComponent(ExperimentDecisionPointsTableComponent); + component = fixture.componentInstance; + component.decisionPoints = [decisionPoint]; + component.isLoading$ = of(false); + fixture.detectChanges(); + }); + + it('should emit the clicked decision point when the decision point text is clicked', () => { + const emitSpy = jest.spyOn(component.decisionPointClick, 'emit'); + const decisionPointButton: HTMLButtonElement = fixture.nativeElement.querySelector('.decision-point-link'); + + decisionPointButton.click(); + + expect(emitSpy).toHaveBeenCalledWith(decisionPoint); + }); + + it('should format decision points using the shared display format', () => { + expect(component.getDecisionPoint(decisionPoint)).toBe('lesson-stream (question-hint)'); + }); +}); diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts index 93ed896734..97ce5d7975 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts @@ -42,6 +42,7 @@ export class ExperimentDecisionPointsTableComponent { @Input() actionsTooltip?: string = ''; @Input() experimentState?: EXPERIMENT_STATE; @Output() rowAction = new EventEmitter(); + @Output() decisionPointClick = new EventEmitter(); displayedColumns: string[] = ['decisionPoint', 'excludeIfReached', 'actions']; @@ -81,6 +82,10 @@ export class ExperimentDecisionPointsTableComponent { return formatDecisionPointDisplay(decisionPoint); } + onDecisionPointClick(decisionPoint: ExperimentDecisionPoint): void { + this.decisionPointClick.emit(decisionPoint); + } + onEditButtonClick(decisionPoint: ExperimentDecisionPoint): void { this.rowAction.emit({ action: EXPERIMENT_ROW_ACTION.EDIT, decisionPoint }); } From 967d7456793d4723c721daef205b0e36328f7e5d Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Fri, 10 Jul 2026 01:30:59 -0400 Subject: [PATCH 2/5] Make decision point links apply search filters atomically --- .../experiments/experiments.service.spec.ts | 25 +++++++++++++++++++ .../core/experiments/experiments.service.ts | 6 +++++ .../experiments/store/experiments.actions.ts | 5 ++++ .../store/experiments.effects.spec.ts | 23 +++++++++++++++++ .../experiments/store/experiments.effects.ts | 13 ++++++++++ .../store/experiments.reducer.spec.ts | 18 +++++++++++++ .../experiments/store/experiments.reducer.ts | 5 ++++ ...sion-points-section-card.component.spec.ts | 11 ++++---- ...-decision-points-section-card.component.ts | 6 +++-- ...iment-decision-points-table.component.scss | 6 +++++ 10 files changed, 111 insertions(+), 7 deletions(-) diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts index 084fd1c8fa..bbb7ba8270 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.spec.ts @@ -28,6 +28,7 @@ import { actionGetExperiments, actionSetGraphRange, actionSetSearchKey, + actionSetSearchParams, actionSetSearchString, actionSetSortingType, actionSetSortKey, @@ -419,6 +420,30 @@ describe('ExperimentService', () => { }); }); + describe('#setSearchParams', () => { + it('should set localStorage items and dispatch actionSetSearchParams with the given inputs', () => { + const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT; + const searchString = 'lesson-stream (question-hint)'; + + service.setSearchParams(searchKey, searchString); + + expect(mockLocalStorageService.setItem).toHaveBeenCalledWith( + ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_KEY, + searchKey + ); + expect(mockLocalStorageService.setItem).toHaveBeenCalledWith( + ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_STRING, + searchString + ); + expect(mockStore.dispatch).toHaveBeenCalledWith( + actionSetSearchParams({ + searchKey, + searchString, + }) + ); + }); + }); + describe('#setSortKey', () => { it('should set localStorage item and dispatch actionSetSortKey with the given input', () => { const sortKey = EXPERIMENT_SORT_KEY.UPDATED_AT; diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts index 12e15a88fd..5f28346aa4 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.service.ts @@ -203,6 +203,12 @@ export class ExperimentService { this.store$.dispatch(experimentAction.actionSetSearchString({ searchString })); } + setSearchParams(searchKey: EXPERIMENT_SEARCH_KEY, searchString: string) { + this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_KEY, searchKey); + this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SEARCH_STRING, searchString); + this.store$.dispatch(experimentAction.actionSetSearchParams({ searchKey, searchString })); + } + setSortKey(sortKey: EXPERIMENT_SORT_KEY) { this.localStorageService.setItem(ExperimentLocalStorageKeys.EXPERIMENT_SORT_KEY, sortKey); this.store$.dispatch(experimentAction.actionSetSortKey({ sortKey })); diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.actions.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.actions.ts index 5bc1194c38..c562464d9c 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.actions.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.actions.ts @@ -159,6 +159,11 @@ export const actionSetSearchKey = createAction( export const actionSetSearchString = createAction('[Experiment] Set Search String', props<{ searchString: string }>()); +export const actionSetSearchParams = createAction( + '[Experiment] Set Search Params', + props<{ searchKey: EXPERIMENT_SEARCH_KEY; searchString: string }>() +); + export const actionSetSortKey = createAction( '[Experiment] Set Sort key value', props<{ sortKey: EXPERIMENT_SORT_KEY }>() diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts index 17c78430c7..ce2a3a098c 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts @@ -38,6 +38,7 @@ import { actionRemoveExperimentStat, actionSetSearchString, actionSetSearchKey, + actionSetSearchParams, actionFetchContextMetaData, actionFetchContextMetaDataSuccess, actionFetchContextMetaDataFailure, @@ -1086,6 +1087,28 @@ describe('ExperimentEffects', () => { })); }); + describe('#fetchExperimentOnSearchParamsChange$', () => { + it('should dispatch actionGetExperiments once if search string is not null', fakeAsync(() => { + const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT; + const searchString = 'lesson-stream (question-hint)'; + store$.dispatch = jest.fn(); + + service.fetchExperimentOnSearchParamsChange$.subscribe(); + + actions$.next( + actionSetSearchParams({ + searchKey, + searchString, + }) + ); + + tick(0); + + expect(store$.dispatch).toHaveBeenCalledTimes(1); + expect(store$.dispatch).toHaveBeenCalledWith(actionGetExperiments({ fromStarting: true })); + })); + }); + describe('#fetchContextMetaData$', () => { it('should not do anything if contextMetaData object already exists', fakeAsync(() => { let neverEmitted = true; diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts index 849acb3fca..a89ce21638 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts @@ -458,6 +458,19 @@ export class ExperimentEffects { { dispatch: false } ); + fetchExperimentOnSearchParamsChange$ = createEffect( + () => + this.actions$.pipe( + ofType(experimentAction.actionSetSearchParams), + tap(({ searchString }) => { + if (searchString !== null) { + this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true })); + } + }) + ), + { dispatch: false } + ); + fetchContextMetaData$ = createEffect(() => this.actions$.pipe( ofType(experimentAction.actionFetchContextMetaData), diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.spec.ts index e9bcf08213..91e87b2c8e 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.spec.ts @@ -23,6 +23,7 @@ import { actionSetIsGraphLoading, actionSetIsLoadingExperiment, actionSetSearchKey, + actionSetSearchParams, actionSetSearchString, actionSetSkipExperiment, actionSetSortingType, @@ -558,6 +559,23 @@ describe('ExperimentsReducer', () => { expect(newState.searchString).toEqual('test'); }); + it('action "actionSetSearchParams" should set search key and search string', () => { + const previousState = { ...initialState }; + previousState.searchKey = EXPERIMENT_SEARCH_KEY.ALL; + previousState.searchString = 'previous'; + + const testAction: Action = actionSetSearchParams({ + searchKey: EXPERIMENT_SEARCH_KEY.DECISION_POINT, + searchString: 'lesson-stream (question-hint)', + }); + + const newState = experimentsReducer(previousState, testAction); + + expect(newState).not.toBe(previousState); + expect(newState.searchKey).toEqual(EXPERIMENT_SEARCH_KEY.DECISION_POINT); + expect(newState.searchString).toEqual('lesson-stream (question-hint)'); + }); + it('action "actionSetSortKey" should set sort key', () => { const previousState = { ...initialState }; previousState.sortKey = EXPERIMENT_SORT_KEY.NAME; diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.ts index ee3b08cf76..91bb8804f3 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.reducer.ts @@ -204,6 +204,11 @@ const reducer = createReducer( })), on(experimentsAction.actionSetSearchKey, (state, { searchKey }) => ({ ...state, searchKey })), on(experimentsAction.actionSetSearchString, (state, { searchString }) => ({ ...state, searchString })), + on(experimentsAction.actionSetSearchParams, (state, { searchKey, searchString }) => ({ + ...state, + searchKey, + searchString, + })), on(experimentsAction.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })), on(experimentsAction.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })), on(experimentsAction.actionSetSkipExperiment, (state, { skipExperiment }) => ({ ...state, skipExperiment })), diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts index a85a792a0e..27ee97db75 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.spec.ts @@ -24,7 +24,7 @@ import { ExperimentDecisionPointsSectionCardComponent } from './experiment-decis describe('ExperimentDecisionPointsSectionCardComponent', () => { let component: ExperimentDecisionPointsSectionCardComponent; - let experimentService: jest.Mocked>; + let experimentService: jest.Mocked>; let router: jest.Mocked>; const decisionPoint = { @@ -41,8 +41,7 @@ describe('ExperimentDecisionPointsSectionCardComponent', () => { beforeEach(() => { experimentService = { - setSearchKey: jest.fn(), - setSearchString: jest.fn(), + setSearchParams: jest.fn(), }; router = { navigate: jest.fn(), @@ -60,8 +59,10 @@ describe('ExperimentDecisionPointsSectionCardComponent', () => { it('should filter experiments by clicked decision point and navigate to the experiments root page', () => { component.onDecisionPointClick(decisionPoint); - expect(experimentService.setSearchKey).toHaveBeenCalledWith(EXPERIMENT_SEARCH_KEY.DECISION_POINT); - expect(experimentService.setSearchString).toHaveBeenCalledWith('lesson-stream (question-hint)'); + expect(experimentService.setSearchParams).toHaveBeenCalledWith( + EXPERIMENT_SEARCH_KEY.DECISION_POINT, + 'lesson-stream (question-hint)' + ); expect(router.navigate).toHaveBeenCalledWith(['/home']); }); }); diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts index 40e8750247..35af4e66a2 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-section-card.component.ts @@ -94,8 +94,10 @@ export class ExperimentDecisionPointsSectionCardComponent implements OnInit { } onDecisionPointClick(decisionPoint: ExperimentDecisionPoint): void { - this.experimentService.setSearchKey(EXPERIMENT_SEARCH_KEY.DECISION_POINT); - this.experimentService.setSearchString(formatDecisionPointDisplay(decisionPoint)); + this.experimentService.setSearchParams( + EXPERIMENT_SEARCH_KEY.DECISION_POINT, + formatDecisionPointDisplay(decisionPoint) + ); this.router.navigate(['/home']); } diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss index 0214533449..94737fe6ed 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss @@ -76,6 +76,12 @@ padding: 0; text-align: left; text-decoration: underline; + + &:focus-visible { + border-radius: 2px; + outline: 2px solid var(--blue); + outline-offset: 2px; + } } } From a3dbc92904f49b9a0c50055d9809dbd6e2e63def Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Fri, 10 Jul 2026 01:42:00 -0400 Subject: [PATCH 3/5] Make decision point links apply search filters atomically --- .../app/core/experiments/store/experiments.effects.spec.ts | 2 +- .../src/app/core/experiments/store/experiments.effects.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts index ce2a3a098c..46de8b939a 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts @@ -1088,7 +1088,7 @@ describe('ExperimentEffects', () => { }); describe('#fetchExperimentOnSearchParamsChange$', () => { - it('should dispatch actionGetExperiments once if search string is not null', fakeAsync(() => { + it('should dispatch actionGetExperiments once when search params change', fakeAsync(() => { const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT; const searchString = 'lesson-stream (question-hint)'; store$.dispatch = jest.fn(); diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts index a89ce21638..bdd9506d56 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts @@ -462,10 +462,8 @@ export class ExperimentEffects { () => this.actions$.pipe( ofType(experimentAction.actionSetSearchParams), - tap(({ searchString }) => { - if (searchString !== null) { - this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true })); - } + tap(() => { + this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true })); }) ), { dispatch: false } From 889aaeca3874dd0b6e463752b743469f12c20419 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Fri, 10 Jul 2026 01:51:52 -0400 Subject: [PATCH 4/5] Make decision point links apply search filters atomically --- .../store/experiments.effects.spec.ts | 23 ++++++++++++++++++- .../experiments/store/experiments.effects.ts | 5 +++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts index 46de8b939a..804d9ab623 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.spec.ts @@ -1088,9 +1088,10 @@ describe('ExperimentEffects', () => { }); describe('#fetchExperimentOnSearchParamsChange$', () => { - it('should dispatch actionGetExperiments once when search params change', fakeAsync(() => { + it('should dispatch actionGetExperiments once when search params change on the experiments root page', fakeAsync(() => { const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT; const searchString = 'lesson-stream (question-hint)'; + router.url = '/home'; store$.dispatch = jest.fn(); service.fetchExperimentOnSearchParamsChange$.subscribe(); @@ -1107,6 +1108,26 @@ describe('ExperimentEffects', () => { expect(store$.dispatch).toHaveBeenCalledTimes(1); expect(store$.dispatch).toHaveBeenCalledWith(actionGetExperiments({ fromStarting: true })); })); + + it('should not dispatch actionGetExperiments when search params change outside the experiments root page', fakeAsync(() => { + const searchKey = EXPERIMENT_SEARCH_KEY.DECISION_POINT; + const searchString = 'lesson-stream (question-hint)'; + router.url = '/home/detail/experiment-id'; + store$.dispatch = jest.fn(); + + service.fetchExperimentOnSearchParamsChange$.subscribe(); + + actions$.next( + actionSetSearchParams({ + searchKey, + searchString, + }) + ); + + tick(0); + + expect(store$.dispatch).not.toHaveBeenCalled(); + })); }); describe('#fetchContextMetaData$', () => { diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts index bdd9506d56..d2c8f23454 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.effects.ts @@ -463,7 +463,10 @@ export class ExperimentEffects { this.actions$.pipe( ofType(experimentAction.actionSetSearchParams), tap(() => { - this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true })); + const pathname = (this.router.url || '').split('?')[0].split('#')[0]; + if (pathname === '/home') { + this.store$.dispatch(experimentAction.actionGetExperiments({ fromStarting: true })); + } }) ), { dispatch: false } From 82445d2788d93c5b0efdc7ef4e323af281aeaf90 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Fri, 10 Jul 2026 02:28:32 -0400 Subject: [PATCH 5/5] Use atomic search params across experiment filters --- ...eriment-overview-details-section-card.component.ts | 5 ++--- .../experiment-root-section-card-table.component.ts | 11 +---------- .../experiment-root-section-card.component.ts | 6 ++++-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-overview-details-section-card/experiment-overview-details-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-overview-details-section-card/experiment-overview-details-section-card.component.ts index 71b663ebdf..965b4fce48 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-overview-details-section-card/experiment-overview-details-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-overview-details-section-card/experiment-overview-details-section-card.component.ts @@ -97,9 +97,8 @@ export class ExperimentOverviewDetailsSectionCardComponent implements OnInit, On ) {} filterExperimentByChips(tagValue: string) { - this.experimentService.setSearchKey(EXPERIMENT_SEARCH_KEY.TAG); - this.experimentService.setSearchString(tagValue); - this.router.navigate(['/experiments']); + this.experimentService.setSearchParams(EXPERIMENT_SEARCH_KEY.TAG, tagValue); + this.router.navigate(['/home']); } ngOnInit(): void { this.subscriptions.add(this.experimentService.currentUserEmailAddress$.subscribe((id) => (this.emailId = id))); diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card-table/experiment-root-section-card-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card-table/experiment-root-section-card-table.component.ts index a55d09d167..fcd225e1fe 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card-table/experiment-root-section-card-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card-table/experiment-root-section-card-table.component.ts @@ -94,16 +94,7 @@ export class ExperimentRootSectionCardTableComponent implements AfterViewInit, O } filterExperimentByChips(tagValue: string, type: EXPERIMENT_SEARCH_KEY) { - this.setSearchKey(type); - this.setSearchString(tagValue); - } - - setSearchKey(searchKey: EXPERIMENT_SEARCH_KEY) { - this.experimentService.setSearchKey(searchKey); - } - - setSearchString(searchString: string) { - this.experimentService.setSearchString(searchString); + this.experimentService.setSearchParams(type, tagValue); } get EXPERIMENT_STATE() { diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card.component.ts index 2e08ec977e..3d66b9b6ac 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-root-page/experiment-root-page-content/experiment-root-section-card/experiment-root-section-card.component.ts @@ -89,8 +89,10 @@ export class ExperimentRootSectionCardComponent { } onSearch(params: CommonSearchWidgetSearchParams) { - this.experimentService.setSearchKey(params.searchKey as EXPERIMENT_SEARCH_KEY); - this.experimentService.setSearchString(params.searchString?.trim() || ''); + this.experimentService.setSearchParams( + params.searchKey as EXPERIMENT_SEARCH_KEY, + params.searchString?.trim() || '' + ); } onAddExperimentButtonClick() {