From 2c08cd7331a9842f4c550f9867934a6bb5d0db02 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 18:17:30 +0900 Subject: [PATCH 1/9] [WIP] Implement Add/Edit/Delete Segment List Modals 2 --- .../validators/FeatureFlagListValidator.ts | 2 +- .../src/api/services/FeatureFlagService.ts | 16 +-- .../controllers/SegmentController.test.ts | 54 ++++++++++ .../controllers/mocks/SegmentServiceMock.ts | 12 ++- .../unit/services/FeatureFlagService.test.ts | 2 +- .../store/feature-flags.model.ts | 1 + .../core/segments/segments.data.service.ts | 71 ++++++++++++- .../src/app/core/segments/segments.service.ts | 19 ++++ .../core/segments/store/segments.actions.ts | 45 +++++++++ .../core/segments/store/segments.effects.ts | 36 +++++++ .../app/core/segments/store/segments.model.ts | 9 +- .../core/segments/store/segments.reducer.ts | 99 ++++++++++++++++++- ...-flag-exclusions-section-card.component.ts | 27 +---- ...-flag-inclusions-section-card.component.ts | 2 +- ...rt-private-segment-list-modal.component.ts | 22 ++++- .../segment-lists-section-card.component.ts | 38 ++++++- .../shared/services/common-dialog.service.ts | 46 ++++++++- .../projects/upgrade/src/assets/i18n/en.json | 3 + .../src/environments/environment-types.ts | 1 + .../src/environments/environment.prod.ts | 1 + .../src/environments/environment.qa.ts | 1 + .../src/environments/environment.staging.ts | 1 + .../upgrade/src/environments/environment.ts | 1 + 23 files changed, 450 insertions(+), 59 deletions(-) diff --git a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts index 767b0677bc..1e9a057fe6 100644 --- a/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts +++ b/backend/packages/Upgrade/src/api/controllers/validators/FeatureFlagListValidator.ts @@ -5,7 +5,7 @@ import { SegmentInputValidator } from './SegmentInputValidator'; export class FeatureFlagListValidator { @IsNotEmpty() @IsUUID() - public flagId: string; + public id: string; @IsDefined() @IsBoolean() diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index 013966e05b..c3adbd6f8e 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -600,7 +600,7 @@ export class FeatureFlagService { const featureFlags = await manager .getRepository(FeatureFlag) - .findByIds(listsInput.map((listInput) => listInput.flagId)); + .findByIds(listsInput.map((listInput) => listInput.id)); const featureFlagSegmentInclusionOrExclusionArray = listsInput.map((listInput) => { const featureFlagSegmentInclusionOrExclusion = @@ -679,23 +679,23 @@ export class FeatureFlagService { return await this.dataSource.transaction(async (transactionalEntityManager) => { // Find the existing record let existingRecord: FeatureFlagSegmentInclusion | FeatureFlagSegmentExclusion; - const featureFlag = await this.findOne(listInput.flagId); + const featureFlag = await this.findOne(listInput.id); if (filterType === FEATURE_FLAG_LIST_FILTER_MODE.INCLUSION) { existingRecord = await this.featureFlagSegmentInclusionRepository.findOne({ - where: { featureFlag: { id: listInput.flagId }, segment: { id: listInput.segment.id } }, + where: { featureFlag: { id: listInput.id }, segment: { id: listInput.segment.id } }, relations: ['featureFlag', 'segment'], }); } else { existingRecord = await this.featureFlagSegmentExclusionRepository.findOne({ - where: { featureFlag: { id: listInput.flagId }, segment: { id: listInput.segment.id } }, + where: { featureFlag: { id: listInput.id }, segment: { id: listInput.segment.id } }, relations: ['featureFlag', 'segment'], }); } if (!existingRecord) { throw new Error( - `No existing ${filterType} record found for feature flag ${listInput.flagId} and segment ${listInput.segment.id}` + `No existing ${filterType} record found for feature flag ${listInput.id} and segment ${listInput.segment.id}` ); } @@ -961,7 +961,7 @@ export class FeatureFlagService { return { ...segmentInclusionList, enabled: false, - flagId: newFlag.id, + id: newFlag.id, segment: { ...segmentInclusionList.segment, userIds, subSegmentIds, groups }, }; }); @@ -981,7 +981,7 @@ export class FeatureFlagService { return { ...segmentExclusionList, - flagId: newFlag.id, + id: newFlag.id, segment: { ...segmentExclusionList.segment, userIds, subSegmentIds, groups }, }; }); @@ -1171,7 +1171,7 @@ export class FeatureFlagService { const listDoc: FeatureFlagListValidator = { ...list, enabled: false, - flagId: featureFlagId, + id: featureFlagId, segment: { ...list.segment, id: uuid() }, }; diff --git a/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts b/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts index 0980189ee3..f0cb073a91 100644 --- a/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts +++ b/backend/packages/Upgrade/test/unit/controllers/SegmentController.test.ts @@ -40,6 +40,18 @@ describe('Segment Controller Testing', () => { subSegmentIds: ['seg2'], }; + const listInputData = { + parentSegmentId: uuid(), + name: 'list1', + description: 'list description', + context: 'home', + type: 'private', + userIds: ['user1', 'user2'], + groups: [], + subSegmentIds: [], + listType: 'Individual', + }; + test('Get request for /api/segments', () => { return request(app) .get('/api/segments') @@ -111,4 +123,46 @@ describe('Segment Controller Testing', () => { .expect('Content-Type', /json/) .expect(200); }); + + test('Post request for /api/segments/list (addSegmentList)', () => { + return request(app) + .post('/api/segments/list') + .send(listInputData) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Post request for /api/segments (updateSegmentList)', () => { + const updateListData = { + id: uuid(), + name: 'updated list', + description: 'updated description', + context: 'home', + type: 'private', + userIds: ['user1', 'user2', 'user3'], + groups: [], + subSegmentIds: [], + listType: 'Individual', + }; + + return request(app) + .post('/api/segments') + .send(updateListData) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); + + test('Delete request for /api/segments/list/:segmentId (deleteSegmentList)', () => { + const segmentId = uuid(); + const parentSegmentId = uuid(); + + return request(app) + .delete(`/api/segments/list/${segmentId}`) + .send({ parentSegmentId }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + }); }); diff --git a/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts b/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts index 74b2e4c754..2bf2d28b83 100644 --- a/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts +++ b/backend/packages/Upgrade/test/unit/controllers/mocks/SegmentServiceMock.ts @@ -1,7 +1,7 @@ import { Service } from 'typedi'; @Service() -export default class ExcludeServiceMock { +export default class SegmentServiceMock { public getAllSegments(): Promise<[]> { return Promise.resolve([]); } @@ -9,7 +9,7 @@ export default class ExcludeServiceMock { public getAllSegmentWithStatus(): Promise<[]> { return Promise.resolve([]); } - + public getSegmentById(id: string): Promise<[]> { return Promise.resolve([]); } @@ -53,4 +53,12 @@ export default class ExcludeServiceMock { public exportSegmentCSV(id: string): Promise<[]> { return Promise.resolve([]); } + + public addList(): Promise<[]> { + return Promise.resolve([]); + } + + public deleteList(): Promise<[]> { + return Promise.resolve([]); + } } diff --git a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts index 8e4f27ff34..f419bdc0be 100644 --- a/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts +++ b/backend/packages/Upgrade/test/unit/services/FeatureFlagService.test.ts @@ -86,7 +86,7 @@ describe('Feature Flag Service Testing', () => { const mockList = new FeatureFlagListValidator(); mockList.enabled = true; - mockList.flagId = mockFlag1.id; + mockList.id = mockFlag1.id; mockList.listType = 'individual'; mockList.segment = mockSegment; diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts index e3326b88df..9e8b5371aa 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.model.ts @@ -50,6 +50,7 @@ export interface FeatureFlagSegmentListDetails { featureFlag: FeatureFlag; enabled: boolean; listType: MemberTypes | string; + parentSegmentId?: string; } export enum UPSERT_FEATURE_FLAG_ACTION { diff --git a/frontend/projects/upgrade/src/app/core/segments/segments.data.service.ts b/frontend/projects/upgrade/src/app/core/segments/segments.data.service.ts index 8780e82e2e..864070caa3 100644 --- a/frontend/projects/upgrade/src/app/core/segments/segments.data.service.ts +++ b/frontend/projects/upgrade/src/app/core/segments/segments.data.service.ts @@ -1,14 +1,18 @@ import { Inject, Injectable } from '@angular/core'; import { + AddPrivateSegmentListRequest, AddSegmentRequest, + EditPrivateSegmentListRequest, + Segment, SegmentFile, SegmentInput, SegmentsPaginationInfo, SegmentsPaginationParams, } from './store/segments.model'; -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { HttpClient, HttpParams } from '@angular/common/http'; import { ENV, Environment } from '../../../environments/environment-types'; +import { FeatureFlagSegmentListDetails } from '../feature-flags/store/feature-flags.model'; @Injectable() export class SegmentsDataService { @@ -95,4 +99,69 @@ export class SegmentsDataService { const url = this.environment.api.validateSegmentsImport; return this.http.post(url, segments); } + + addSegmentList(list: AddPrivateSegmentListRequest): Observable { + const url = this.environment.api.addSegmentList; + + // Transform AddPrivateSegmentListRequest to ListInputValidator format + const transformedList = { + parentSegmentId: list.id, + name: list.segment.name, + description: list.segment.description, + context: list.segment.context, + type: list.segment.type, + userIds: list.segment.userIds, + groups: list.segment.groups, + subSegmentIds: list.segment.subSegmentIds, + listType: list.listType, + }; + + // Return type transformation - adapting Segment to FeatureFlagSegmentListDetails + return this.http.post(url, transformedList).pipe( + map((segment: Segment) => { + return { + segment: segment, + featureFlag: null, + enabled: list.enabled, + listType: list.listType, + parentSegmentId: list.id, + } as FeatureFlagSegmentListDetails; + }) + ); + } + + updateSegmentList(list: EditPrivateSegmentListRequest): Observable { + const url = this.environment.api.segments; + + // Transform EditPrivateSegmentListRequest to SegmentInputValidator format + const transformedList = { + id: list.segment.id, + name: list.segment.name, + description: list.segment.description, + context: list.segment.context, + type: list.segment.type, + userIds: list.segment.userIds, + groups: list.segment.groups, + subSegmentIds: list.segment.subSegmentIds, + listType: list.listType, + }; + + // Return type transformation - adapting Segment to FeatureFlagSegmentListDetails + return this.http.post(url, transformedList).pipe( + map((segment: Segment) => { + return { + segment: segment, + featureFlag: null, + enabled: list.enabled, + listType: list.listType, + parentSegmentId: list.id, + } as FeatureFlagSegmentListDetails; + }) + ); + } + + deleteSegmentList(segmentId: string, parentSegmentId: string) { + const url = `${this.environment.api.addSegmentList}/${segmentId}`; + return this.http.delete(url, { body: { parentSegmentId } }); + } } diff --git a/frontend/projects/upgrade/src/app/core/segments/segments.service.ts b/frontend/projects/upgrade/src/app/core/segments/segments.service.ts index 4a966e63d5..822e475a0b 100644 --- a/frontend/projects/upgrade/src/app/core/segments/segments.service.ts +++ b/frontend/projects/upgrade/src/app/core/segments/segments.service.ts @@ -27,7 +27,9 @@ import { selectSegmentIdAfterNavigation, } from './store/segments.selectors'; import { + AddPrivateSegmentListRequest, AddSegmentRequest, + EditPrivateSegmentListRequest, LIST_OPTION_TYPE, Segment, SegmentInput, @@ -233,4 +235,21 @@ export class SegmentsService { setDuplicateSegmentNameError(error: DuplicateSegmentNameError) { this.duplicateSegmentNameError$.next(error); } + + addPrivateSegmentList(list: AddPrivateSegmentListRequest) { + this.store$.dispatch(SegmentsActions.actionAddSegmentList({ list })); + } + + updatePrivateSegmentList(list: EditPrivateSegmentListRequest) { + this.store$.dispatch(SegmentsActions.actionUpdateSegmentList({ list })); + } + + deletePrivateSegmentList(segmentId: string, parentSegmentId: string) { + this.store$.dispatch( + SegmentsActions.actionDeleteSegmentList({ + segmentId, + parentSegmentId, + }) + ); + } } diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.actions.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.actions.ts index 866155a6ab..823e2bbd32 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.actions.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.actions.ts @@ -1,6 +1,8 @@ import { createAction, props } from '@ngrx/store'; import { + AddPrivateSegmentListRequest, AddSegmentRequest, + EditPrivateSegmentListRequest, Segment, SegmentInput, UpdateSegmentRequest, @@ -13,6 +15,7 @@ import { SORT_AS_DIRECTION, SEGMENT_SORT_KEY, } from '../../../../../../../../types/src/Experiment/enums'; +import { FeatureFlagSegmentListDetails } from '../../feature-flags/store/feature-flags.model'; export const actionFetchSegments = createAction('[Segments] Segments', props<{ fromStarting?: boolean }>()); @@ -147,3 +150,45 @@ export const actionSetGlobalSortingType = createAction( '[Global Segments] Set Sorting type', props<{ sortingType: SORT_AS_DIRECTION }>() ); + +export const actionAddSegmentList = createAction( + '[Segments] Add Segment List', + props<{ list: AddPrivateSegmentListRequest }>() +); + +export const actionAddSegmentListSuccess = createAction( + '[Segments] Add Segment List Success', + props<{ listResponse: FeatureFlagSegmentListDetails }>() +); + +export const actionAddSegmentListFailure = createAction('[Segments] Add Segment List Failure', props<{ error: any }>()); + +export const actionUpdateSegmentList = createAction( + '[Segments] Update Segment List', + props<{ list: EditPrivateSegmentListRequest }>() +); + +export const actionUpdateSegmentListSuccess = createAction( + '[Segments] Update Segment List Success', + props<{ listResponse: FeatureFlagSegmentListDetails }>() +); + +export const actionUpdateSegmentListFailure = createAction( + '[Segments] Update Segment List Failure', + props<{ error: any }>() +); + +export const actionDeleteSegmentList = createAction( + '[Segments] Delete Segment List', + props<{ segmentId: string; parentSegmentId: string }>() +); + +export const actionDeleteSegmentListSuccess = createAction( + '[Segments] Delete Segment List Success', + props<{ segmentId: string }>() +); + +export const actionDeleteSegmentListFailure = createAction( + '[Segments] Delete Segment List Failure', + props<{ error: any }>() +); diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts index 06c7c9dbd7..d4b8635b35 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts @@ -291,4 +291,40 @@ export class SegmentsEffects { element.click(); document.body.removeChild(element); } + + addSegmentList$ = createEffect(() => + this.actions$.pipe( + ofType(SegmentsActions.actionAddSegmentList), + switchMap((action) => { + return this.segmentsDataService.addSegmentList(action.list).pipe( + map((listResponse) => SegmentsActions.actionAddSegmentListSuccess({ listResponse })), + catchError((error) => of(SegmentsActions.actionAddSegmentListFailure({ error }))) + ); + }) + ) + ); + + updateSegmentList$ = createEffect(() => + this.actions$.pipe( + ofType(SegmentsActions.actionUpdateSegmentList), + switchMap((action) => { + return this.segmentsDataService.updateSegmentList(action.list).pipe( + map((listResponse) => SegmentsActions.actionUpdateSegmentListSuccess({ listResponse })), + catchError((error) => of(SegmentsActions.actionUpdateSegmentListFailure({ error }))) + ); + }) + ) + ); + + deleteSegmentList$ = createEffect(() => + this.actions$.pipe( + ofType(SegmentsActions.actionDeleteSegmentList), + switchMap(({ segmentId, parentSegmentId }) => { + return this.segmentsDataService.deleteSegmentList(segmentId, parentSegmentId).pipe( + map(() => SegmentsActions.actionDeleteSegmentListSuccess({ segmentId })), + catchError((error) => of(SegmentsActions.actionDeleteSegmentListFailure({ error }))) + ); + }) + ) + ); } diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts index 3a5eefc7f7..757ee3b162 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.model.ts @@ -287,12 +287,7 @@ export interface UpsertPrivateSegmentListParams { sourceList: ParticipantListTableRow; sourceAppContext: string; action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION; - flagId: string; -} - -export interface ImportListParams { - listType: FEATURE_FLAG_LIST_FILTER_MODE; - flagId: string; + id: string; } export enum LIST_OPTION_TYPE { @@ -340,7 +335,7 @@ export interface EditPrivateSegmentListDetails extends PrivateSegmentListRequest } export interface PrivateSegmentListRequest { - flagId: string; + id: string; enabled: boolean; listType: string; segment: AddPrivateSegmentListRequestDetails | EditPrivateSegmentListDetails; diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts index 4c2e4b2929..fc0a0bd82b 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts @@ -110,7 +110,104 @@ const reducer = createReducer( on(SegmentsActions.actionSetSortKey, (state, { sortKey }) => ({ ...state, sortKey })), on(SegmentsActions.actionSetSortingType, (state, { sortingType }) => ({ ...state, sortAs: sortingType })), on(SegmentsActions.actionDeleteSegmentSuccess, (state, { segment }) => adapter.removeOne(segment.id, state)), - on(SegmentsActions.actionSetIsLoadingSegments, (state, { isLoadingSegments }) => ({ ...state, isLoadingSegments })) + on(SegmentsActions.actionSetIsLoadingSegments, (state, { isLoadingSegments }) => ({ ...state, isLoadingSegments })), + + // Segment List Add Actions + on(SegmentsActions.actionAddSegmentList, (state) => ({ + ...state, + isLoadingSegments: true, + })), + on(SegmentsActions.actionAddSegmentListSuccess, (state, { listResponse }) => { + const { segment } = listResponse; + const parentSegmentId = segment?.id; // This should be the ID of the parent segment + const existingSegment = state.entities[parentSegmentId]; + + if (existingSegment) { + // Create updated subSegments array with the new list/segment + const updatedSubSegments = existingSegment.subSegments + ? [...existingSegment.subSegments, listResponse.segment] + : [listResponse.segment]; + + return adapter.updateOne( + { + id: parentSegmentId, + changes: { subSegments: updatedSubSegments }, + }, + { ...state, isLoadingSegments: false } + ); + } + + return { ...state, isLoadingSegments: false }; + }), + on(SegmentsActions.actionAddSegmentListFailure, (state) => ({ + ...state, + isLoadingSegments: false, + })), + + // Segment List Update Actions + on(SegmentsActions.actionUpdateSegmentList, (state) => ({ + ...state, + isLoadingSegments: true, + })), + on(SegmentsActions.actionUpdateSegmentListSuccess, (state, { listResponse }) => { + const { segment } = listResponse; + const parentSegmentId = segment?.id; // Parent segment ID + const existingSegment = state.entities[parentSegmentId]; + + if (existingSegment && existingSegment.subSegments) { + // Create updated subSegments array replacing the edited segment + const updatedSubSegments = existingSegment.subSegments.map((subSegment) => + subSegment.id === listResponse.segment.id ? listResponse.segment : subSegment + ); + + return adapter.updateOne( + { + id: parentSegmentId, + changes: { subSegments: updatedSubSegments }, + }, + { ...state, isLoadingSegments: false } + ); + } + + return { ...state, isLoadingSegments: false }; + }), + on(SegmentsActions.actionUpdateSegmentListFailure, (state) => ({ + ...state, + isLoadingSegments: false, + })), + + // Segment List Delete Actions + on(SegmentsActions.actionDeleteSegmentList, (state) => ({ + ...state, + isLoadingSegments: true, + })), + on(SegmentsActions.actionDeleteSegmentListSuccess, (state, { segmentId }) => { + // Find the parent segment that contains this subSegment + const parentSegmentId = Object.keys(state.entities).find((id) => + state.entities[id]?.subSegments?.some((subSegment) => subSegment.id === segmentId) + ); + + if (parentSegmentId) { + const parentSegment = state.entities[parentSegmentId]; + + // Filter out the deleted subSegment + const updatedSubSegments = parentSegment.subSegments.filter((subSegment) => subSegment.id !== segmentId); + + return adapter.updateOne( + { + id: parentSegmentId, + changes: { subSegments: updatedSubSegments }, + }, + { ...state, isLoadingSegments: false } + ); + } + + return { ...state, isLoadingSegments: false }; + }), + on(SegmentsActions.actionDeleteSegmentListFailure, (state) => ({ + ...state, + isLoadingSegments: false, + })) ); export function segmentsReducer(state: SegmentState | undefined, action: Action) { diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts index 995187ea52..754f76899d 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-exclusions-section-card/feature-flag-exclusions-section-card.component.ts @@ -6,7 +6,7 @@ import { } from '../../../../../../../shared-standalone-component-lib/components'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; -import { IMenuButtonItem, SEGMENT_TYPE } from 'upgrade_types'; +import { IMenuButtonItem } from 'upgrade_types'; import { FeatureFlagExclusionsTableComponent } from './feature-flag-exclusions-table/feature-flag-exclusions-table.component'; import { FeatureFlagsService } from '../../../../../../../core/feature-flags/feature-flags.service'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; @@ -17,11 +17,7 @@ import { ParticipantListRowActionEvent, ParticipantListTableRow, } from '../../../../../../../core/feature-flags/store/feature-flags.model'; -import { - EditPrivateSegmentListDetails, - EditPrivateSegmentListRequest, - Segment, -} from '../../../../../../../core/segments/store/segments.model'; +import { Segment } from '../../../../../../../core/segments/store/segments.model'; import { UserPermission } from '../../../../../../../core/auth/store/auth.models'; import { Observable } from 'rxjs'; import { AuthService } from '../../../../../../../core/auth/auth.service'; @@ -119,25 +115,6 @@ export class FeatureFlagExclusionsSectionCardComponent { this.dialogService.openEditExcludeListModal(rowData, rowData.segment.context, flagId); } - createEditPrivateSegmentListDetails(segment: Segment): EditPrivateSegmentListDetails { - const editPrivateSegmentListDetails: EditPrivateSegmentListDetails = { - id: segment.id, - name: segment.name, - description: segment.description, - context: segment.context, - type: SEGMENT_TYPE.PRIVATE, - userIds: segment.individualForSegment.map((individual) => individual.userId), - groups: segment.groupForSegment, - subSegmentIds: segment.subSegments.map((subSegment) => subSegment.id), - }; - - return editPrivateSegmentListDetails; - } - - sendUpdateFeatureFlagExclusionRequest(request: EditPrivateSegmentListRequest): void { - this.featureFlagService.updateFeatureFlagExclusionPrivateSegmentList(request); - } - onDeleteExcludeList(segment: Segment): void { this.dialogService .openDeleteExcludeListModal(segment.name) diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts index f2987dac00..385b8342b1 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-inclusions-section-card/feature-flag-inclusions-section-card.component.ts @@ -209,7 +209,7 @@ export class FeatureFlagInclusionsSectionCardComponent { const list: EditPrivateSegmentListDetails = this.createEditPrivateSegmentListDetails(segment); const listRequest: EditPrivateSegmentListRequest = { - flagId, + id: flagId, enabled, listType, segment: list, diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts index 78991aee75..39e21730ef 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts @@ -92,6 +92,7 @@ export class UpsertPrivateSegmentListModalComponent { [ UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_FLAG_INCLUDE_LIST, UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_FLAG_EXCLUDE_LIST, + UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_SEGMENT_LIST, ].includes(this.config.params.action) ) { // Slight delay before opening the type select dropdown for a smoother UX @@ -153,6 +154,7 @@ export class UpsertPrivateSegmentListModalComponent { ![ UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_FLAG_INCLUDE_LIST, UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_FLAG_EXCLUDE_LIST, + UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_SEGMENT_LIST, ].includes(this.config.params.action) ) { return; @@ -184,11 +186,11 @@ export class UpsertPrivateSegmentListModalComponent { determineValues(listType: string, segment: Segment): string[] { switch (listType) { case LIST_OPTION_TYPE.INDIVIDUAL: - return segment.individualForSegment.map((individual) => individual.userId); + return segment.individualForSegment.map((individual) => individual.userId) || []; case LIST_OPTION_TYPE.SEGMENT: return []; default: - return segment.groupForSegment.map((group) => group.groupId); + return segment.groupForSegment.map((group) => group.groupId) || []; } } @@ -295,7 +297,7 @@ export class UpsertPrivateSegmentListModalComponent { list = this.createRequestByListType(formData, listType); const listRequest: PrivateSegmentListRequest = { - flagId: this.config.params.flagId, + id: this.config.params.id, enabled: this.config.params.sourceList?.enabled || isExcludeList, // Maintain existing status for edits, default to false for new include lists, true for all exclude lists listType, segment: list, @@ -323,6 +325,12 @@ export class UpsertPrivateSegmentListModalComponent { case UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_FLAG_EXCLUDE_LIST: this.sendUpdateFeatureFlagExclusionRequest(editRequest); break; + case UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_SEGMENT_LIST: + this.sendAddSegmentListRequest(addListRequest); + break; + case UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_SEGMENT_LIST: + this.sendUpdateSegmentListRequest(editRequest); + break; } } @@ -375,6 +383,14 @@ export class UpsertPrivateSegmentListModalComponent { this.featureFlagService.updateFeatureFlagExclusionPrivateSegmentList(editListRequest); } + sendAddSegmentListRequest(addListRequest: AddPrivateSegmentListRequest): void { + this.segmentsService.addPrivateSegmentList(addListRequest); + } + + sendUpdateSegmentListRequest(editListRequest: EditPrivateSegmentListRequest): void { + this.segmentsService.updatePrivateSegmentList(editListRequest); + } + onDownloadRequested(values: string[]) { if (this.privateSegmentListForm.get('name').valid) { this.downloadValuesAsCSV(values, this.privateSegmentListForm.get('name').value); diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts index 8f8893e6b6..f9f2c6147a 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts @@ -9,11 +9,18 @@ import { TranslateModule } from '@ngx-translate/core'; import { IMenuButtonItem } from 'upgrade_types'; import { SegmentsService } from '../../../../../../../core/segments/segments.service'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; -import { Segment, SEGMENT_LIST_ACTIONS } from '../../../../../../../core/segments/store/segments.model'; +import { + ParticipantListTableRow, + Segment, + SEGMENT_LIST_ACTIONS, +} from '../../../../../../../core/segments/store/segments.model'; import { Observable } from 'rxjs'; import { AuthService } from '../../../../../../../core/auth/auth.service'; import { UserPermission } from '../../../../../../../core/auth/store/auth.models'; -import { ParticipantListRowActionEvent } from '../../../../../../../core/feature-flags/store/feature-flags.model'; +import { + PARTICIPANT_LIST_ROW_ACTION, + ParticipantListRowActionEvent, +} from '../../../../../../../core/feature-flags/store/feature-flags.model'; import { SegmentListsTableComponent } from './segment-lists-table/segment-lists-table.component'; @Component({ @@ -61,7 +68,7 @@ export class SegmentListsSectionCardComponent { } onAddListClick(appContext: string, segmentId: string) { - // this.dialogService.openAddListModal(appContext, segmentId); + this.dialogService.openAddListModal(appContext, segmentId); } onMenuButtonItemClick(event, segment: Segment) { @@ -82,7 +89,28 @@ export class SegmentListsSectionCardComponent { } onRowAction(event: ParticipantListRowActionEvent, segmentId: string): void { - // This will be implemented later when we implement the table component - console.log('Row action', event, segmentId); + switch (event.action) { + case PARTICIPANT_LIST_ROW_ACTION.EDIT: + this.onEditList(event.rowData, segmentId); + break; + case PARTICIPANT_LIST_ROW_ACTION.DELETE: + this.onDeleteList(event.rowData.segment); + break; + } + } + + onEditList(rowData: ParticipantListTableRow, flagId: string): void { + this.dialogService.openEditListModal(rowData, rowData.segment.context, flagId); + } + + onDeleteList(segment: Segment): void { + this.dialogService + .openDeleteExcludeListModal(segment.name) + .afterClosed() + .subscribe((confirmClicked) => { + if (confirmClicked) { + this.segmentsService.deletePrivateSegmentList(segment.id, this.data.id); + } + }); } } diff --git a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts index b8d8b3a788..5b55b199c6 100644 --- a/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts +++ b/frontend/projects/upgrade/src/app/shared/services/common-dialog.service.ts @@ -194,7 +194,7 @@ export class DialogService { sourceList: null, sourceAppContext: appContext, action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_FLAG_INCLUDE_LIST, - flagId: flagId, + id: flagId, }, }; return this.openUpsertPrivateSegmentListModal(commonModalConfig); @@ -213,7 +213,7 @@ export class DialogService { sourceList, sourceAppContext: appContext, action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_FLAG_INCLUDE_LIST, - flagId: flagId, + id: flagId, }, }; return this.openUpsertPrivateSegmentListModal(commonModalConfig); @@ -232,7 +232,7 @@ export class DialogService { sourceList: null, sourceAppContext: appContext, action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_FLAG_EXCLUDE_LIST, - flagId: flagId, + id: flagId, }, }; return this.openUpsertPrivateSegmentListModal(commonModalConfig); @@ -251,7 +251,45 @@ export class DialogService { sourceList, sourceAppContext: appContext, action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_FLAG_EXCLUDE_LIST, - flagId: flagId, + id: flagId, + }, + }; + return this.openUpsertPrivateSegmentListModal(commonModalConfig); + } + + openAddListModal(appContext: string, segmentId: string) { + const commonModalConfig: CommonModalConfig = { + title: 'Add List', + nameHint: 'segments.upsert-list-modal.name-hint.text', + valuesLabel: 'segments.upsert-list-modal.values-label.text', + valuesPlaceholder: 'segments.upsert-list-modal.values-placeholder.text', + primaryActionBtnLabel: 'Create', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + params: { + sourceList: null, + sourceAppContext: appContext, + action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.ADD_SEGMENT_LIST, + id: segmentId, + }, + }; + return this.openUpsertPrivateSegmentListModal(commonModalConfig); + } + + openEditListModal(sourceList: ParticipantListTableRow, appContext: string, segmentId: string) { + const commonModalConfig: CommonModalConfig = { + title: 'Edit List', + nameHint: 'segments.upsert-list-modal.name-hint.text', + valuesLabel: 'segments.upsert-list-modal.values-label.text', + valuesPlaceholder: 'segments.upsert-list-modal.values-placeholder.text', + primaryActionBtnLabel: 'Save', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + params: { + sourceList, + sourceAppContext: appContext, + action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION.EDIT_SEGMENT_LIST, + id: segmentId, }, }; return this.openUpsertPrivateSegmentListModal(commonModalConfig); diff --git a/frontend/projects/upgrade/src/assets/i18n/en.json b/frontend/projects/upgrade/src/assets/i18n/en.json index cdaddde67e..b40cdf20a1 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -509,6 +509,9 @@ "segments.import-segment-modal.compatibility-description.warning.text": "This JSON file can be imported, but it may contain outdated or missing properties/features. Please review the segment details post-import.", "segments.import-segment.error.message.text": "Invalid Segment JSON data", "segments.export-all-lists-design.confirmation-text.text": "Are you sure you want to export all lists (JSON)?", + "segments.upsert-list-modal.values-label.text": "Values", + "segments.upsert-list-modal.values-placeholder.text": "Values separated by commas", + "segments.upsert-list-modal.name-hint.text": "The name for this list.", "segments.details.lists.card.title.text": "Lists", "segments.details.lists.card.subtitle.text": "Define lists for this segment.", "segments.details.add-list.button.text": "Add List", diff --git a/frontend/projects/upgrade/src/environments/environment-types.ts b/frontend/projects/upgrade/src/environments/environment-types.ts index 595d0b8829..dac820664c 100644 --- a/frontend/projects/upgrade/src/environments/environment-types.ts +++ b/frontend/projects/upgrade/src/environments/environment-types.ts @@ -56,6 +56,7 @@ export interface APIEndpoints { importSegments: string; exportSegments: string; exportSegmentCSV: string; + addSegmentList: string; getGroupAssignmentStatus: string; stratification: string; } diff --git a/frontend/projects/upgrade/src/environments/environment.prod.ts b/frontend/projects/upgrade/src/environments/environment.prod.ts index 59c957f45c..2dd40b9d8e 100755 --- a/frontend/projects/upgrade/src/environments/environment.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.prod.ts @@ -74,6 +74,7 @@ export const environment: Environment = { importSegments: '/segments/import', exportSegments: '/segments/export/json', exportSegmentCSV: '/segments/export/csv', + addSegmentList: '/segments/list', getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', }, }; diff --git a/frontend/projects/upgrade/src/environments/environment.qa.ts b/frontend/projects/upgrade/src/environments/environment.qa.ts index ad29a41c27..f107c7483c 100644 --- a/frontend/projects/upgrade/src/environments/environment.qa.ts +++ b/frontend/projects/upgrade/src/environments/environment.qa.ts @@ -74,6 +74,7 @@ export const environment: Environment = { importSegments: '/segments/import', exportSegments: '/segments/export/json', exportSegmentCSV: '/segments/export/csv', + addSegmentList: '/segments/list', getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', }, }; diff --git a/frontend/projects/upgrade/src/environments/environment.staging.ts b/frontend/projects/upgrade/src/environments/environment.staging.ts index 499a6fb51e..5549faae13 100644 --- a/frontend/projects/upgrade/src/environments/environment.staging.ts +++ b/frontend/projects/upgrade/src/environments/environment.staging.ts @@ -74,6 +74,7 @@ export const environment: Environment = { importSegments: '/segments/import', exportSegments: '/segments/export/json', exportSegmentCSV: '/segments/export/csv', + addSegmentList: '/segments/list', getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', }, }; diff --git a/frontend/projects/upgrade/src/environments/environment.ts b/frontend/projects/upgrade/src/environments/environment.ts index ab0c596d00..5fad43409f 100755 --- a/frontend/projects/upgrade/src/environments/environment.ts +++ b/frontend/projects/upgrade/src/environments/environment.ts @@ -80,6 +80,7 @@ export const environment = { importSegments: '/segments/import', exportSegments: '/segments/export/json', exportSegmentCSV: '/segments/export/csv', + addSegmentList: '/segments/list', getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', }, }; From 52ea537c59c1623bee573995c36b476c7db93409 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 18:24:47 +0900 Subject: [PATCH 2/9] Add missing addSegmentList --- .../upgrade/src/environments/environment.beanstalk.prod.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts index 582883c78b..0551faef6e 100644 --- a/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts @@ -74,6 +74,7 @@ export const environment: Environment = { importSegments: '/segments/import', exportSegments: '/segments/export/json', exportSegmentCSV: '/segments/export/csv', + addSegmentList: '/segments/list', getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', }, }; From fa4eda4db162c10ed39b3b3e0e407ba4c57865bf Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 18:28:43 +0900 Subject: [PATCH 3/9] Update unreplaced flagId in FeatureFlagService --- backend/packages/Upgrade/src/api/services/FeatureFlagService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts index c3adbd6f8e..c6b8d6c018 100644 --- a/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts +++ b/backend/packages/Upgrade/src/api/services/FeatureFlagService.ts @@ -607,7 +607,7 @@ export class FeatureFlagService { filterType === 'inclusion' ? new FeatureFlagSegmentInclusion() : new FeatureFlagSegmentExclusion(); featureFlagSegmentInclusionOrExclusion.enabled = listInput.enabled; featureFlagSegmentInclusionOrExclusion.listType = listInput.listType; - featureFlagSegmentInclusionOrExclusion.featureFlag = featureFlags.find((flag) => flag.id === listInput.flagId); + featureFlagSegmentInclusionOrExclusion.featureFlag = featureFlags.find((flag) => flag.id === listInput.id); featureFlagSegmentInclusionOrExclusion.segment = newSegments.find( (segment) => segment.id === listInput.segment.id ); From 40fb955ff6963f38d53e09a85b6f4194a8f8d6e8 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 18:57:37 +0900 Subject: [PATCH 4/9] Close modal on segment list add or update --- .../src/app/core/segments/store/segments.effects.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts index d4b8635b35..53dc7d07ec 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.effects.ts @@ -297,7 +297,10 @@ export class SegmentsEffects { ofType(SegmentsActions.actionAddSegmentList), switchMap((action) => { return this.segmentsDataService.addSegmentList(action.list).pipe( - map((listResponse) => SegmentsActions.actionAddSegmentListSuccess({ listResponse })), + map((listResponse) => { + this.commonModalEventService.forceCloseModal(); + return SegmentsActions.actionAddSegmentListSuccess({ listResponse }); + }), catchError((error) => of(SegmentsActions.actionAddSegmentListFailure({ error }))) ); }) @@ -309,7 +312,10 @@ export class SegmentsEffects { ofType(SegmentsActions.actionUpdateSegmentList), switchMap((action) => { return this.segmentsDataService.updateSegmentList(action.list).pipe( - map((listResponse) => SegmentsActions.actionUpdateSegmentListSuccess({ listResponse })), + map((listResponse) => { + this.commonModalEventService.forceCloseModal(); + return SegmentsActions.actionUpdateSegmentListSuccess({ listResponse }); + }), catchError((error) => of(SegmentsActions.actionUpdateSegmentListFailure({ error }))) ); }) From 1e2a4012d5a2ea5a83f23bbb9b81c08f32886987 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 19:03:18 +0900 Subject: [PATCH 5/9] Update segments reducer --- .../upgrade/src/app/core/segments/store/segments.reducer.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts b/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts index fc0a0bd82b..19f6e40bc9 100644 --- a/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts +++ b/frontend/projects/upgrade/src/app/core/segments/store/segments.reducer.ts @@ -118,8 +118,7 @@ const reducer = createReducer( isLoadingSegments: true, })), on(SegmentsActions.actionAddSegmentListSuccess, (state, { listResponse }) => { - const { segment } = listResponse; - const parentSegmentId = segment?.id; // This should be the ID of the parent segment + const parentSegmentId = listResponse.parentSegmentId; const existingSegment = state.entities[parentSegmentId]; if (existingSegment) { @@ -150,8 +149,7 @@ const reducer = createReducer( isLoadingSegments: true, })), on(SegmentsActions.actionUpdateSegmentListSuccess, (state, { listResponse }) => { - const { segment } = listResponse; - const parentSegmentId = segment?.id; // Parent segment ID + const parentSegmentId = listResponse.parentSegmentId; const existingSegment = state.entities[parentSegmentId]; if (existingSegment && existingSegment.subSegments) { From 8a8c1ac8ec01225494c5d808ebf74f1597239aed Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 19:35:50 +0900 Subject: [PATCH 6/9] Update argument name for onEditList --- .../segment-lists-section-card.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts index f9f2c6147a..98416990ed 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/pages/segment-details-page/segment-details-page-content/segment-lists-section-card/segment-lists-section-card.component.ts @@ -99,8 +99,8 @@ export class SegmentListsSectionCardComponent { } } - onEditList(rowData: ParticipantListTableRow, flagId: string): void { - this.dialogService.openEditListModal(rowData, rowData.segment.context, flagId); + onEditList(rowData: ParticipantListTableRow, segmentId: string): void { + this.dialogService.openEditListModal(rowData, rowData.segment.context, segmentId); } onDeleteList(segment: Segment): void { From c49b0e0608517cd98c80d22156bb242dbb6693bb Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 19:58:36 +0900 Subject: [PATCH 7/9] Fix null Values on Edit List modal open by not fetching all segments on modal open --- .../upsert-private-segment-list-modal.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts index 39e21730ef..8e0e6b3e32 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts @@ -127,7 +127,6 @@ export class UpsertPrivateSegmentListModalComponent { fetchData() { this.experimentService.fetchContextMetaData(); - this.segmentsService.fetchAllSegments(); } initializeListeners() { From 35d0669f4158cc554136ade1a53c01b2f3f8f97b Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Wed, 9 Apr 2025 20:05:58 +0900 Subject: [PATCH 8/9] Revert redundant change --- .../upsert-private-segment-list-modal.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts index 8e0e6b3e32..9e179ff630 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component.ts @@ -185,11 +185,11 @@ export class UpsertPrivateSegmentListModalComponent { determineValues(listType: string, segment: Segment): string[] { switch (listType) { case LIST_OPTION_TYPE.INDIVIDUAL: - return segment.individualForSegment.map((individual) => individual.userId) || []; + return segment.individualForSegment.map((individual) => individual.userId); case LIST_OPTION_TYPE.SEGMENT: return []; default: - return segment.groupForSegment.map((group) => group.groupId) || []; + return segment.groupForSegment.map((group) => group.groupId); } } From e5b3596eaf5ef15626ee68d7374ea2b100f23cf7 Mon Sep 17 00:00:00 2001 From: Zack Lee Date: Thu, 10 Apr 2025 19:29:45 +0900 Subject: [PATCH 9/9] Add test cases for segment list CRUD operations --- .../segments/segments.data.service.spec.ts | 170 +++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/frontend/projects/upgrade/src/app/core/segments/segments.data.service.spec.ts b/frontend/projects/upgrade/src/app/core/segments/segments.data.service.spec.ts index 6d350e93cb..826c70b7b3 100644 --- a/frontend/projects/upgrade/src/app/core/segments/segments.data.service.spec.ts +++ b/frontend/projects/upgrade/src/app/core/segments/segments.data.service.spec.ts @@ -2,9 +2,16 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { of } from 'rxjs'; import { environment } from '../../../environments/environment'; import { SegmentsDataService } from './segments.data.service'; -import { Segment, SegmentFile, SegmentInput } from './store/segments.model'; +import { + AddPrivateSegmentListRequest, + EditPrivateSegmentListRequest, + Segment, + SegmentFile, + SegmentInput, +} from './store/segments.model'; import { SEGMENT_STATUS, SEGMENT_TYPE } from 'upgrade_types'; import { Environment } from '../../../environments/environment-types'; +import { FeatureFlagSegmentListDetails } from '../feature-flags/store/feature-flags.model'; class MockHTTPClient { get = jest.fn().mockReturnValue(of()); @@ -20,6 +27,9 @@ describe('SegmentDataService', () => { let mockSegmentId: string; let mockEnvironment: Environment; let mockSegmentFile: SegmentFile; + let mockAddPrivateSegmentListRequest: AddPrivateSegmentListRequest; + let mockEditPrivateSegmentListRequest: EditPrivateSegmentListRequest; + let mockParentSegmentId: string; beforeEach(() => { mockHttpClient = new MockHTTPClient(); @@ -27,6 +37,7 @@ describe('SegmentDataService', () => { service = new SegmentsDataService(mockHttpClient as HttpClient, mockEnvironment); mockSegmentId = 'segmentId1'; + mockParentSegmentId = 'parentSegmentId1'; mockSegmentInput = { createdAt: 'time', updatedAt: 'time', @@ -56,6 +67,37 @@ describe('SegmentDataService', () => { status: SEGMENT_STATUS.UNUSED, }; mockSegmentFile = { fileName: 'test', fileContent: JSON.stringify(mockSegment) }; + + mockAddPrivateSegmentListRequest = { + id: mockParentSegmentId, + enabled: true, + listType: 'Individual', + segment: { + name: 'Test Segment List', + description: 'Test description', + context: 'test', + type: SEGMENT_TYPE.PRIVATE, + userIds: ['user1', 'user2'], + groups: [], + subSegmentIds: [], + }, + }; + + mockEditPrivateSegmentListRequest = { + id: mockParentSegmentId, + enabled: true, + listType: 'Individual', + segment: { + id: mockSegmentId, + name: 'Updated Segment List', + description: 'Updated description', + context: 'test', + type: SEGMENT_TYPE.PRIVATE, + userIds: ['user1', 'user2', 'user3'], + groups: [], + subSegmentIds: [], + }, + }; }); describe('#fetchSegmentsPaginated', () => { @@ -125,4 +167,130 @@ describe('SegmentDataService', () => { expect(mockHttpClient.post).toHaveBeenCalledWith(mockUrl, [mockSegmentFile]); }); }); + + describe('#addSegmentList', () => { + it('should post the addSegmentList http observable with transformed request', () => { + const expectedUrl = mockEnvironment.api.addSegmentList; + const mockSegment: Segment = { + id: 'newSegmentId', + name: 'New Segment', + description: 'Test description', + context: 'test', + tags: [], + type: SEGMENT_TYPE.PRIVATE, + status: SEGMENT_STATUS.UNUSED, + createdAt: 'test', + updatedAt: 'test', + versionNumber: 0, + individualForSegment: [], + groupForSegment: [], + subSegments: [], + }; + + const expectedOutput: FeatureFlagSegmentListDetails = { + segment: mockSegment, + featureFlag: null, + enabled: mockAddPrivateSegmentListRequest.enabled, + listType: mockAddPrivateSegmentListRequest.listType, + parentSegmentId: mockAddPrivateSegmentListRequest.id, + }; + + // Setup mock response + mockHttpClient.post.mockReturnValue(of(mockSegment)); + + // Expected transformed request + const expectedTransformedRequest = { + parentSegmentId: mockAddPrivateSegmentListRequest.id, + name: mockAddPrivateSegmentListRequest.segment.name, + description: mockAddPrivateSegmentListRequest.segment.description, + context: mockAddPrivateSegmentListRequest.segment.context, + type: mockAddPrivateSegmentListRequest.segment.type, + userIds: mockAddPrivateSegmentListRequest.segment.userIds, + groups: mockAddPrivateSegmentListRequest.segment.groups, + subSegmentIds: mockAddPrivateSegmentListRequest.segment.subSegmentIds, + listType: mockAddPrivateSegmentListRequest.listType, + }; + + // Call the method and subscribe to result + let result: FeatureFlagSegmentListDetails; + service.addSegmentList(mockAddPrivateSegmentListRequest).subscribe((res) => { + result = res; + }); + + // Verify HTTP call was made correctly + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, expectedTransformedRequest); + + // Verify expected output transformation + expect(result).toEqual(expectedOutput); + }); + }); + + describe('#updateSegmentList', () => { + it('should post the updateSegmentList http observable with transformed request', () => { + const expectedUrl = mockEnvironment.api.segments; + const mockSegment: Segment = { + id: mockSegmentId, + name: 'Updated Segment', + description: 'Updated description', + context: 'test', + tags: [], + type: SEGMENT_TYPE.PRIVATE, + status: SEGMENT_STATUS.UNUSED, + createdAt: 'test', + updatedAt: 'test', + versionNumber: 0, + individualForSegment: [], + groupForSegment: [], + subSegments: [], + }; + + const expectedOutput: FeatureFlagSegmentListDetails = { + segment: mockSegment, + featureFlag: null, + enabled: mockEditPrivateSegmentListRequest.enabled, + listType: mockEditPrivateSegmentListRequest.listType, + parentSegmentId: mockEditPrivateSegmentListRequest.id, + }; + + // Setup mock response + mockHttpClient.post.mockReturnValue(of(mockSegment)); + + // Expected transformed request + const expectedTransformedRequest = { + id: mockEditPrivateSegmentListRequest.segment.id, + name: mockEditPrivateSegmentListRequest.segment.name, + description: mockEditPrivateSegmentListRequest.segment.description, + context: mockEditPrivateSegmentListRequest.segment.context, + type: mockEditPrivateSegmentListRequest.segment.type, + userIds: mockEditPrivateSegmentListRequest.segment.userIds, + groups: mockEditPrivateSegmentListRequest.segment.groups, + subSegmentIds: mockEditPrivateSegmentListRequest.segment.subSegmentIds, + listType: mockEditPrivateSegmentListRequest.listType, + }; + + // Call the method and subscribe to result + let result: FeatureFlagSegmentListDetails; + service.updateSegmentList(mockEditPrivateSegmentListRequest).subscribe(res => { + result = res; + }); + + // Verify HTTP call was made correctly + expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, expectedTransformedRequest); + + // Verify expected output transformation + expect(result).toEqual(expectedOutput); + }); + }); + + describe('#deleteSegmentList', () => { + it('should delete the segment list with the correct parameters', () => { + const segmentId = mockSegmentId; + const parentSegmentId = mockParentSegmentId; + const expectedUrl = `${mockEnvironment.api.addSegmentList}/${segmentId}`; + + service.deleteSegmentList(segmentId, parentSegmentId); + + expect(mockHttpClient.delete).toHaveBeenCalledWith(expectedUrl, { body: { parentSegmentId } }); + }); + }); });