Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
75aa79b
Add list details page for managing include and exclude list values
zackcl Aug 4, 2026
2927fdd
Merge branch 'dev' into feature/3255-list-details-page
zackcl Aug 20, 2026
30f15ae
fix segment list details navigation race condition
zackcl Aug 20, 2026
4162dfa
fix list value validation and import limits
zackcl Aug 20, 2026
b540b01
fix list details permissions and edge cases
zackcl Aug 20, 2026
999449e
fix list ownership validation and scoped deletion
zackcl Aug 21, 2026
157fe78
fix: prevent formula injection in list CSV exports
zackcl Aug 21, 2026
678d06c
fix: remove client-only list value limit
zackcl Aug 21, 2026
b86b9e1
fix: preserve quoted values in CSV round trips
zackcl Aug 21, 2026
6ddd670
fix: make CSV formula escaping reversible
zackcl Aug 21, 2026
8b84666
fix: preserve CSV duplicates for result reporting
zackcl Aug 21, 2026
229384b
fix: align CSV imports with list value validation
zackcl Aug 21, 2026
8cb7777
fix: preserve list values in CSV round trips
zackcl Aug 21, 2026
48aed1c
refactor: preserve the existing list delete API contract
zackcl Aug 21, 2026
6b68664
test: remove obsolete list deletion mocks
zackcl Aug 21, 2026
a0a4ff8
refactor: move experiment query optimization to a dedicated PR
zackcl Aug 22, 2026
5810367
fix: reject invalid list filter modes
zackcl Aug 22, 2026
3db1d05
fix: cancel stale CSV file reads
zackcl Aug 22, 2026
70fedfa
fix: restore existing CSV export behavior
zackcl Aug 22, 2026
a94f94c
fix: align list permissions with owner pages
zackcl Aug 22, 2026
f6eeb10
fix: report list details load errors consistently
zackcl Aug 22, 2026
f776a67
fix: reject segment-backed list detail routes
zackcl Aug 22, 2026
48fa029
fix: restore simple list CSV import parsing
zackcl Aug 23, 2026
0591e69
fix: restore existing list separator behavior
zackcl Aug 23, 2026
cd57be3
fix: handle legacy segment lists and import modal labels
zackcl Aug 23, 2026
0ef8eef
fix: paginate list detail values
zackcl Aug 24, 2026
937bd88
fix: sync list values from update response
zackcl Aug 24, 2026
c56d83b
test: cover post-create list navigation
zackcl Aug 24, 2026
c59b885
test: streamline list details coverage
zackcl Aug 24, 2026
e438af2
fix: align list details actions with experiment state restrictions
zackcl Aug 24, 2026
17f027b
fix: infer unambiguous legacy list types in list details
zackcl Aug 24, 2026
4de7e15
fix: reject tabs in list values before saving
zackcl Aug 24, 2026
4246f4d
fix: align filtered list empty-state spacing
zackcl Aug 24, 2026
2dc730f
fix: display specific CSV import errors
zackcl Aug 25, 2026
3bd0f6e
Merge remote-tracking branch 'origin/dev' into feature/3255-list-deta…
zackcl Aug 25, 2026
72f49f1
Merge remote-tracking branch 'origin/dev' into feature/3255-list-deta…
zackcl Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import {
actionFetchRewardsDataForExperiment,
actionFetchRewardsDataForExperimentSuccess,
actionFetchRewardsDataForExperimentFailure,
actionAddExperimentInclusionList,
actionAddExperimentInclusionListSuccess,
actionAddExperimentExclusionList,
actionAddExperimentExclusionListSuccess,
} from './experiments.actions';
import { ExperimentEffects } from './experiments.effects';
import {
Expand All @@ -67,6 +71,8 @@ import { actionExecuteQuery, actionFetchMetrics } from '../../analysis/store/ana
import { selectCurrentUser } from '../../auth/store/auth.selectors';
import { UserRole } from '../../users/store/users.model';
import { Environment } from '../../../../environments/environment-types';
import { LIST_FILTER_MODE, SEGMENT_TYPE } from 'upgrade_types';
import { ExperimentSegmentListRequest, LIST_OPTION_TYPE } from '../../segments/store/segments.model';

describe('ExperimentEffects', () => {
let service: ExperimentEffects;
Expand Down Expand Up @@ -1376,6 +1382,108 @@ describe('ExperimentEffects', () => {
}));
});

