diff --git a/packages/backend/src/api/controllers/ExperimentController.ts b/packages/backend/src/api/controllers/ExperimentController.ts index 517a870f0..c84fd5fea 100644 --- a/packages/backend/src/api/controllers/ExperimentController.ts +++ b/packages/backend/src/api/controllers/ExperimentController.ts @@ -477,6 +477,9 @@ interface ExperimentListImportValidation { * description: * type: string * minLength: 1 + * usedByCount: + * type: integer + * description: Number of non-archived experiments using this decision point * order: {} * queries: * type: array diff --git a/packages/backend/src/api/repositories/DecisionPointRepository.ts b/packages/backend/src/api/repositories/DecisionPointRepository.ts index e341d1fa8..cc4819a49 100644 --- a/packages/backend/src/api/repositories/DecisionPointRepository.ts +++ b/packages/backend/src/api/repositories/DecisionPointRepository.ts @@ -2,6 +2,12 @@ import { DecisionPoint } from '../models/DecisionPoint'; import { Repository, EntityManager } from 'typeorm'; import { EntityRepository } from '../../typeorm-typedi-extensions'; import repositoryError from './utils/repositoryError'; +import { EXPERIMENT_STATE } from 'upgrade_types'; + +export interface DecisionPointUsageCount { + decisionPointId: string; + usedByCount: number; +} @EntityRepository(DecisionPoint) export class DecisionPointRepository extends Repository { @@ -106,6 +112,40 @@ export class DecisionPointRepository extends Repository { }); } + public async getUsageCountsForExperiment( + experimentId: string, + entityManager?: EntityManager + ): Promise { + // Treat site-target pairs as case-insensitive and normalize missing targets to empty. + const repository = entityManager ? entityManager.getRepository(DecisionPoint) : this; + + return await repository + .createQueryBuilder('sourceDecisionPoint') + .select('sourceDecisionPoint.id', 'decisionPointId') + .addSelect('COUNT(DISTINCT usedByExperiment.id)::int', 'usedByCount') + .leftJoin( + DecisionPoint, + 'usedByDecisionPoint', + `LOWER(usedByDecisionPoint.site) = LOWER(sourceDecisionPoint.site) + AND LOWER(COALESCE(usedByDecisionPoint.target, '')) = LOWER(COALESCE(sourceDecisionPoint.target, ''))` + ) + .leftJoin('usedByDecisionPoint.experiment', 'usedByExperiment', 'usedByExperiment.state != :archivedState', { + archivedState: EXPERIMENT_STATE.ARCHIVED, + }) + .where('"sourceDecisionPoint"."experimentId" = :experimentId', { experimentId }) + .groupBy('sourceDecisionPoint.id') + .getRawMany() + .catch((errorMsg: any) => { + const errorMsgString = repositoryError( + this.constructor.name, + 'getUsageCountsForExperiment', + { experimentId }, + errorMsg + ); + throw errorMsgString; + }); + } + public async setAllPendingActivationFalse(experimentId: string, entityManager?: EntityManager): Promise { const that = entityManager ? entityManager : this; await that diff --git a/packages/backend/src/api/services/ExperimentService.ts b/packages/backend/src/api/services/ExperimentService.ts index 95c8cd5d9..e7c7fa563 100644 --- a/packages/backend/src/api/services/ExperimentService.ts +++ b/packages/backend/src/api/services/ExperimentService.ts @@ -12,7 +12,7 @@ import { } from '../models/Experiment'; import { ExperimentConditionRepository } from '../repositories/ExperimentConditionRepository'; -import { DecisionPointRepository } from '../repositories/DecisionPointRepository'; +import { DecisionPointRepository, DecisionPointUsageCount } from '../repositories/DecisionPointRepository'; import { ExperimentCondition } from '../models/ExperimentCondition'; import { DecisionPoint } from '../models/DecisionPoint'; import { ExperimentSchedulerService } from './ExperimentSchedulerService'; @@ -232,14 +232,38 @@ export class ExperimentService { } public async getSingleExperiment(id: string, logger?: UpgradeLogger): Promise { - const experiment = await this.findOne(id, logger); + const [experiment, decisionPointUsageCounts] = await Promise.all([ + this.findOne(id, logger), + this.decisionPointRepository.getUsageCountsForExperiment(id), + ]); + if (experiment) { - return this.reducedConditionPayload(this.formattingPayload(experiment)); + return this.attachDecisionPointUsageCounts( + this.reducedConditionPayload(this.formattingPayload(experiment)), + decisionPointUsageCounts + ); } else { return undefined; } } + private attachDecisionPointUsageCounts( + experiment: ExperimentDTO, + decisionPointUsageCounts: DecisionPointUsageCount[] + ): ExperimentDTO { + const decisionPointUsageCountById = new Map( + decisionPointUsageCounts.map(({ decisionPointId, usedByCount }) => [decisionPointId, Number(usedByCount)]) + ); + + return { + ...experiment, + partitions: experiment.partitions.map((decisionPoint) => ({ + ...decisionPoint, + usedByCount: decisionPointUsageCountById.get(decisionPoint.id) ?? 0, + })), + }; + } + public async findOne(id: string, logger?: UpgradeLogger): Promise { if (logger) { logger.info({ message: `Find experiment by id => ${id}` }); @@ -420,9 +444,15 @@ export class ExperimentService { if (logger) { logger.info({ message: `Update the experiment`, details: experiment }); } - return this.reducedConditionPayload( + const updatedExperiment = this.reducedConditionPayload( await this.updateExperimentInDB(experiment, currentUser, logger, entityManager) ); + const decisionPointUsageCounts = await this.decisionPointRepository.getUsageCountsForExperiment( + updatedExperiment.id, + entityManager + ); + + return this.attachDecisionPointUsageCounts(updatedExperiment, decisionPointUsageCounts); } public async getExperimentalConditions(experimentId: string, logger: UpgradeLogger): Promise { @@ -542,7 +572,15 @@ export class ExperimentService { this.transformStateTimeLogs([responseStateTimeLog])[0], ]; - return this.reducedConditionPayload(this.formattingPayload(this.formattingConditionPayload(oldExperiment))); + const updatedExperiment = this.reducedConditionPayload( + this.formattingPayload(this.formattingConditionPayload(oldExperiment)) + ); + const decisionPointUsageCounts = await this.decisionPointRepository.getUsageCountsForExperiment( + experimentId, + entityManager + ); + + return this.attachDecisionPointUsageCounts(updatedExperiment, decisionPointUsageCounts); } public async verifyExperiments( diff --git a/packages/backend/test/unit/repositories/DecisionPointRepository.test.ts b/packages/backend/test/unit/repositories/DecisionPointRepository.test.ts index c8d8c23fc..e2f204607 100644 --- a/packages/backend/test/unit/repositories/DecisionPointRepository.test.ts +++ b/packages/backend/test/unit/repositories/DecisionPointRepository.test.ts @@ -3,6 +3,7 @@ import { DecisionPointRepository } from '../../../src/api/repositories/DecisionP import { DecisionPoint } from '../../../src/api/models/DecisionPoint'; import { Container } from '../../../src/typeorm-typedi-extensions'; import { initializeMocks } from '../mockdata/mockRepo'; +import { EXPERIMENT_STATE } from 'upgrade_types'; let mock; let manager; @@ -38,6 +39,7 @@ beforeEach(() => { manager = { createQueryBuilder: repo.createQueryBuilder, + getRepository: jest.fn().mockReturnValue({ createQueryBuilder: repo.createQueryBuilder }), }; }); @@ -196,6 +198,51 @@ describe('DecisionPointRepository Testing', () => { expect(mock.getMany).toHaveBeenCalledTimes(1); }); + it('should get distinct non-archived experiment usage counts for each decision point in an experiment', async () => { + const usageCounts = [{ decisionPointId: decisionPoint.id, usedByCount: 2 }]; + mock.getRawMany.mockResolvedValue(usageCounts); + + const res = await repo.getUsageCountsForExperiment('experiment-1'); + + expect(repo.createQueryBuilder).toHaveBeenCalledWith('sourceDecisionPoint'); + expect(mock.select).toHaveBeenCalledWith('sourceDecisionPoint.id', 'decisionPointId'); + expect(mock.addSelect).toHaveBeenCalledWith('COUNT(DISTINCT usedByExperiment.id)::int', 'usedByCount'); + expect(mock.leftJoin).toHaveBeenNthCalledWith( + 1, + DecisionPoint, + 'usedByDecisionPoint', + expect.stringContaining('COALESCE(usedByDecisionPoint.target') + ); + expect(mock.leftJoin).toHaveBeenNthCalledWith( + 2, + 'usedByDecisionPoint.experiment', + 'usedByExperiment', + 'usedByExperiment.state != :archivedState', + { archivedState: EXPERIMENT_STATE.ARCHIVED } + ); + expect(mock.where).toHaveBeenCalledWith('"sourceDecisionPoint"."experimentId" = :experimentId', { + experimentId: 'experiment-1', + }); + expect(mock.groupBy).toHaveBeenCalledWith('sourceDecisionPoint.id'); + expect(mock.getRawMany).toHaveBeenCalledTimes(1); + expect(res).toEqual(usageCounts); + }); + + it('should use the provided entity manager to get decision point usage counts within a transaction', async () => { + mock.getRawMany.mockResolvedValue([]); + + await repo.getUsageCountsForExperiment('experiment-1', manager); + + expect(manager.getRepository).toHaveBeenCalledWith(DecisionPoint); + expect(repo.createQueryBuilder).toHaveBeenCalledWith('sourceDecisionPoint'); + }); + + it('should throw an error when getting decision point usage counts fails', async () => { + mock.getRawMany.mockRejectedValue(err); + + await expect(repo.getUsageCountsForExperiment('experiment-1')).rejects.toThrow(err); + }); + describe('setAllPendingActivationFalse', () => { it('should set pendingActivation to false for all DPs using entityManager', async () => { await repo.setAllPendingActivationFalse('experiment-1', manager); diff --git a/packages/backend/test/unit/services/ExperimentService.test.ts b/packages/backend/test/unit/services/ExperimentService.test.ts index 2422717c8..82d3e71be 100644 --- a/packages/backend/test/unit/services/ExperimentService.test.ts +++ b/packages/backend/test/unit/services/ExperimentService.test.ts @@ -214,6 +214,7 @@ describe('ExperimentService Testing', () => { return { save: jest.fn().mockResolvedValue(mockExperiment), findOne: jest.fn().mockResolvedValue(mockExperiment), + findBy: jest.fn().mockResolvedValue([mockExperiment]), }; } if (entity === StateTimeLog) { @@ -289,6 +290,7 @@ describe('ExperimentService Testing', () => { find: jest.fn().mockResolvedValue([mockDecisionPoint1]), save: jest.fn().mockResolvedValue(mockDecisionPoint1), findOne: jest.fn().mockResolvedValue(null), + getUsageCountsForExperiment: jest.fn().mockResolvedValue([]), upsertDecisionPoint: jest.fn().mockImplementation((value) => Promise.resolve(value)), deleteDecisionPoint: jest.fn().mockResolvedValue({ affected: 1 }), deleteByIds: jest.fn().mockResolvedValue({ affected: 1 }), @@ -468,6 +470,19 @@ describe('ExperimentService Testing', () => { ); }); + it('should include current decision point usage counts in the updated experiment response', async () => { + decisionPointRepo.getUsageCountsForExperiment = jest + .fn() + .mockResolvedValue([{ decisionPointId: mockDecisionPoint1.id, usedByCount: 2 }]); + + const result = await service.update(mockExperimentDTO, mockUser, logger); + + expect(decisionPointRepo.getUsageCountsForExperiment).toHaveBeenCalledWith(mockExperimentDTO.id, undefined); + expect(result.partitions).toEqual( + expect.arrayContaining([expect.objectContaining({ id: mockDecisionPoint1.id, usedByCount: 2 })]) + ); + }); + it('should update conditions when they are modified', async () => { const result = await service.update(mockExperimentDTO, mockUser, logger); @@ -983,6 +998,55 @@ describe('ExperimentService Testing', () => { expect(decisionPointRepo.setAllPendingActivationFalse).not.toHaveBeenCalled(); }); + + it('should include current decision point usage counts in the updated state response', async () => { + decisionPointRepo.getUsageCountsForExperiment = jest + .fn() + .mockResolvedValue([{ decisionPointId: mockDecisionPoint1.id, usedByCount: 2 }]); + + const result = await service.updateState( + mockExperiment.id, + EXPERIMENT_STATE.PAUSED, + mockUser, + logger, + undefined, + entityManager + ); + + expect(decisionPointRepo.getUsageCountsForExperiment).toHaveBeenCalledWith(mockExperiment.id, entityManager); + expect(result.partitions).toEqual( + expect.arrayContaining([expect.objectContaining({ id: mockDecisionPoint1.id, usedByCount: 2 })]) + ); + }); + }); + + describe('getSingleExperiment()', () => { + it('should attach decision point usage counts to the single experiment response', async () => { + const secondDecisionPoint = { + ...createMockDecisionPoint1(), + id: 'partition-2', + site: 'another-site', + } as DecisionPoint; + const experiment = { + ...mockExperiment, + partitions: [mockDecisionPoint1 as DecisionPoint, secondDecisionPoint], + } as Experiment; + + jest.spyOn(service, 'findOne').mockResolvedValue(experiment); + decisionPointRepo.getUsageCountsForExperiment = jest + .fn() + .mockResolvedValue([{ decisionPointId: mockDecisionPoint1.id, usedByCount: 2 }]); + + const result = await service.getSingleExperiment(mockExperiment.id); + + expect(decisionPointRepo.getUsageCountsForExperiment).toHaveBeenCalledWith(mockExperiment.id); + expect(result.partitions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: mockDecisionPoint1.id, usedByCount: 2 }), + expect.objectContaining({ id: secondDecisionPoint.id, usedByCount: 0 }), + ]) + ); + }); }); describe('paginatedSearchString()', () => { diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts index baf26ab65..0d175d531 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.spec.ts @@ -181,6 +181,32 @@ describe('ExperimentDataService', () => { expect(mockHttpClient.put).toHaveBeenCalledWith(expectedUrl, { ...experiment }); }); + + it('should strip decision point usage counts from the update request', () => { + const experiment = { + ...mockExperiment, + partitions: [ + { + id: 'decision-point-1', + site: 'lesson-stream', + target: 'question-hint', + description: '', + order: 1, + createdAt: 'time', + updatedAt: 'time', + versionNumber: 1, + excludeIfReached: false, + usedByCount: 2, + }, + ], + } as Experiment; + + service.updateExperiment(experiment); + + const requestBody = mockHttpClient.put.mock.calls[0][1]; + expect(requestBody.partitions[0]).not.toHaveProperty('usedByCount'); + expect(requestBody.partitions[0]).toEqual(expect.objectContaining({ id: 'decision-point-1' })); + }); }); describe('#updateExperimentState', () => { diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.ts index 8bf502b07..52be0ad3b 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/experiments.data.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Experiment, + ExperimentDecisionPoint, ExperimentStateInfo, ExperimentPaginationParams, UpdateExperimentFilterModeRequest, @@ -57,7 +58,14 @@ export class ExperimentDataService { private stripVMProperties(experiment: Experiment): Experiment { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { stat, weightingMethod, ...experimentData } = experiment as any; - return experimentData; + return { + ...experimentData, + partitions: experimentData.partitions.map((decisionPoint: ExperimentDecisionPoint) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { usedByCount, ...decisionPointData } = decisionPoint; + return decisionPointData; + }), + }; } updateExperimentState(experimentId: string, experimentStateInfo: ExperimentStateInfo) { diff --git a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts index b176ad751..20f6acb39 100644 --- a/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts +++ b/packages/frontend/projects/upgrade/src/app/core/experiments/store/experiments.model.ts @@ -204,6 +204,7 @@ export interface ExperimentDecisionPoint { versionNumber: number; excludeIfReached: boolean; pendingActivation?: boolean; + usedByCount?: number; } export interface ExperimentFactor { diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html index 04cf8fa1b..c1d01a7d6 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.html @@ -29,6 +29,16 @@ + + + + {{ DECISION_POINT_TRANSLATION_KEYS.USED_BY | translate }} + + + {{ getUsedByCountText(decisionPoint) }} + + + diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss index 94737fe6e..0f349a825 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.scss @@ -63,7 +63,7 @@ } .decision-point-column { - width: 70%; + width: 55%; padding-left: 32px; .decision-point-link { @@ -85,6 +85,11 @@ } } + .used-by-column { + width: 15%; + white-space: nowrap; + } + .exclude-if-reached-column { width: 20%; text-align: center; diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts index 79d885120..efbb31460 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs'; import { ExperimentDecisionPointsTableComponent } from './experiment-decision-points-table.component'; @@ -19,6 +19,7 @@ describe('ExperimentDecisionPointsTableComponent', () => { updatedAt: '', versionNumber: 1, excludeIfReached: false, + usedByCount: 1, } as ExperimentDecisionPoint; beforeEach(async () => { @@ -45,4 +46,40 @@ describe('ExperimentDecisionPointsTableComponent', () => { it('should format decision points using the shared display format', () => { expect(component.getDecisionPoint(decisionPoint)).toBe('lesson-stream (question-hint)'); }); + + it('should display the Used By column after Decision Point', () => { + expect(component.displayedColumns).toEqual(['decisionPoint', 'usedBy', 'excludeIfReached', 'actions']); + }); + + it('should format singular, plural, and zero experiment counts', () => { + const translate = TestBed.inject(TranslateService); + jest + .spyOn(translate, 'instant') + .mockImplementation((translationKey: string, { count }: { count: number }) => + translationKey.endsWith('.one.text') ? `${count} Experiment` : `${count} Experiments` + ); + + expect(component.getUsedByCountText({ ...decisionPoint, usedByCount: 0 })).toBe('0 Experiments'); + expect(component.getUsedByCountText({ ...decisionPoint, usedByCount: 1 })).toBe('1 Experiment'); + expect(component.getUsedByCountText({ ...decisionPoint, usedByCount: 2 })).toBe('2 Experiments'); + }); + + it('should leave the Used By cell blank until its count is loaded', () => { + expect(component.getUsedByCountText({ ...decisionPoint, usedByCount: undefined })).toBe(''); + }); + + it('should render the formatted usage count in the Used By cell', () => { + const translate = TestBed.inject(TranslateService); + const translateInstantSpy = jest + .spyOn(translate, 'instant') + .mockImplementation((_translationKey: string, { count }: { count: number }) => `${count} Experiment`); + + fixture.componentRef.setInput('decisionPoints', [{ ...decisionPoint }]); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('td.used-by-column').textContent).toContain('1 Experiment'); + expect(translateInstantSpy).toHaveBeenCalledWith('experiments.details.decision-points.used-by-count.one.text', { + count: 1, + }); + }); }); diff --git a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts index 97ce5d797..bd49df959 100644 --- a/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts +++ b/packages/frontend/projects/upgrade/src/app/features/dashboard/experiments/pages/experiment-details-page/experiment-details-page-content/experiment-decision-points-section-card/experiment-decision-points-table/experiment-decision-points-table.component.ts @@ -44,10 +44,11 @@ export class ExperimentDecisionPointsTableComponent { @Output() rowAction = new EventEmitter(); @Output() decisionPointClick = new EventEmitter(); - displayedColumns: string[] = ['decisionPoint', 'excludeIfReached', 'actions']; + displayedColumns: string[] = ['decisionPoint', 'usedBy', 'excludeIfReached', 'actions']; DECISION_POINT_TRANSLATION_KEYS = { DECISION_POINT: 'experiments.details.decision-points.decision-point.text', + USED_BY: 'experiments.details.decision-points.used-by.text', EXCLUDE_IF_REACHED: 'experiments.details.decision-points.exclude-if-reached.text', ACTIONS: 'experiments.details.decision-points.actions.text', }; @@ -82,6 +83,20 @@ export class ExperimentDecisionPointsTableComponent { return formatDecisionPointDisplay(decisionPoint); } + getUsedByCountText(decisionPoint: ExperimentDecisionPoint): string { + const count = decisionPoint.usedByCount; + if (count === undefined || count === null) { + return ''; + } + + const translationKey = + count === 1 + ? 'experiments.details.decision-points.used-by-count.one.text' + : 'experiments.details.decision-points.used-by-count.other.text'; + + return this.translate.instant(translationKey, { count }); + } + onDecisionPointClick(decisionPoint: ExperimentDecisionPoint): void { this.decisionPointClick.emit(decisionPoint); } diff --git a/packages/frontend/projects/upgrade/src/assets/i18n/en.json b/packages/frontend/projects/upgrade/src/assets/i18n/en.json index f0bc34e50..4b015c586 100644 --- a/packages/frontend/projects/upgrade/src/assets/i18n/en.json +++ b/packages/frontend/projects/upgrade/src/assets/i18n/en.json @@ -485,6 +485,9 @@ "experiments.details.decision-points.card.subtitle.text": "Define decision points for this experiment.", "experiments.details.add-decision-point.button.text": "Add Decision Point", "experiments.details.decision-points.decision-point.text": "Decision Point", + "experiments.details.decision-points.used-by.text": "Used By", + "experiments.details.decision-points.used-by-count.one.text": "{{ count }} Experiment", + "experiments.details.decision-points.used-by-count.other.text": "{{ count }} Experiments", "experiments.details.decision-points.target.text": "Target", "experiments.details.decision-points.exclude-if-reached.text": "Exclude If Reached", "experiments.details.decision-points.actions.text": "Actions",