Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';

import { ExperimentService } from './experiments.service';
import {
Experiment,
ExperimentDecisionPoint,
UpdateExperimentDecisionPointsRequest,
DecisionPointFormData,
} from './store/experiments.model';

@Injectable({
providedIn: 'root',
})
export class DecisionPointHelperService {
constructor(private experimentService: ExperimentService) {}

/**
* Add a new decision point to an experiment
*/
addDecisionPoint(experiment: Experiment, decisionPointData: DecisionPointFormData): void {
const currentDecisionPoints = [...(experiment.partitions || [])];
const newDecisionPoint = {
id: uuidv4(),
site: decisionPointData.site,
target: decisionPointData.target,
description: '',
order: currentDecisionPoints.length + 1,
excludeIfReached: decisionPointData.excludeIfReached,
};

const updatedDecisionPoints = [...currentDecisionPoints, newDecisionPoint] as ExperimentDecisionPoint[];

this.updateExperimentDecisionPoints(experiment, updatedDecisionPoints);
}

/**
* Update an existing decision point in an experiment
*/
updateDecisionPoint(
experiment: Experiment,
sourceDecisionPoint: ExperimentDecisionPoint,
decisionPointData: DecisionPointFormData
): void {
const currentDecisionPoints = [...(experiment.partitions || [])];
const updatedDecisionPoints = currentDecisionPoints.map((dp) =>
dp.id === sourceDecisionPoint.id
? {
...dp,
site: decisionPointData.site,
target: decisionPointData.target,
excludeIfReached: decisionPointData.excludeIfReached,
}
: dp
);

this.updateExperimentDecisionPoints(experiment, updatedDecisionPoints);
}

/**
* Delete a decision point from an experiment
*/
deleteDecisionPoint(experiment: Experiment, decisionPointToDelete: ExperimentDecisionPoint): void {
const currentDecisionPoints = [...(experiment.partitions || [])];
const updatedDecisionPoints = currentDecisionPoints.filter((dp) => dp.id !== decisionPointToDelete.id);

// Reorder the remaining decision points
const reorderedDecisionPoints = updatedDecisionPoints.map((dp, index) => ({
...dp,
order: index + 1,
}));

this.updateExperimentDecisionPoints(experiment, reorderedDecisionPoints);
}

/**
* Common method to update experiment decision points
*/
private updateExperimentDecisionPoints(
experiment: Experiment,
updatedDecisionPoints: ExperimentDecisionPoint[]
): void {
const updateRequest: UpdateExperimentDecisionPointsRequest = {
experiment,
decisionPoints: updatedDecisionPoints,
};

this.experimentService.updateExperimentDecisionPoints(updateRequest);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ExperimentStateInfo,
ExperimentPaginationParams,
UpdateExperimentFilterModeRequest,
UpdateExperimentDecisionPointsRequest,
ExperimentSegmentListResponse,
} from './store/experiments.model';
import { HttpClient, HttpParams } from '@angular/common/http';
Expand Down Expand Up @@ -156,4 +157,12 @@ export class ExperimentDataService {
};
return this.updateExperiment(updatedExperiment);
}

