diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeOverwriteWarning/strategy-change-diff-calculation.test.ts b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeOverwriteWarning/strategy-change-diff-calculation.test.ts index 3eecee1e84..ea83dbf387 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeOverwriteWarning/strategy-change-diff-calculation.test.ts +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ChangeOverwriteWarning/strategy-change-diff-calculation.test.ts @@ -9,6 +9,7 @@ import { getSegmentChangesThatWouldBeOverwritten, getStrategyChangesThatWouldBeOverwritten, } from './strategy-change-diff-calculation.js'; +import { constraintId } from 'constants/constraintId.js'; describe('Strategy change conflict detection', () => { const existingStrategy: IFeatureStrategy = { @@ -175,6 +176,7 @@ describe('Strategy change conflict detection', () => { operator: 'IN' as const, contextName: 'appName', caseInsensitive: false, + [constraintId]: 'id1', }, ], variants: [ @@ -230,6 +232,7 @@ describe('Strategy change conflict detection', () => { operator: 'IN' as const, contextName: 'appName', caseInsensitive: false, + [constraintId]: 'id2', }, ], }; @@ -249,6 +252,7 @@ describe('Strategy change conflict detection', () => { inverted: false, operator: 'IN' as const, values: ['blah'], + [constraintId]: 'id2', }, ], }, @@ -478,6 +482,7 @@ describe('Segment change conflict detection', () => { operator: 'IN' as const, contextName: 'appName', caseInsensitive: false, + [constraintId]: 'id3', }, ], }; @@ -494,6 +499,7 @@ describe('Segment change conflict detection', () => { operator: 'IN' as const, contextName: 'appName', caseInsensitive: false, + [constraintId]: 'id4', }, ], }, diff --git a/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx b/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx index fd6f23c2f7..35c561a83d 100644 --- a/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx +++ b/frontend/src/component/project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/ProjectEnvironmentDefaultStrategy.tsx @@ -11,6 +11,8 @@ import { } from '@server/types/permissions'; import { StrategyItem } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem'; import type { IFeatureStrategy } from 'interfaces/strategy'; +import { constraintId } from 'constants/constraintId'; +import { v4 as uuidv4 } from 'uuid'; interface ProjectEnvironmentDefaultStrategyProps { environment: ProjectEnvironmentType; @@ -55,7 +57,11 @@ export const ProjectEnvironmentDefaultStrategy = ({ return { ...baseDefaultStrategy, disabled: false, - constraints: baseDefaultStrategy.constraints ?? [], + constraints: + baseDefaultStrategy.constraints?.map((constraint) => ({ + ...constraint, + [constraintId]: uuidv4(), + })) ?? [], title: baseDefaultStrategy.title ?? '', parameters: baseDefaultStrategy.parameters ?? {}, }; diff --git a/frontend/src/hooks/api/getters/useChangeRequest/useChangeRequest.ts b/frontend/src/hooks/api/getters/useChangeRequest/useChangeRequest.ts index 98e5f217ea..606c5e4d92 100644 --- a/frontend/src/hooks/api/getters/useChangeRequest/useChangeRequest.ts +++ b/frontend/src/hooks/api/getters/useChangeRequest/useChangeRequest.ts @@ -1,7 +1,22 @@ import useSWR from 'swr'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler.js'; -import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types'; +import type { + ChangeRequestType, + IChangeRequestAddStrategy, + IChangeRequestUpdateStrategy, + IFeatureChange, +} from 'component/changeRequest/changeRequest.types'; +import { useMemo } from 'react'; +import { constraintId } from 'constants/constraintId.js'; +import { v4 as uuidv4 } from 'uuid'; + +const isAddStrategyChange = ( + change: IFeatureChange, +): change is IChangeRequestAddStrategy => change.action === 'addStrategy'; +const isUpdateStrategyChange = ( + change: IFeatureChange, +): change is IChangeRequestUpdateStrategy => change.action === 'updateStrategy'; export const useChangeRequest = (projectId: string, id: string) => { const { data, error, mutate } = useSWR( @@ -10,8 +25,47 @@ export const useChangeRequest = (projectId: string, id: string) => { { refreshInterval: 15000 }, ); + const dataWithConstraintIds: ChangeRequestType | undefined = useMemo(() => { + if (!data) { + return data; + } + + const features = data.features.map((feature) => { + const changes: IFeatureChange[] = feature.changes.map((change) => { + if ( + isAddStrategyChange(change) || + isUpdateStrategyChange(change) + ) { + const { constraints, ...rest } = change.payload; + return { + ...change, + payload: { + ...rest, + constraints: constraints.map((constraint) => ({ + ...constraint, + [constraintId]: uuidv4(), + })), + }, + } as IFeatureChange; + } + return change; + }); + + return { + ...feature, + changes, + }; + }); + + const value: ChangeRequestType = { + ...data, + features, + }; + return value; + }, [data]); + return { - data, + data: dataWithConstraintIds, loading: !error && !data, refetchChangeRequest: () => mutate(), error, diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts index e5b19a6a63..fa1540f719 100644 --- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts +++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts @@ -1,9 +1,12 @@ import useSWR, { type SWRConfiguration } from 'swr'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { emptyFeature } from './emptyFeature.ts'; import handleErrorResponses from '../httpErrorResponseHandler.ts'; import { formatApiPath } from 'utils/formatPath'; import type { IFeatureToggle } from 'interfaces/featureToggle'; +import { constraintId } from 'constants/constraintId.ts'; +import { v4 as uuidv4 } from 'uuid'; +import type { IFeatureStrategy } from 'interfaces/strategy.ts'; export interface IUseFeatureOutput { feature: IFeatureToggle; @@ -35,8 +38,10 @@ export const useFeature = ( mutate().catch(console.warn); }, [mutate]); + const feature = useMemo(enrichConstraintsWithIds(data), [data?.body]); + return { - feature: data?.body || emptyFeature, + feature, refetchFeature, loading: !error && !data, status: data?.status, @@ -63,6 +68,41 @@ export const featureFetcher = async ( }; }; +export const enrichConstraintsWithIds = (data?: IFeatureResponse) => () => { + if (!data?.body) { + return emptyFeature; + } + + const { strategies, environments, ...rest } = data.body; + + const addConstraintIds = (strategy: IFeatureStrategy) => { + const { constraints, ...strategyRest } = strategy; + return { + ...strategyRest, + constraints: constraints?.map((constraint) => ({ + ...constraint, + [constraintId]: uuidv4(), + })), + }; + }; + + const strategiesWithConstraintIds = strategies?.map(addConstraintIds); + + const environmentsWithStrategyIds = environments?.map((environment) => { + const { strategies, ...environmentRest } = environment; + return { + ...environmentRest, + strategies: strategies?.map(addConstraintIds), + }; + }); + + return { + ...rest, + strategies: strategiesWithConstraintIds, + environments: environmentsWithStrategyIds, + }; +}; + export const formatFeatureApiPath = ( projectId: string, featureId: string, diff --git a/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts b/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts index b5c3656bfe..1e17c3f2ea 100644 --- a/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts +++ b/frontend/src/hooks/api/getters/useFeature/useFeatureImmutable.ts @@ -1,12 +1,12 @@ import useSWRImmutable from 'swr/immutable'; -import { useCallback } from 'react'; -import { emptyFeature } from './emptyFeature.js'; +import { useCallback, useMemo } from 'react'; import { type IUseFeatureOutput, type IFeatureResponse, featureFetcher, formatFeatureApiPath, useFeature, + enrichConstraintsWithIds, } from 'hooks/api/getters/useFeature/useFeature'; // useFeatureImmutable is like useFeature, except it won't refetch data on @@ -29,8 +29,10 @@ export const useFeatureImmutable = ( await refetchFeature(); }, [mutate, refetchFeature]); + const feature = useMemo(enrichConstraintsWithIds(data), [data?.body]); + return { - feature: data?.body || emptyFeature, + feature, refetchFeature: refetch, loading: !error && !data, status: data?.status,