diff --git a/frontend/projects/upgrade/src/app/core/auth/store/auth.selectors.ts b/frontend/projects/upgrade/src/app/core/auth/store/auth.selectors.ts index faacfdf121..063494942e 100755 --- a/frontend/projects/upgrade/src/app/core/auth/store/auth.selectors.ts +++ b/frontend/projects/upgrade/src/app/core/auth/store/auth.selectors.ts @@ -11,6 +11,11 @@ export const selectIsAuthenticating = createSelector(selectAuthState, (state: Au export const selectCurrentUser = createSelector(selectAuthState, (state: AuthState) => state.user); +export const selectCurrentUserEmail = createSelector( + selectAuthState, + (state) => state.user?.email || '' +); + export const selectRedirectUrl = createSelector(selectAuthState, (state: AuthState) => state.redirectUrl ? state.redirectUrl : null ); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts index 3b5e726ab2..1903e471b7 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.data.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@angular/core'; import { ENV, Environment } from '../../../environments/environment-types'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { AddFeatureFlagRequest, FeatureFlag, @@ -10,11 +10,12 @@ import { UpdateFeatureFlagRequest, UpdateFeatureFlagStatusRequest, } from './store/feature-flags.model'; -import { Observable } from 'rxjs'; +import { Observable, delay, of } from 'rxjs'; import { AddPrivateSegmentListRequest, EditPrivateSegmentListRequest } from '../segments/store/segments.model'; @Injectable() export class FeatureFlagsDataService { + mockFeatureFlags: FeatureFlag[] = []; constructor(private http: HttpClient, @Inject(ENV) private environment: Environment) {} fetchFeatureFlagsPaginated(params: FeatureFlagsPaginationParams): Observable { @@ -42,6 +43,22 @@ export class FeatureFlagsDataService { return this.http.put(url, flag); } + emailFeatureFlagData(flagId: string, email: string){ + let featureFlagInfoParams = new HttpParams(); + featureFlagInfoParams = featureFlagInfoParams.append('experimentId', flagId); + featureFlagInfoParams = featureFlagInfoParams.append('email', email); + + const url = this.environment.api.emailFlagData; + // return this.http.post(url, { params: featureFlagInfoParams }); + + // mock + return of(true).pipe(delay(2000)); + } + + exportFeatureFlagsDesign(flagId: string) { + return this.fetchFeatureFlagById(flagId); + } + deleteFeatureFlag(id: string) { const url = `${this.environment.api.featureFlag}/${id}`; return this.http.delete(url); diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts index 365aaa941b..e7279f545f 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/feature-flags.service.ts @@ -22,6 +22,7 @@ import { selectSortKey, selectSortAs, selectAppContexts, + selectFeatureFlagIds, } from './store/feature-flags.selectors'; import * as FeatureFlagsActions from './store/feature-flags.actions'; import { actionFetchContextMetaData } from '../experiments/store/experiments.actions'; @@ -33,15 +34,20 @@ import { } from './store/feature-flags.model'; import { filter, map, pairwise } from 'rxjs'; import isEqual from 'lodash.isequal'; +import { selectCurrentUserEmail } from '../auth/store/auth.selectors'; import { AddPrivateSegmentListRequest, EditPrivateSegmentListRequest } from '../segments/store/segments.model'; @Injectable() export class FeatureFlagsService { constructor(private store$: Store) {} + currentUserEmailAddress$ = this.store$.pipe(select(selectCurrentUserEmail)); + allFeatureFlagsIds$ = this.store$.pipe(select(selectFeatureFlagIds)); isInitialFeatureFlagsLoading$ = this.store$.pipe(select(selectHasInitialFeatureFlagsDataLoaded)); isLoadingFeatureFlags$ = this.store$.pipe(select(selectIsLoadingFeatureFlags)); isLoadingSelectedFeatureFlag$ = this.store$.pipe(select(selectIsLoadingSelectedFeatureFlag)); + isLoadingUpsertFeatureFlag$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag)); + IsLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete)); isLoadingUpdateFeatureFlagStatus$ = this.store$.pipe(select(selectIsLoadingUpdateFeatureFlagStatus)); isLoadingUpsertPrivateSegmentList$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag)); allFeatureFlags$ = this.store$.pipe(select(selectAllFeatureFlagsSortedByDate)); @@ -51,8 +57,6 @@ export class FeatureFlagsService { searchKey$ = this.store$.pipe(select(selectSearchKey)); sortKey$ = this.store$.pipe(select(selectSortKey)); sortAs$ = this.store$.pipe(select(selectSortAs)); - isLoadingUpsertFeatureFlag$ = this.store$.pipe(select(selectIsLoadingUpsertFeatureFlag)); - IsLoadingFeatureFlagDelete$ = this.store$.pipe(select(selectIsLoadingFeatureFlagDelete)); hasFeatureFlagsCountChanged$ = this.allFeatureFlags$.pipe( pairwise(), @@ -124,6 +128,14 @@ export class FeatureFlagsService { this.store$.dispatch(FeatureFlagsActions.actionDeleteFeatureFlag({ flagId })); } + emailFeatureFlagData(featureFlagId: string) { + this.store$.dispatch(FeatureFlagsActions.actionEmailFeatureFlagData({ featureFlagId })); + } + + exportFeatureFlagsData(featureFlagId: string) { + this.store$.dispatch(FeatureFlagsActions.actionExportFeatureFlagDesign({ featureFlagId })); + } + setSearchKey(searchKey: FLAG_SEARCH_KEY) { this.store$.dispatch(FeatureFlagsActions.actionSetSearchKey({ searchKey })); } diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts index c69b5d02f2..8f3762c934 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.actions.ts @@ -65,6 +65,24 @@ export const actionUpdateFeatureFlagSuccess = createAction( export const actionUpdateFeatureFlagFailure = createAction('[Feature Flags] Update Feature Flag Failure'); +export const actionEmailFeatureFlagData = createAction( + '[Feature Flags] Email Feature Flag Data', + props<{ featureFlagId: string }>() +); + +export const actionEmailFeatureFlagDataSuccess = createAction('[Feature Flags] Email Feature Flag Data Success'); + +export const actionEmailFeatureFlagDataFailure = createAction('[Feature Flags] Email Feature Flag Data Failure'); + +export const actionExportFeatureFlagDesign = createAction( + '[Feature Flags] Export Feature Flag Design', + props<{ featureFlagId: string }>() +); + +export const actionExportFeatureFlagDesignSuccess = createAction('[Feature Flags] Export Feature Flag Design Success'); + +export const actionExportFeatureFlagDesignFailure = createAction('[Feature Flags] Export Feature Flag Design Failure'); + export const actionSetIsLoadingFeatureFlags = createAction( '[Feature Flags] Set Is Loading Flags', props<{ isLoadingFeatureFlags: boolean }>() diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts index fb8f6249c9..54a7bde32d 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.effects.ts @@ -6,7 +6,7 @@ import { catchError, switchMap, map, filter, withLatestFrom, tap, first } from ' import { FeatureFlag, FeatureFlagsPaginationParams, NUMBER_OF_FLAGS } from './feature-flags.model'; import { Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; -import { AppState } from '../../core.module'; +import { AppState, NotificationService } from '../../core.module'; import { selectTotalFlags, selectSearchKey, @@ -15,7 +15,10 @@ import { selectSortAs, selectSearchString, selectIsAllFlagsFetched, + selectSelectedFeatureFlag, } from './feature-flags.selectors'; +import { selectCurrentUser } from '../../auth/store/auth.selectors'; +import { CommonExportHelpersService } from '../../../shared/services/common-export-helpers.service'; import { of } from 'rxjs'; @Injectable() @@ -24,7 +27,9 @@ export class FeatureFlagsEffects { private store$: Store, private actions$: Actions, private featureFlagsDataService: FeatureFlagsDataService, - private router: Router + private router: Router, + private notificationService: NotificationService, + private commonExportHelpersService: CommonExportHelpersService, ) {} fetchFeatureFlags$ = createEffect(() => @@ -149,6 +154,25 @@ export class FeatureFlagsEffects { ) ); + + upsertFeatureFlagInclusionList$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionAddFeatureFlagInclusionList), + map((action) => action.list), + withLatestFrom(this.store$.pipe(select(selectSelectedFeatureFlag))), + switchMap(([list, flag]) => { + const request = { + flagId: flag.id, + ...list, + }; + return this.featureFlagsDataService.addInclusionList(request).pipe( + map((listResponse) => FeatureFlagsActions.actionUpdateFeatureFlagInclusionListSuccess({ listResponse })), + catchError((error) => of(FeatureFlagsActions.actionUpdateFeatureFlagInclusionListFailure({ error }))) + ); + }) + ) + ); + addFeatureFlagInclusionList$ = createEffect(() => this.actions$.pipe( ofType(FeatureFlagsActions.actionAddFeatureFlagInclusionList), @@ -231,5 +255,45 @@ export class FeatureFlagsEffects { ) ); + emailFeatureFlagData$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionEmailFeatureFlagData), + map((action) => ({ featureFlagId: action.featureFlagId })), + withLatestFrom(this.store$.pipe(select(selectCurrentUser))), + filter(([{ featureFlagId }, { email }]) => !!featureFlagId && !!email), + switchMap(([{ featureFlagId }, { email }]) => + this.featureFlagsDataService.emailFeatureFlagData(featureFlagId, email).pipe( + map(() => { + this.notificationService.showSuccess(`Email will be sent to ${email}`); + return FeatureFlagsActions.actionEmailFeatureFlagDataSuccess(); + }), + catchError(() => [FeatureFlagsActions.actionEmailFeatureFlagDataFailure()]) + ) + ) + ) + ); + exportFeatureFlagsDesign$ = createEffect(() => + this.actions$.pipe( + ofType(FeatureFlagsActions.actionExportFeatureFlagDesign), + map((action) => ({ featureFlagId: action.featureFlagId })), + filter(({ featureFlagId }) => !!featureFlagId), + switchMap(({ featureFlagId }) => + this.featureFlagsDataService.exportFeatureFlagsDesign(featureFlagId).pipe( + map((data) => { + if (data) { + this.commonExportHelpersService.convertDataToDownload([data], 'FeatureFlags'); + this.notificationService.showSuccess('Feature Flag Design JSON downloaded!'); + } + return FeatureFlagsActions.actionExportFeatureFlagDesignSuccess(); + }), + catchError((error) => { + this.notificationService.showError('Failed to export Feature Flag Design'); + return of(FeatureFlagsActions.actionExportFeatureFlagDesignFailure()); + }) + ) + ) + ) + ); + private getSearchString$ = () => this.store$.pipe(select(selectSearchString)).pipe(first()); } 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 f33c729141..2a8cf0a298 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 @@ -125,6 +125,15 @@ export interface FeatureFlagsPaginationParams { sortParams?: IFeatureFlagsSortParams; } +export enum FEATURE_FLAG_DETAILS_PAGE_ACTIONS { + EDIT = 'Edit Feature Flag', + DUPLICATE = 'Duplicate Feature Flag', + ARCHIVE = 'Archive Feature Flag', + DELETE = 'Delete Feature Flag', + EXPORT_DESIGN = 'Export Feature Flag Design', + EMAIL_DATA = 'Email Feature Flag Data' +} + export enum FEATURE_FLAG_PARTICIPANT_LIST_KEY { INCLUDE = 'featureFlagSegmentInclusion', EXCLUDE = 'featureFlagSegmentExclusion', diff --git a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts index f36a2a67e0..eb8a9b8552 100644 --- a/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts +++ b/frontend/projects/upgrade/src/app/core/feature-flags/store/feature-flags.selectors.ts @@ -1,13 +1,15 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import { FLAG_SEARCH_KEY, FeatureFlag, FeatureFlagState, ParticipantListTableRow } from './feature-flags.model'; import { selectRouterState } from '../../core.state'; -import { selectAll } from './feature-flags.reducer'; import { selectContextMetaData } from '../../experiments/store/experiments.selectors'; +import { selectAll, selectIds } from './feature-flags.reducer'; export const selectFeatureFlagsState = createFeatureSelector('featureFlags'); export const selectAllFeatureFlags = createSelector(selectFeatureFlagsState, selectAll); +export const selectFeatureFlagIds = createSelector(selectFeatureFlagsState, selectIds); + export const selectAllFeatureFlagsSortedByDate = createSelector(selectAllFeatureFlags, (featureFlags) => { if (!featureFlags) { return []; diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-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-overview-details-section-card/feature-flag-overview-details-section-card.component.ts index 6ed34c4756..676e3806aa 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-details-page/feature-flag-details-page-content/feature-flag-overview-details-section-card/feature-flag-overview-details-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-overview-details-section-card/feature-flag-overview-details-section-card.component.ts @@ -1,21 +1,21 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { CommonSectionCardActionButtonsComponent, CommonSectionCardComponent, CommonSectionCardTitleHeaderComponent, } from '../../../../../../../shared-standalone-component-lib/components'; import { FeatureFlagOverviewDetailsFooterComponent } from './feature-flag-overview-details-footer/feature-flag-overview-details-footer.component'; - import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { FeatureFlagsService } from '../../../../../../../core/feature-flags/feature-flags.service'; import { FEATURE_FLAG_STATUS, IMenuButtonItem } from 'upgrade_types'; import { CommonModule } from '@angular/common'; import { CommonSectionCardOverviewDetailsComponent } from '../../../../../../../shared-standalone-component-lib/components/common-section-card-overview-details/common-section-card-overview-details.component'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; -import { FeatureFlag } from '../../../../../../../core/feature-flags/store/feature-flags.model'; +import { FEATURE_FLAG_DETAILS_PAGE_ACTIONS, FeatureFlag } from '../../../../../../../core/feature-flags/store/feature-flags.model'; +import { Subscription } from 'rxjs'; +import { AuthService } from '../../../../../../../core/auth/auth.service'; import { MatDialogRef } from '@angular/material/dialog'; import { CommonSimpleConfirmationModalComponent } from '../../../../../../../shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component'; -import { Subscription } from 'rxjs'; @Component({ selector: 'app-feature-flag-overview-details-section-card', standalone: true, @@ -31,22 +31,28 @@ import { Subscription } from 'rxjs'; styleUrl: './feature-flag-overview-details-section-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FeatureFlagOverviewDetailsSectionCardComponent { +export class FeatureFlagOverviewDetailsSectionCardComponent implements OnInit, OnDestroy { isSectionCardExpanded = true; @Output() sectionCardExpandChange = new EventEmitter(); + emailId = ''; featureFlag$ = this.featureFlagService.selectedFeatureFlag$; flagOverviewDetails$ = this.featureFlagService.selectedFlagOverviewDetails; subscriptions = new Subscription(); - + confirmStatusChangeDialogRef: MatDialogRef; menuButtonItems: IMenuButtonItem[] = [ - { name: 'Edit', disabled: false }, - { name: 'Delete', disabled: false }, - { name: 'Duplicate', disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EDIT, disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.DUPLICATE, disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EXPORT_DESIGN, disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EMAIL_DATA, disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.ARCHIVE, disabled: false }, + { name: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.DELETE, disabled: false }, ]; - confirmStatusChangeDialogRef: MatDialogRef; + constructor(private dialogService: DialogService, private featureFlagService: FeatureFlagsService, private authService: AuthService,) {} - constructor(private dialogService: DialogService, private featureFlagService: FeatureFlagsService) {} + ngOnInit(): void { + this.subscriptions.add(this.featureFlagService.currentUserEmailAddress$.subscribe((id) => this.emailId = id)); + } get FEATURE_FLAG_STATUS() { return FEATURE_FLAG_STATUS; @@ -98,22 +104,60 @@ export class FeatureFlagOverviewDetailsSectionCardComponent { return this.dialogService.openDisableFeatureFlagConfirmModel(flagName); } - onMenuButtonItemClick(event: 'Edit' | 'Delete' | 'Duplicate', flag: FeatureFlag) { - if (event === 'Delete') { - this.dialogService.openDeleteFeatureFlagModal(); - } else if (event === 'Edit') { - this.dialogService.openEditFeatureFlagModal(flag); - } else if (event === 'Duplicate') { - this.dialogService.openDuplicateFeatureFlagModal(flag); + onMenuButtonItemClick(event: FEATURE_FLAG_DETAILS_PAGE_ACTIONS, flag: FeatureFlag) { + switch (event) { + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.DELETE: + this.dialogService.openDeleteFeatureFlagModal(); + break; + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EDIT: + this.dialogService.openEditFeatureFlagModal(flag); + break; + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.DUPLICATE: + this.dialogService.openDuplicateFeatureFlagModal(flag); + break; + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.ARCHIVE: + console.log('Archive feature flag'); + break; + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EXPORT_DESIGN: + this.openConfirmExportDesignModal(flag.id); + break; + case FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EMAIL_DATA: + this.openConfirmEmailDataModal(flag.id); + break; + default: + console.log('Unknown action'); } } + openConfirmExportDesignModal(id: string) { + const confirmMessage = 'feature-flags.export-feature-flag-design.confirmation-text.text'; + this.dialogService.openExportFeatureFlagDesignModal(confirmMessage) + .afterClosed() + .subscribe((isExportClicked: boolean) => { + if (isExportClicked) { + this.featureFlagService.exportFeatureFlagsData(id); + } + }); + } + + openConfirmEmailDataModal(id: string) { + const confirmMessage = 'feature-flags.export-feature-flags-data.confirmation-text.text'; + const emailConfirmationMessage = "The feature flag will be sent to '" + this.emailId + "'." ; + this.dialogService.openEmailFeatureFlagDataModal(confirmMessage, emailConfirmationMessage) + .afterClosed() + .subscribe((isEmailClicked: boolean) => { + if (isEmailClicked) { + this.featureFlagService.emailFeatureFlagData(id); + } + }); + } + onSectionCardExpandChange(isSectionCardExpanded: boolean) { this.isSectionCardExpanded = isSectionCardExpanded; this.sectionCardExpandChange.emit(this.isSectionCardExpanded); } - ngOnDestroy() { + ngOnDestroy(): void { this.subscriptions.unsubscribe(); } } diff --git a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts index ae07c422d8..b08892a716 100644 --- a/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts +++ b/frontend/projects/upgrade/src/app/features/dashboard/feature-flags/pages/feature-flag-root-page/feature-flag-root-page-content/feature-flag-root-section-card/feature-flag-root-section-card.component.ts @@ -13,7 +13,7 @@ import { FLAG_SEARCH_KEY, IMenuButtonItem } from 'upgrade_types'; import { RouterModule } from '@angular/router'; import { MatTableDataSource } from '@angular/material/table'; import { DialogService } from '../../../../../../../shared/services/common-dialog.service'; -import { Observable, map } from 'rxjs'; +import { Observable, Subscription, map } from 'rxjs'; import { FeatureFlag } from '../../../../../../../core/feature-flags/store/feature-flags.model'; import { CommonSearchWidgetSearchParams } from '../../../../../../../shared-standalone-component-lib/components/common-section-card-search-header/common-section-card-search-header.component'; import { @@ -100,8 +100,6 @@ export class FeatureFlagRootSectionCardComponent { onMenuButtonItemClick(menuButtonItemName: string) { if (menuButtonItemName === 'Import Feature Flag') { this.dialogService.openImportFeatureFlagModal(); - } else if (menuButtonItemName === 'Export All Feature Flags') { - console.log('onMenuButtonItemClick:', menuButtonItemName); } } diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal-config.ts b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal-config.ts index d48cb898ae..1c5f77da00 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal-config.ts +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-modal/common-modal-config.ts @@ -5,3 +5,4 @@ export const ENDPOINTS_TO_INTERCEPT_FOR_MODAL_CLOSE = [ environment.api.addFlagInclusionList, environment.api.addFlagExclusionList, ]; + diff --git a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component.scss b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component.scss index 50071bb105..685982aa20 100644 --- a/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component.scss +++ b/frontend/projects/upgrade/src/app/shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component.scss @@ -1,25 +1,25 @@ .confirm-modal-content { - padding: 20px 0px; + padding: 20px 0px; - .subMessage { - margin-top: 10px; + .subMessage { + margin-top: 10px; + } + + display: flex; + flex-direction: column; + row-gap: 16px; + padding: 40px 32px; + + &-text { + color: var(--dark-grey); + margin-bottom: 0; + + &.info { + color: var(--blue); } - display: flex; - flex-direction: column; - row-gap: 16px; - padding: 40px 32px; - - &-text { - color: var(--dark-grey); - margin-bottom: 0; - - &.info { - color: var(--blue); - } - - &.warn { - color: var(--red-2); - } + &.warn { + color: var(--red-2); } + } } \ No newline at end of file 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 60233969ab..1d3226823f 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 @@ -2,21 +2,21 @@ import { Injectable } from '@angular/core'; import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; import { MatConfirmDialogComponent } from '../components/mat-confirm-dialog/mat-confirm-dialog.component'; import { DeleteFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/delete-feature-flag-modal/delete-feature-flag-modal.component'; - import { ImportFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/import-feature-flag-modal/import-feature-flag-modal.component'; +import { UpsertFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/upsert-feature-flag-modal/upsert-feature-flag-modal.component'; import { UpsertPrivateSegmentListModalComponent } from '../../features/dashboard/segments/modals/upsert-private-segment-list-modal/upsert-private-segment-list-modal.component'; import { UPSERT_PRIVATE_SEGMENT_LIST_ACTION, UpsertPrivateSegmentListParams, } from '../../core/segments/store/segments.model'; import { + FEATURE_FLAG_DETAILS_PAGE_ACTIONS, FeatureFlag, ParticipantListTableRow, UPSERT_FEATURE_FLAG_ACTION, UPSERT_FEATURE_FLAG_LIST_ACTION, UpsertFeatureFlagParams, } from '../../core/feature-flags/store/feature-flags.model'; -import { UpsertFeatureFlagModalComponent } from '../../features/dashboard/feature-flags/modals/upsert-feature-flag-modal/upsert-feature-flag-modal.component'; import { CommonSimpleConfirmationModalComponent } from '../../shared-standalone-component-lib/components/common-simple-confirmation-modal/common-simple-confirmation-modal.component'; import { CommonModalConfig, @@ -223,6 +223,34 @@ export class DialogService { return this.dialog.open(DeleteFeatureFlagModalComponent, config); } + openExportFeatureFlagDesignModal(warning: string): MatDialogRef { + const commonModalConfig: CommonModalConfig = { + title: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EXPORT_DESIGN, + primaryActionBtnLabel: 'Export', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + params: { + message: warning, + }, + }; + return this.openSimpleCommonConfirmationModal(commonModalConfig); + } + + openEmailFeatureFlagDataModal(warning: string, subtext: string): MatDialogRef { + const commonModalConfig: CommonModalConfig = { + title: FEATURE_FLAG_DETAILS_PAGE_ACTIONS.EMAIL_DATA, + primaryActionBtnLabel: 'Email', + primaryActionBtnColor: 'primary', + cancelBtnLabel: 'Cancel', + params: { + message: warning, + subMessage: subtext, + subMessageClass: 'info', + }, + }; + return this.openSimpleCommonConfirmationModal(commonModalConfig); + } + openImportFeatureFlagModal() { const commonModalConfig: CommonModalConfig = { title: 'Import Feature Flag', @@ -239,9 +267,7 @@ export class DialogService { return this.dialog.open(ImportFeatureFlagModalComponent, config); } - openSimpleCommonConfirmationModal( - commonModalConfig: CommonModalConfig - ): MatDialogRef { + openSimpleCommonConfirmationModal(commonModalConfig: CommonModalConfig): MatDialogRef { const config: MatDialogConfig = { data: commonModalConfig, width: '656px', diff --git a/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts b/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts new file mode 100644 index 0000000000..03e7c779ac --- /dev/null +++ b/frontend/projects/upgrade/src/app/shared/services/common-export-helpers.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import JSZip from 'jszip'; + +@Injectable({ + providedIn: 'root', +}) +export class CommonExportHelpersService { + download(filename, text, isZip: boolean) { + const element = document.createElement('a'); + isZip + ? element.setAttribute('href', 'data:application/zip;base64,' + text) + : element.setAttribute('href', 'data:text/plain;charset=utf-8,' + JSON.stringify(text)); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + + convertDataToDownload(data: any[], zipFileName: string) { + if (data.length > 1) { + const zip = new JSZip(); + data.forEach((element, index) => { + zip.file(element.name + ' (File ' + (index + 1) + ').json', JSON.stringify(element)); + }); + zip.generateAsync({ type: 'base64' }).then((content) => { + this.download(zipFileName + '.zip', content, true); + }); + } else { + this.download(data[0].name + '.json', data[0], false); + } + } +} \ No newline at end of file diff --git a/frontend/projects/upgrade/src/assets/i18n/en.json b/frontend/projects/upgrade/src/assets/i18n/en.json index 9b2f9950c9..cefd074ba8 100644 --- a/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/frontend/projects/upgrade/src/assets/i18n/en.json @@ -389,8 +389,13 @@ "feature-flags.enable.text": "Enable", "feature-flags.add-feature-flag.text": "Add Feature Flag", "feature-flags.import-feature-flag.text": "Import Feature Flag", - "feature-flags.import-feature-flag.message.text": "The Feature Flag JSON file should include the required properties for it to be imported.", - "feature-flags.export-all-feature-flags.text": "Export All Feature Flags", + "feature-flags.import-feature-flag.message.text": "The Feature Flag JSON file should include the required properties for it to be imported", + "feature-flags.export-all-feature-flags.text": "Export All Feature Flag Designs", + "feature-flags.export-feature-flag-design.text": "Export Feature Flag Design", + "feature-flags.export-feature-flags-data.text": "Email Feature Flag Data", + "feature-flags.export-all-feature-flags.confirmation-text.text": "Are you sure you want to export all feature flags design (JSON)?", + "feature-flags.export-feature-flag-design.confirmation-text.text": "Are you sure you want to export the feature flags design (JSON)?", + "feature-flags.export-feature-flags-data.confirmation-text.text": "Are you sure you want to email the feature flags data (CSV)?", "feature-flags.upsert-private-segment-list-modal": "Please select an available segment from the dropdown.", "segments.title.text": "Segments", "segments.subtitle.text": "Define new segments to include or exclude from any experiment", diff --git a/frontend/projects/upgrade/src/environments/environment-types.ts b/frontend/projects/upgrade/src/environments/environment-types.ts index d3f0f2e1aa..9fc8faf8e4 100644 --- a/frontend/projects/upgrade/src/environments/environment-types.ts +++ b/frontend/projects/upgrade/src/environments/environment-types.ts @@ -31,6 +31,8 @@ export interface APIEndpoints { featureFlag: string; updateFlagStatus: string; getPaginatedFlags: string; + exportFlagsDesign: string, + emailFlagData: string, addFlagInclusionList: string; addFlagExclusionList: string; setting: string; diff --git a/frontend/projects/upgrade/src/environments/environment.bsnl.ts b/frontend/projects/upgrade/src/environments/environment.bsnl.ts index f7350a8d28..63f2c6d4ce 100644 --- a/frontend/projects/upgrade/src/environments/environment.bsnl.ts +++ b/frontend/projects/upgrade/src/environments/environment.bsnl.ts @@ -44,6 +44,8 @@ export const environment = { featureFlag: '/flags', updateFlagStatus: '/flags/status', getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', setting: '/setting', diff --git a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts index 80b95a90ad..d9c29f8386 100755 --- a/frontend/projects/upgrade/src/environments/environment.demo.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.demo.prod.ts @@ -44,6 +44,8 @@ export const environment = { featureFlag: '/flags', updateFlagStatus: '/flags/status', getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', setting: '/setting', diff --git a/frontend/projects/upgrade/src/environments/environment.prod.ts b/frontend/projects/upgrade/src/environments/environment.prod.ts index 5078773d98..a0996a60fd 100755 --- a/frontend/projects/upgrade/src/environments/environment.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.prod.ts @@ -44,6 +44,8 @@ export const environment = { featureFlag: '/flags', updateFlagStatus: '/flags/status', getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', setting: '/setting', diff --git a/frontend/projects/upgrade/src/environments/environment.staging.ts b/frontend/projects/upgrade/src/environments/environment.staging.ts index e9dd2d732e..807d1e8363 100644 --- a/frontend/projects/upgrade/src/environments/environment.staging.ts +++ b/frontend/projects/upgrade/src/environments/environment.staging.ts @@ -44,6 +44,8 @@ export const environment = { featureFlag: '/flags', updateFlagStatus: '/flags/status', getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', setting: '/setting', diff --git a/frontend/projects/upgrade/src/environments/environment.ts b/frontend/projects/upgrade/src/environments/environment.ts index c2f6efe6b8..1a0234e7e8 100755 --- a/frontend/projects/upgrade/src/environments/environment.ts +++ b/frontend/projects/upgrade/src/environments/environment.ts @@ -49,6 +49,8 @@ export const environment = { featureFlag: '/flags', updateFlagStatus: '/flags/status', getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', addFlagInclusionList: '/flags/inclusionList', addFlagExclusionList: '/flags/exclusionList', setting: '/setting',