describe('add experiment lists', () => {
const experimentId = 'experiment-id';
const listId = 'list-id';
const listResponse = { segment: { id: listId } } as any;

const createListRequest = (listType: string): ExperimentSegmentListRequest => ({
experimentId,
list: {
name: 'Test list',
description: '',
context: 'test',
type: SEGMENT_TYPE.PRIVATE,
userIds: [],
groups: [],
subSegmentIds: [],
listType,
},
});

describe('addExperimentInclusionList$', () => {
it('should navigate a direct-value list to its List Details page', fakeAsync(() => {
const list = createListRequest(LIST_OPTION_TYPE.INDIVIDUAL);
experimentDataService.addInclusionList = jest.fn().mockReturnValue(of(listResponse));

const expectedAction = actionAddExperimentInclusionListSuccess({ listResponse });

service.addExperimentInclusionList$.subscribe((result) => {
expect(result).toEqual(expectedAction);
expect(router.navigate).toHaveBeenCalledWith([
'/home',
'detail',
experimentId,
'list',
LIST_FILTER_MODE.INCLUSION,
listId,
]);
});

actions$.next(actionAddExperimentInclusionList({ list }));

tick(0);
}));

it('should keep a Segment-backed list on the owner page', fakeAsync(() => {
const list = createListRequest(LIST_OPTION_TYPE.SEGMENT);
experimentDataService.addInclusionList = jest.fn().mockReturnValue(of(listResponse));

const expectedAction = actionAddExperimentInclusionListSuccess({ listResponse });

service.addExperimentInclusionList$.subscribe((result) => {
expect(result).toEqual(expectedAction);
expect(router.navigate).not.toHaveBeenCalled();
});

actions$.next(actionAddExperimentInclusionList({ list }));

tick(0);
}));
});

describe('addExperimentExclusionList$', () => {
it('should navigate a direct-value list to its List Details page', fakeAsync(() => {
const list = createListRequest(LIST_OPTION_TYPE.INDIVIDUAL);
experimentDataService.addExclusionList = jest.fn().mockReturnValue(of(listResponse));

const expectedAction = actionAddExperimentExclusionListSuccess({ listResponse });

service.addExperimentExclusionList$.subscribe((result) => {
expect(result).toEqual(expectedAction);
expect(router.navigate).toHaveBeenCalledWith([
'/home',
'detail',
experimentId,
'list',
LIST_FILTER_MODE.EXCLUSION,
listId,
]);
});

actions$.next(actionAddExperimentExclusionList({ list }));

tick(0);
}));

it('should keep a Segment-backed list on the owner page', fakeAsync(() => {
const list = createListRequest(LIST_OPTION_TYPE.SEGMENT);
experimentDataService.addExclusionList = jest.fn().mockReturnValue(of(listResponse));

const expectedAction = actionAddExperimentExclusionListSuccess({ listResponse });

service.addExperimentExclusionList$.subscribe((result) => {
expect(result).toEqual(expectedAction);
expect(router.navigate).not.toHaveBeenCalled();
});

actions$.next(actionAddExperimentExclusionList({ list }));

tick(0);
}));
});
});

describe('fetchRewardsDataForExperiment$', () => {
it('should dispatch actionFetchRewardsDataForExperimentSuccess on successful fetch', fakeAsync(() => {
const experimentId = 'test-experiment-123';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import JSZip from 'jszip';
import { TranslateService } from '@ngx-translate/core';
import { CommonModalEventsService } from '../../../shared/services/common-modal-event.service';
import { CommonExportHelpersService } from '../../../shared/services/common-export-helpers.service';
import { LIST_FILTER_MODE } from 'upgrade_types';
import { LIST_OPTION_TYPE } from '../../segments/store/segments.model';
@Injectable()
export class ExperimentEffects {
constructor(
Expand Down Expand Up @@ -560,6 +562,16 @@ export class ExperimentEffects {
map((listResponse) => {
this.notificationService.showSuccess(this.translate.instant('experiments.inclusions.add-success.text'));
this.commonModalEvents.forceCloseModal();
if (action.list.list.listType?.toLowerCase() !== LIST_OPTION_TYPE.SEGMENT.toLowerCase()) {
this.router.navigate([
'/home',
'detail',
action.list.experimentId,
'list',
LIST_FILTER_MODE.INCLUSION,
listResponse.segment.id,
]);
}
return experimentAction.actionAddExperimentInclusionListSuccess({ listResponse });
}),
catchError((error) => {
Expand Down Expand Up @@ -617,6 +629,16 @@ export class ExperimentEffects {
map((listResponse) => {
this.notificationService.showSuccess(this.translate.instant('experiments.exclusions.add-success.text'));
this.commonModalEvents.forceCloseModal();
if (action.list.list.listType?.toLowerCase() !== LIST_OPTION_TYPE.SEGMENT.toLowerCase()) {
this.router.navigate([
'/home',
'detail',
action.list.experimentId,
'list',
LIST_FILTER_MODE.EXCLUSION,
listResponse.segment.id,
]);
}
return experimentAction.actionAddExperimentExclusionListSuccess({ listResponse });
}),
catchError((error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { selectSearchString, selectFeatureFlagPaginationParams } from './feature
import { selectCurrentUser } from '../../auth/store/auth.selectors';
import { CommonExportHelpersService } from '../../../shared/services/common-export-helpers.service';
import { of } from 'rxjs';
import { SERVER_ERROR } from 'upgrade_types';
import { LIST_FILTER_MODE, SERVER_ERROR } from 'upgrade_types';
import { LIST_OPTION_TYPE } from '../../segments/store/segments.model';
import { CommonModalEventsService } from '../../../shared/services/common-modal-event.service';

@Injectable()
Expand Down Expand Up @@ -189,6 +190,16 @@ export class FeatureFlagsEffects {
map((listResponse) => {
this.notificationService.showSuccess(this.translate.instant('feature-flags.inclusions.add-success.text'));
this.commonModalEvents.forceCloseModal();
if (action.list.listType?.toLowerCase() !== LIST_OPTION_TYPE.SEGMENT.toLowerCase()) {
this.router.navigate([
'/featureflags',
'detail',
action.list.id,
'list',
LIST_FILTER_MODE.INCLUSION,
listResponse.segment.id,
]);
}
return FeatureFlagsActions.actionAddFeatureFlagInclusionListSuccess({ listResponse });
}),
catchError((error) => {
Expand Down Expand Up @@ -270,6 +281,16 @@ export class FeatureFlagsEffects {
map((listResponse) => {
this.notificationService.showSuccess(this.translate.instant('feature-flags.exclusions.add-success.text'));
this.commonModalEvents.forceCloseModal();
if (action.list.listType?.toLowerCase() !== LIST_OPTION_TYPE.SEGMENT.toLowerCase()) {
this.router.navigate([
'/featureflags',
'detail',
action.list.id,
'list',
LIST_FILTER_MODE.EXCLUSION,
listResponse.segment.id,
]);
}
return FeatureFlagsActions.actionAddFeatureFlagExclusionListSuccess({ listResponse });
}),
catchError((error) => {
Expand Down
Loading