updateExperimentDecisionPoints(params: UpdateExperimentDecisionPointsRequest): Observable<Experiment> {
const updatedExperiment = {
...params.experiment,
partitions: params.decisionPoints,
};
return this.updateExperiment(updatedExperiment);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
EXPERIMENT_STATE,
AddExperimentRequest,
UpdateExperimentFilterModeRequest,
UpdateExperimentDecisionPointsRequest,
} from './store/experiments.model';
import { Store, select } from '@ngrx/store';
import {
Expand Down Expand Up @@ -192,6 +193,12 @@ export class ExperimentService {
this.store$.dispatch(experimentAction.actionUpdateExperimentFilterMode({ updateExperimentFilterModeRequest }));
}

updateExperimentDecisionPoints(updateExperimentDecisionPointsRequest: UpdateExperimentDecisionPointsRequest) {
this.store$.dispatch(
experimentAction.actionUpdateExperimentDecisionPoints({ updateExperimentDecisionPointsRequest })
);
}

fetchContextMetaData() {
this.store$.dispatch(experimentAction.actionFetchContextMetaData({ isLoadingContextMetaData: true }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IEnrollmentStatByDate,
IContextMetaData,
UpdateExperimentFilterModeRequest,
UpdateExperimentDecisionPointsRequest,
ExperimentSegmentListResponse,
} from './experiments.model';
import { ExperimentSegmentListRequest, ExperimentSegmentListDetails } from '../../segments/store/segments.model';
Expand Down Expand Up @@ -101,6 +102,20 @@ export const actionUpdateExperimentFilterModeFailure = createAction(
'[Experiment] Update Experiment Filter Mode Failure'
);

export const actionUpdateExperimentDecisionPoints = createAction(
'[Experiment] Update Experiment Decision Points',
props<{ updateExperimentDecisionPointsRequest: UpdateExperimentDecisionPointsRequest }>()
);

export const actionUpdateExperimentDecisionPointsSuccess = createAction(
'[Experiment] Update Experiment Decision Points Success',
props<{ experiment: Experiment }>()
);

export const actionUpdateExperimentDecisionPointsFailure = createAction(
'[Experiment] Update Experiment Decision Points Failure'
);

export const actionFetchAllDecisionPoints = createAction('[Experiment] Fetch All Decision Points');

export const actionFetchAllDecisionPointsSuccess = createAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,30 @@ export class ExperimentEffects {
)
);

updateExperimentDecisionPoints$ = createEffect(() =>
this.actions$.pipe(
ofType(experimentAction.actionUpdateExperimentDecisionPoints),
switchMap((action) => {
return this.experimentDataService
.updateExperimentDecisionPoints(action.updateExperimentDecisionPointsRequest)
.pipe(
map((experiment) => {
this.notificationService.showSuccess(
this.translate.instant('experiments.decision-points.update-success.text')
);
return experimentAction.actionUpdateExperimentDecisionPointsSuccess({ experiment });
}),
catchError(() => {
this.notificationService.showError(
this.translate.instant('experiments.decision-points.update-error.text')
);
return [experimentAction.actionUpdateExperimentDecisionPointsFailure()];
})
);
})
)
);

deleteExperiment$ = createEffect(() =>
this.actions$.pipe(
ofType(experimentAction.actionDeleteExperiment),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,13 @@ export interface UpsertExperimentParams {
action: UPSERT_EXPERIMENT_ACTION;
}

export interface UpsertDecisionPointParams {
sourceDecisionPoint: ExperimentDecisionPoint;
context: string;
action: UPSERT_EXPERIMENT_ACTION;
experimentId: string;
}
Comment thread
zackcl marked this conversation as resolved.

export interface ExperimentFormData {
name: string;
description: string;
Expand All @@ -348,6 +355,12 @@ export interface ExperimentFormData {
tags: string[];
}

export interface DecisionPointFormData {
site: string;
target: string;
excludeIfReached: boolean;
}

// Base interfaces matching backend DTO structure
export interface ExperimentConditionDTO {
id: string;
Expand Down Expand Up @@ -482,6 +495,11 @@ export interface UpdateExperimentFilterModeRequest {
filterMode: FILTER_MODE;
}

export interface UpdateExperimentDecisionPointsRequest {
experiment: Experiment;
decisionPoints: ExperimentDecisionPoint[];
}

export const EXPERIMENT_ROOT_COLUMN_NAMES = {
NAME: 'name',
STATUS: 'state',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ const reducer = createReducer(
on(experimentsAction.actionUpdateExperimentFilterModeSuccess, (state, { experiment }) =>
adapter.upsertOne(experiment, { ...state, isLoadingExperiment: false })
),
on(experimentsAction.actionUpdateExperimentDecisionPoints, (state) => ({ ...state, isLoadingExperiment: true })),
on(experimentsAction.actionUpdateExperimentDecisionPointsSuccess, (state, { experiment }) =>
adapter.upsertOne(experiment, { ...state, isLoadingExperiment: false })
),
on(experimentsAction.actionUpdateExperimentDecisionPointsFailure, (state) => ({
...state,
isLoadingExperiment: false,
})),
on(experimentsAction.actionFetchAllDecisionPointsSuccess, (state, { decisionPoints }) => ({
...state,
allDecisionPoints: decisionPoints,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<app-common-dialog
[title]="config.title"
[cancelBtnLabel]="config.cancelBtnLabel"
[primaryActionBtnLabel]="config.primaryActionBtnLabel"
[primaryActionBtnColor]="config.primaryActionBtnColor"
[primaryActionBtnDisabled]="isPrimaryButtonDisabled$ | async"
(primaryActionBtnClicked)="onPrimaryActionBtnClicked()"
>
<form [formGroup]="decisionPointForm" class="form-standard dense-3">

<mat-form-field appearance="outline">
<mat-label class="ft-14-400">Site</mat-label>
<input
matInput
formControlName="site"
placeholder="e.g., assignProblem"
class="ft-14-400"
[matAutocomplete]="siteAutocomplete"
/>
<mat-autocomplete #siteAutocomplete="matAutocomplete" panelWidth="auto">
<mat-option
*ngFor="let site of filteredSites$ | async"
[value]="site"
class="ft-14-400"
>
{{ site }}
</mat-option>
</mat-autocomplete>
<mat-hint class="form-hint ft-12-400">
{{ 'experiments.upsert-decision-point-modal.site-hint.text' | translate }}
<a class="learn-more-link" href="https://www.upgradeplatform.org/" target="_blank" rel="noopener noreferrer">
Learn more
</a>
</mat-hint>
</mat-form-field>

<mat-form-field appearance="outline">
<mat-label class="ft-14-400">Target</mat-label>
<input
matInput
formControlName="target"
placeholder="e.g., targetProblem"
class="ft-14-400"
[matAutocomplete]="targetAutocomplete"
/>
<mat-autocomplete #targetAutocomplete="matAutocomplete" panelWidth="auto">
<mat-option
*ngFor="let target of filteredTargets$ | async"
[value]="target"
class="ft-14-400"
>
{{ target }}
</mat-option>
</mat-autocomplete>
<mat-hint class="form-hint ft-12-400">
{{ 'experiments.upsert-decision-point-modal.target-hint.text' | translate }}
<a class="learn-more-link" href="https://www.upgradeplatform.org/" target="_blank" rel="noopener noreferrer">
Learn more
</a>
</mat-hint>
</mat-form-field>

<div class="exclude-if-reached-container dense-1">
<mat-checkbox formControlName="excludeIfReached">
<span class="checkbox-label ft-14-600">Exclude If Reached</span>
<span class="form-hint ft-12-400" style="display:block; user-select: none;">
{{ 'experiments.upsert-decision-point-modal.exclude-if-reached-hint.text' | translate }}
<a class="learn-more-link" href="https://www.upgradeplatform.org/" target="_blank" rel="noopener noreferrer">
Learn more
</a>
</span>
</mat-checkbox>
</div>

</form>
</app-common-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.exclude-if-reached-container {
margin-bottom: 16px;

.checkbox-label {
margin-left: 0;
}
}
Loading
Loading