mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: remove unused components and rename new (#6357)
This commit is contained in:
		
							parent
							
								
									9cd324bd7c
								
							
						
					
					
						commit
						20a9e1d725
					
				@ -24,7 +24,7 @@ import {
 | 
			
		||||
    Typography,
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import { Delete, Edit, MoreVert } from '@mui/icons-material';
 | 
			
		||||
import { NewEditChange } from './NewEditChange';
 | 
			
		||||
import { EditChange } from './EditChange';
 | 
			
		||||
 | 
			
		||||
const useShowActions = (changeRequest: ChangeRequestType, change: IChange) => {
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(
 | 
			
		||||
@ -149,7 +149,7 @@ export const ChangeActions: FC<{
 | 
			
		||||
                                                Edit change
 | 
			
		||||
                                            </Typography>
 | 
			
		||||
                                        </ListItemText>
 | 
			
		||||
                                        <NewEditChange
 | 
			
		||||
                                        <EditChange
 | 
			
		||||
                                            changeRequestId={changeRequest.id}
 | 
			
		||||
                                            featureId={feature}
 | 
			
		||||
                                            change={
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import useToast from 'hooks/useToast';
 | 
			
		||||
import { IFeatureStrategy } from 'interfaces/strategy';
 | 
			
		||||
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { formatStrategyName } from 'utils/strategyNames';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { useCollaborateData } from 'hooks/useCollaborateData';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
@ -16,13 +15,17 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { comparisonModerator } from 'component/feature/FeatureStrategy/featureStrategy.utils';
 | 
			
		||||
import {
 | 
			
		||||
    ChangeRequestAddStrategy,
 | 
			
		||||
    ChangeRequestEditStrategy,
 | 
			
		||||
    IChangeRequestAddStrategy,
 | 
			
		||||
    IChangeRequestUpdateStrategy,
 | 
			
		||||
} from 'component/changeRequest/changeRequest.types';
 | 
			
		||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
 | 
			
		||||
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
 | 
			
		||||
import { FeatureStrategyForm } from '../../../../feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
import { constraintId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
 | 
			
		||||
interface IEditChangeProps {
 | 
			
		||||
    change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
 | 
			
		||||
@ -34,6 +37,16 @@ interface IEditChangeProps {
 | 
			
		||||
    onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const addIdSymbolToConstraints = (
 | 
			
		||||
    strategy?: ChangeRequestAddStrategy | ChangeRequestEditStrategy,
 | 
			
		||||
) => {
 | 
			
		||||
    if (!strategy) return;
 | 
			
		||||
 | 
			
		||||
    return strategy?.constraints.map((constraint) => {
 | 
			
		||||
        return { ...constraint, [constraintId]: uuidv4() };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EditChange = ({
 | 
			
		||||
    change,
 | 
			
		||||
    changeRequestId,
 | 
			
		||||
@ -47,9 +60,12 @@ export const EditChange = ({
 | 
			
		||||
    const { editChange } = useChangeRequestApi();
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>(
 | 
			
		||||
        change.payload,
 | 
			
		||||
    );
 | 
			
		||||
    const constraintsWithId = addIdSymbolToConstraints(change.payload);
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({
 | 
			
		||||
        ...change.payload,
 | 
			
		||||
        constraints: constraintsWithId,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { segments: allSegments } = useSegments();
 | 
			
		||||
    const strategySegments = (allSegments || []).filter((segment) => {
 | 
			
		||||
@ -134,7 +150,7 @@ export const EditChange = ({
 | 
			
		||||
        >
 | 
			
		||||
            <FormTemplate
 | 
			
		||||
                modal
 | 
			
		||||
                title={formatStrategyName(strategyDefinition.name ?? '')}
 | 
			
		||||
                disablePadding
 | 
			
		||||
                description={featureStrategyHelp}
 | 
			
		||||
                documentationLink={featureStrategyDocsLink}
 | 
			
		||||
                documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
@ -148,7 +164,7 @@ export const EditChange = ({
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
                <NewFeatureStrategyForm
 | 
			
		||||
                <FeatureStrategyForm
 | 
			
		||||
                    projectId={projectId}
 | 
			
		||||
                    feature={data}
 | 
			
		||||
                    strategy={strategy}
 | 
			
		||||
@ -165,7 +181,7 @@ export const EditChange = ({
 | 
			
		||||
                    tab={tab}
 | 
			
		||||
                    setTab={setTab}
 | 
			
		||||
                    StrategyVariants={
 | 
			
		||||
                        <StrategyVariants
 | 
			
		||||
                        <NewStrategyVariants
 | 
			
		||||
                            strategy={strategy}
 | 
			
		||||
                            setStrategy={setStrategy}
 | 
			
		||||
                            environment={environment}
 | 
			
		||||
 | 
			
		||||
@ -1,227 +0,0 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { IFeatureStrategy } from 'interfaces/strategy';
 | 
			
		||||
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { useCollaborateData } from 'hooks/useCollaborateData';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { comparisonModerator } from 'component/feature/FeatureStrategy/featureStrategy.utils';
 | 
			
		||||
import {
 | 
			
		||||
    ChangeRequestAddStrategy,
 | 
			
		||||
    ChangeRequestEditStrategy,
 | 
			
		||||
    IChangeRequestAddStrategy,
 | 
			
		||||
    IChangeRequestUpdateStrategy,
 | 
			
		||||
} from 'component/changeRequest/changeRequest.types';
 | 
			
		||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
import { constraintId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
 | 
			
		||||
interface IEditChangeProps {
 | 
			
		||||
    change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
 | 
			
		||||
    changeRequestId: number;
 | 
			
		||||
    featureId: string;
 | 
			
		||||
    environment: string;
 | 
			
		||||
    open: boolean;
 | 
			
		||||
    onSubmit: () => void;
 | 
			
		||||
    onClose: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const addIdSymbolToConstraints = (
 | 
			
		||||
    strategy?: ChangeRequestAddStrategy | ChangeRequestEditStrategy,
 | 
			
		||||
) => {
 | 
			
		||||
    if (!strategy) return;
 | 
			
		||||
 | 
			
		||||
    return strategy?.constraints.map((constraint) => {
 | 
			
		||||
        return { ...constraint, [constraintId]: uuidv4() };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const NewEditChange = ({
 | 
			
		||||
    change,
 | 
			
		||||
    changeRequestId,
 | 
			
		||||
    environment,
 | 
			
		||||
    open,
 | 
			
		||||
    onSubmit,
 | 
			
		||||
    onClose,
 | 
			
		||||
    featureId,
 | 
			
		||||
}: IEditChangeProps) => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const { editChange } = useChangeRequestApi();
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
 | 
			
		||||
    const constraintsWithId = addIdSymbolToConstraints(change.payload);
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({
 | 
			
		||||
        ...change.payload,
 | 
			
		||||
        constraints: constraintsWithId,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const { segments: allSegments } = useSegments();
 | 
			
		||||
    const strategySegments = (allSegments || []).filter((segment) => {
 | 
			
		||||
        return change.payload.segments?.includes(segment.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const [segments, setSegments] = useState<ISegment[]>(strategySegments);
 | 
			
		||||
 | 
			
		||||
    const strategyDefinition = {
 | 
			
		||||
        parameters: change.payload.parameters,
 | 
			
		||||
        name: change.payload.name,
 | 
			
		||||
    };
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const errors = useFormErrors();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { unleashUrl } = uiConfig;
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
			
		||||
 | 
			
		||||
    const { feature, refetchFeature } = useFeature(projectId, featureId);
 | 
			
		||||
 | 
			
		||||
    const ref = useRef<IFeatureToggle>(feature);
 | 
			
		||||
 | 
			
		||||
    const { data, staleDataNotification, forceRefreshCache } =
 | 
			
		||||
        useCollaborateData<IFeatureToggle>(
 | 
			
		||||
            {
 | 
			
		||||
                unleashGetter: useFeature,
 | 
			
		||||
                params: [projectId, featureId],
 | 
			
		||||
                dataKey: 'feature',
 | 
			
		||||
                refetchFunctionKey: 'refetchFeature',
 | 
			
		||||
                options: {},
 | 
			
		||||
            },
 | 
			
		||||
            feature,
 | 
			
		||||
            {
 | 
			
		||||
                afterSubmitAction: refetchFeature,
 | 
			
		||||
            },
 | 
			
		||||
            comparisonModerator,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (ref.current.name === '' && feature.name) {
 | 
			
		||||
            forceRefreshCache(feature);
 | 
			
		||||
            ref.current = feature;
 | 
			
		||||
        }
 | 
			
		||||
    }, [feature]);
 | 
			
		||||
 | 
			
		||||
    const payload = {
 | 
			
		||||
        ...strategy,
 | 
			
		||||
        segments: segments.map((segment) => segment.id),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onInternalSubmit = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await editChange(projectId, changeRequestId, change.id, {
 | 
			
		||||
                action: strategy.id ? 'updateStrategy' : 'addStrategy',
 | 
			
		||||
                feature: featureId,
 | 
			
		||||
                payload,
 | 
			
		||||
            });
 | 
			
		||||
            onSubmit();
 | 
			
		||||
            setToastData({
 | 
			
		||||
                title: 'Change updated',
 | 
			
		||||
                type: 'success',
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!strategyDefinition) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!data) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <SidebarModal
 | 
			
		||||
            open={open}
 | 
			
		||||
            onClose={onClose}
 | 
			
		||||
            label='Edit change'
 | 
			
		||||
            onClick={(e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <FormTemplate
 | 
			
		||||
                modal
 | 
			
		||||
                disablePadding
 | 
			
		||||
                description={featureStrategyHelp}
 | 
			
		||||
                documentationLink={featureStrategyDocsLink}
 | 
			
		||||
                documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
                formatApiCode={() =>
 | 
			
		||||
                    formatUpdateStrategyApiCode(
 | 
			
		||||
                        projectId,
 | 
			
		||||
                        changeRequestId,
 | 
			
		||||
                        change.id,
 | 
			
		||||
                        payload,
 | 
			
		||||
                        unleashUrl,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
                <NewFeatureStrategyForm
 | 
			
		||||
                    projectId={projectId}
 | 
			
		||||
                    feature={data}
 | 
			
		||||
                    strategy={strategy}
 | 
			
		||||
                    setStrategy={setStrategy}
 | 
			
		||||
                    segments={segments}
 | 
			
		||||
                    setSegments={setSegments}
 | 
			
		||||
                    environmentId={environment}
 | 
			
		||||
                    onSubmit={onInternalSubmit}
 | 
			
		||||
                    onCancel={onClose}
 | 
			
		||||
                    loading={false}
 | 
			
		||||
                    permission={UPDATE_FEATURE_STRATEGY}
 | 
			
		||||
                    errors={errors}
 | 
			
		||||
                    isChangeRequest={isChangeRequestConfigured(environment)}
 | 
			
		||||
                    tab={tab}
 | 
			
		||||
                    setTab={setTab}
 | 
			
		||||
                    StrategyVariants={
 | 
			
		||||
                        <NewStrategyVariants
 | 
			
		||||
                            strategy={strategy}
 | 
			
		||||
                            setStrategy={setStrategy}
 | 
			
		||||
                            environment={environment}
 | 
			
		||||
                            projectId={projectId}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                {staleDataNotification}
 | 
			
		||||
            </FormTemplate>
 | 
			
		||||
        </SidebarModal>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatUpdateStrategyApiCode = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    changeRequestId: number,
 | 
			
		||||
    changeId: number,
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>,
 | 
			
		||||
    unleashUrl?: string,
 | 
			
		||||
): string => {
 | 
			
		||||
    if (!unleashUrl) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const url = `${unleashUrl}/api/admin/projects/${projectId}/change-requests/${changeRequestId}/changes/${changeId}`;
 | 
			
		||||
    const payload = JSON.stringify(strategy, undefined, 2);
 | 
			
		||||
 | 
			
		||||
    return `curl --location --request PUT '${url}' \\
 | 
			
		||||
    --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
    --header 'Content-Type: application/json' \\
 | 
			
		||||
    --data-raw '${payload}'`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const featureStrategyHelp = `
 | 
			
		||||
    An activation strategy will only run when a feature toggle is enabled and provides a way to control who will get access to the feature.
 | 
			
		||||
    If any of a feature toggle's activation strategies returns true, the user will get access.
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const featureStrategyDocsLink =
 | 
			
		||||
    'https://docs.getunleash.io/reference/activation-strategies';
 | 
			
		||||
 | 
			
		||||
export const featureStrategyDocsLinkLabel = 'Strategies documentation';
 | 
			
		||||
@ -1,6 +1,77 @@
 | 
			
		||||
import { formatAddStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
 | 
			
		||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { Route, Routes } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
test('formatAddStrategyApiCode', () => {
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_FEATURE_STRATEGY,
 | 
			
		||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
    UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
} from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { FeatureStrategyCreate } from './FeatureStrategyCreate';
 | 
			
		||||
import {
 | 
			
		||||
    setupProjectEndpoint,
 | 
			
		||||
    setupSegmentsEndpoint,
 | 
			
		||||
    setupStrategyEndpoint,
 | 
			
		||||
    setupFeaturesEndpoint,
 | 
			
		||||
    setupUiConfigEndpoint,
 | 
			
		||||
    setupContextEndpoint,
 | 
			
		||||
} from './featureStrategyFormTestSetup';
 | 
			
		||||
 | 
			
		||||
const featureName = 'my-new-feature';
 | 
			
		||||
 | 
			
		||||
const setupComponent = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        wrapper: render(
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                    path={
 | 
			
		||||
                        '/projects/:projectId/features/:featureId/strategies/create'
 | 
			
		||||
                    }
 | 
			
		||||
                    element={<FeatureStrategyCreate />}
 | 
			
		||||
                />
 | 
			
		||||
            </Routes>,
 | 
			
		||||
            {
 | 
			
		||||
                route: `/projects/default/features/${featureName}/strategies/create?environmentId=development&strategyName=flexibleRollout&defaultStrategy=true`,
 | 
			
		||||
                permissions: [
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: CREATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        expectedSegmentName: 'test',
 | 
			
		||||
        expectedGroupId: 'newGroupId',
 | 
			
		||||
        expectedVariantName: 'Blue',
 | 
			
		||||
        expectedSliderValue: '50',
 | 
			
		||||
        expectedConstraintValue: 'new value',
 | 
			
		||||
        expectedMultipleValues: '1234,4141,51515',
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
    setupProjectEndpoint();
 | 
			
		||||
    setupSegmentsEndpoint();
 | 
			
		||||
    setupStrategyEndpoint();
 | 
			
		||||
    setupFeaturesEndpoint(featureName);
 | 
			
		||||
    setupUiConfigEndpoint();
 | 
			
		||||
    setupContextEndpoint();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('NewFeatureStrategyCreate', () => {
 | 
			
		||||
    test('formatAddStrategyApiCode', () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            formatAddStrategyApiCode(
 | 
			
		||||
                'projectId',
 | 
			
		||||
@ -17,4 +88,360 @@ test('formatAddStrategyApiCode', () => {
 | 
			
		||||
            "id": "strategyId"
 | 
			
		||||
          }'"
 | 
			
		||||
        `);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should navigate tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        expect(slider).toHaveValue('100');
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const segmentsEl = await screen.findByText('Segments');
 | 
			
		||||
        expect(segmentsEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        expect(addVariantEl).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change general settings', async () => {
 | 
			
		||||
        const { expectedGroupId, expectedSliderValue } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        const groupIdInput = await screen.getByLabelText('groupId');
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue('100');
 | 
			
		||||
        expect(groupIdInput).toHaveValue(featureName);
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(slider, { target: { value: expectedSliderValue } });
 | 
			
		||||
        fireEvent.change(groupIdInput, { target: { value: expectedGroupId } });
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue(expectedSliderValue);
 | 
			
		||||
        expect(groupIdInput).toHaveValue(expectedGroupId);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change targeting settings', async () => {
 | 
			
		||||
        const { expectedConstraintValue, expectedSegmentName } =
 | 
			
		||||
            setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedConstraintValue },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEl = screen.getByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEl);
 | 
			
		||||
 | 
			
		||||
        const doneEl = screen.getByText('Done');
 | 
			
		||||
        fireEvent.click(doneEl);
 | 
			
		||||
 | 
			
		||||
        const selectElement = screen.getByPlaceholderText('Select segments');
 | 
			
		||||
        fireEvent.mouseDown(selectElement);
 | 
			
		||||
 | 
			
		||||
        const optionElement = await screen.findByText(expectedSegmentName);
 | 
			
		||||
        fireEvent.click(optionElement);
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedSegmentName)).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(expectedConstraintValue)).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change variants settings', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedVariantName },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedVariantName)).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const generalSettingsEl = screen.getByText('General');
 | 
			
		||||
        fireEvent.click(generalSettingsEl);
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            const codeSnippet = document.querySelector('pre')?.innerHTML;
 | 
			
		||||
            const variantNameMatches = (
 | 
			
		||||
                codeSnippet!.match(new RegExp(expectedVariantName, 'g')) || []
 | 
			
		||||
            ).length;
 | 
			
		||||
            const metaDataMatches = (codeSnippet!.match(/isValid/g) || [])
 | 
			
		||||
                .length;
 | 
			
		||||
            expect(variantNameMatches).toBe(1);
 | 
			
		||||
            expect(metaDataMatches).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change variant name after changing tab', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedVariantName },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const targetingEl = await screen.findByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        expect(addConstraintEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
        const inputElement2 = screen.getAllByRole('textbox')[0];
 | 
			
		||||
 | 
			
		||||
        expect(inputElement2).not.toBeDisabled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should remove empty variants when changing tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const variants = screen.queryAllByTestId('VARIANT');
 | 
			
		||||
        expect(variants.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const targetingEl = await screen.findByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        expect(addConstraintEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const variants2 = screen.queryAllByTestId('VARIANT');
 | 
			
		||||
        expect(variants2.length).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should autosave constraint settings when navigating between tabs', async () => {
 | 
			
		||||
        const { expectedMultipleValues } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedMultipleValues },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEl = await screen.findByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEl);
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const values = expectedMultipleValues.split(',');
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(values[0])).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(values[1])).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(values[2])).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should update multiple constraints correctly', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElements = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements[0], {
 | 
			
		||||
            target: { value: '123' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[1], {
 | 
			
		||||
            target: { value: '456' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[2], {
 | 
			
		||||
            target: { value: '789' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEls = await screen.findAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls[0]);
 | 
			
		||||
        fireEvent.click(addValueEls[1]);
 | 
			
		||||
        fireEvent.click(addValueEls[2]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).toBeInTheDocument();
 | 
			
		||||
        const deleteBtns = await screen.findAllByTestId('CancelIcon');
 | 
			
		||||
        fireEvent.click(deleteBtns[0]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('456')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('789')).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should update multiple constraints with the correct react key', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElements = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements[0], {
 | 
			
		||||
            target: { value: '123' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[1], {
 | 
			
		||||
            target: { value: '456' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[2], {
 | 
			
		||||
            target: { value: '789' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEls = await screen.findAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls[0]);
 | 
			
		||||
        fireEvent.click(addValueEls[1]);
 | 
			
		||||
        fireEvent.click(addValueEls[2]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const deleteBtns = screen.getAllByTestId('DELETE_CONSTRAINT_BUTTON');
 | 
			
		||||
        fireEvent.click(deleteBtns[0]);
 | 
			
		||||
 | 
			
		||||
        const inputElements2 = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements2[0], {
 | 
			
		||||
            target: { value: '666' },
 | 
			
		||||
        });
 | 
			
		||||
        const addValueEls2 = screen.getAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls2[0]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('456')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('789')).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should undo changes made to constraints', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputEl = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputEl, {
 | 
			
		||||
            target: { value: '6, 7, 8' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addBtn = await screen.findByText('Add values');
 | 
			
		||||
        addBtn.click();
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('6')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('7')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('8')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const undoBtn = await screen.findByTestId(
 | 
			
		||||
            'UNDO_CONSTRAINT_CHANGE_BUTTON',
 | 
			
		||||
        );
 | 
			
		||||
        undoBtn.click();
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('6')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('7')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('8')).not.toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should remove constraint when no valid values are set and moving between tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const seconAddConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
 | 
			
		||||
        expect(seconAddConstraintEl).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('appName')).not.toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
 | 
			
		||||
import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
 | 
			
		||||
@ -18,7 +17,6 @@ import {
 | 
			
		||||
} from '../FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { formatStrategyName } from 'utils/strategyNames';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { createFeatureStrategy } from 'utils/createFeatureStrategy';
 | 
			
		||||
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
 | 
			
		||||
@ -33,8 +31,11 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import useQueryParams from 'hooks/useQueryParams';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
 | 
			
		||||
import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
 | 
			
		||||
export const FeatureStrategyCreate = () => {
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const environmentId = useRequiredQueryParam('environmentId');
 | 
			
		||||
@ -178,10 +179,10 @@ export const FeatureStrategyCreate = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            modal
 | 
			
		||||
            title={formatStrategyName(strategyName)}
 | 
			
		||||
            description={featureStrategyHelp}
 | 
			
		||||
            documentationLink={featureStrategyDocsLink}
 | 
			
		||||
            documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
            disablePadding
 | 
			
		||||
            formatApiCode={() =>
 | 
			
		||||
                formatAddStrategyApiCode(
 | 
			
		||||
                    projectId,
 | 
			
		||||
@ -205,6 +206,17 @@ export const FeatureStrategyCreate = () => {
 | 
			
		||||
                permission={CREATE_FEATURE_STRATEGY}
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                isChangeRequest={isChangeRequestConfigured(environmentId)}
 | 
			
		||||
                tab={tab}
 | 
			
		||||
                setTab={setTab}
 | 
			
		||||
                StrategyVariants={
 | 
			
		||||
                    <NewStrategyVariants
 | 
			
		||||
                        strategy={strategy}
 | 
			
		||||
                        setStrategy={setStrategy}
 | 
			
		||||
                        environment={environmentId}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                        editable
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            {staleDataNotification}
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,77 @@
 | 
			
		||||
import { formatUpdateStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { IFeatureStrategy, IStrategy } from 'interfaces/strategy';
 | 
			
		||||
import { screen, waitFor, fireEvent } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { Route, Routes } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
test('formatUpdateStrategyApiCode', () => {
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_FEATURE_STRATEGY,
 | 
			
		||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
    UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
} from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { FeatureStrategyEdit } from './FeatureStrategyEdit';
 | 
			
		||||
import {
 | 
			
		||||
    setupContextEndpoint,
 | 
			
		||||
    setupFeaturesEndpoint,
 | 
			
		||||
    setupProjectEndpoint,
 | 
			
		||||
    setupSegmentsEndpoint,
 | 
			
		||||
    setupStrategyEndpoint,
 | 
			
		||||
    setupUiConfigEndpoint,
 | 
			
		||||
} from '../FeatureStrategyCreate/featureStrategyFormTestSetup';
 | 
			
		||||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
 | 
			
		||||
const featureName = 'my-new-feature';
 | 
			
		||||
const variantName = 'Blue';
 | 
			
		||||
 | 
			
		||||
const setupComponent = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        wrapper: render(
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                    path={
 | 
			
		||||
                        '/projects/:projectId/features/:featureId/strategies/edit'
 | 
			
		||||
                    }
 | 
			
		||||
                    element={<FeatureStrategyEdit />}
 | 
			
		||||
                />
 | 
			
		||||
            </Routes>,
 | 
			
		||||
            {
 | 
			
		||||
                route: `/projects/default/features/${featureName}/strategies/edit?environmentId=development&strategyId=1`,
 | 
			
		||||
                permissions: [
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: CREATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        expectedGroupId: 'newGroupId',
 | 
			
		||||
        expectedVariantName: variantName,
 | 
			
		||||
        expectedSliderValue: '75',
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
    setupProjectEndpoint();
 | 
			
		||||
    setupSegmentsEndpoint();
 | 
			
		||||
    setupStrategyEndpoint();
 | 
			
		||||
    setupFeaturesEndpoint(featureName, variantName);
 | 
			
		||||
    setupUiConfigEndpoint();
 | 
			
		||||
    setupContextEndpoint();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('NewFeatureStrategyEdit', () => {
 | 
			
		||||
    test('formatUpdateStrategyApiCode', () => {
 | 
			
		||||
        const strategy: IFeatureStrategy = {
 | 
			
		||||
            id: 'a',
 | 
			
		||||
            name: 'b',
 | 
			
		||||
@ -51,4 +121,53 @@ test('formatUpdateStrategyApiCode', () => {
 | 
			
		||||
        "constraints": []
 | 
			
		||||
      }'"
 | 
			
		||||
    `);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change general settings', async () => {
 | 
			
		||||
        const { expectedGroupId, expectedSliderValue, wrapper } =
 | 
			
		||||
            setupComponent();
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            expect(screen.getByText('Gradual rollout')).toBeInTheDocument();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        const groupIdInput = await screen.getByLabelText('groupId');
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue('50');
 | 
			
		||||
        expect(groupIdInput).toHaveValue(featureName);
 | 
			
		||||
        const defaultStickiness = await screen.findByText('default');
 | 
			
		||||
        userEvent.click(defaultStickiness);
 | 
			
		||||
        const randomStickiness = await screen.findByText('random');
 | 
			
		||||
        userEvent.click(randomStickiness);
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(slider, { target: { value: expectedSliderValue } });
 | 
			
		||||
        fireEvent.change(groupIdInput, { target: { value: expectedGroupId } });
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue(expectedSliderValue);
 | 
			
		||||
        expect(groupIdInput).toHaveValue(expectedGroupId);
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            const codeSnippet = document.querySelector('pre')?.innerHTML;
 | 
			
		||||
            const count = (codeSnippet!.match(/random/g) || []).length;
 | 
			
		||||
            // strategy stickiness and variant stickiness
 | 
			
		||||
            expect(count).toBe(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should not change variant names', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            expect(screen.getByText('Gradual rollout')).toBeInTheDocument();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedVariantName)).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        expect(inputElement).toBeDisabled();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { FeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
 | 
			
		||||
@ -16,7 +15,6 @@ import {
 | 
			
		||||
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { formatStrategyName } from 'utils/strategyNames';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
 | 
			
		||||
import { sortStrategyParameters } from 'utils/sortStrategyParameters';
 | 
			
		||||
@ -28,6 +26,10 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
import { constraintId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import { useScheduledChangeRequestsWithStrategy } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
 | 
			
		||||
import {
 | 
			
		||||
    getChangeRequestConflictCreatedData,
 | 
			
		||||
@ -80,11 +82,20 @@ const useTitleTracking = () => {
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addIdSymbolToConstraints = (strategy?: IFeatureStrategy) => {
 | 
			
		||||
    if (!strategy) return;
 | 
			
		||||
 | 
			
		||||
    return strategy?.constraints.map((constraint) => {
 | 
			
		||||
        return { ...constraint, [constraintId]: uuidv4() };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FeatureStrategyEdit = () => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const environmentId = useRequiredQueryParam('environmentId');
 | 
			
		||||
    const strategyId = useRequiredQueryParam('strategyId');
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
 | 
			
		||||
    const [segments, setSegments] = useState<ISegment[]>([]);
 | 
			
		||||
@ -168,7 +179,15 @@ export const FeatureStrategyEdit = () => {
 | 
			
		||||
        const savedStrategy = data?.environments
 | 
			
		||||
            .flatMap((environment) => environment.strategies)
 | 
			
		||||
            .find((strategy) => strategy.id === strategyId);
 | 
			
		||||
        setStrategy((prev) => ({ ...prev, ...savedStrategy }));
 | 
			
		||||
 | 
			
		||||
        const constraintsWithId = addIdSymbolToConstraints(savedStrategy);
 | 
			
		||||
 | 
			
		||||
        const formattedStrategy = {
 | 
			
		||||
            ...savedStrategy,
 | 
			
		||||
            constraints: constraintsWithId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        setStrategy((prev) => ({ ...prev, ...formattedStrategy }));
 | 
			
		||||
        setPreviousTitle(savedStrategy?.title || '');
 | 
			
		||||
    }, [strategyId, data]);
 | 
			
		||||
 | 
			
		||||
@ -235,7 +254,7 @@ export const FeatureStrategyEdit = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            modal
 | 
			
		||||
            title={formatStrategyName(strategy.name ?? '')}
 | 
			
		||||
            disablePadding
 | 
			
		||||
            description={featureStrategyHelp}
 | 
			
		||||
            documentationLink={featureStrategyDocsLink}
 | 
			
		||||
            documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
@ -264,6 +283,16 @@ export const FeatureStrategyEdit = () => {
 | 
			
		||||
                permission={UPDATE_FEATURE_STRATEGY}
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                isChangeRequest={isChangeRequestConfigured(environmentId)}
 | 
			
		||||
                tab={tab}
 | 
			
		||||
                setTab={setTab}
 | 
			
		||||
                StrategyVariants={
 | 
			
		||||
                    <NewStrategyVariants
 | 
			
		||||
                        strategy={strategy}
 | 
			
		||||
                        setStrategy={setStrategy}
 | 
			
		||||
                        environment={environmentId}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            {staleDataNotification}
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,15 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { Alert, Button, styled } from '@mui/material';
 | 
			
		||||
import {
 | 
			
		||||
    Alert,
 | 
			
		||||
    Button,
 | 
			
		||||
    styled,
 | 
			
		||||
    Tabs,
 | 
			
		||||
    Tab,
 | 
			
		||||
    Box,
 | 
			
		||||
    Divider,
 | 
			
		||||
    Typography,
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import {
 | 
			
		||||
    IFeatureStrategy,
 | 
			
		||||
    IFeatureStrategyParameters,
 | 
			
		||||
@ -31,8 +40,12 @@ import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequ
 | 
			
		||||
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
 | 
			
		||||
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
 | 
			
		||||
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
 | 
			
		||||
import { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { formatStrategyName } from 'utils/strategyNames';
 | 
			
		||||
import { Badge } from 'component/common/Badge/Badge';
 | 
			
		||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
 | 
			
		||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
 | 
			
		||||
interface IFeatureStrategyFormProps {
 | 
			
		||||
    feature: IFeatureToggle;
 | 
			
		||||
@ -42,7 +55,7 @@ interface IFeatureStrategyFormProps {
 | 
			
		||||
    onSubmit: () => void;
 | 
			
		||||
    onCancel?: () => void;
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
    isChangeRequest?: boolean;
 | 
			
		||||
    isChangeRequest: boolean;
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>;
 | 
			
		||||
    setStrategy: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<Partial<IFeatureStrategy>>
 | 
			
		||||
@ -50,28 +63,131 @@ interface IFeatureStrategyFormProps {
 | 
			
		||||
    segments: ISegment[];
 | 
			
		||||
    setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
 | 
			
		||||
    errors: IFormErrors;
 | 
			
		||||
    tab: number;
 | 
			
		||||
    setTab: React.Dispatch<React.SetStateAction<number>>;
 | 
			
		||||
    StrategyVariants: JSX.Element;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    display: 'grid',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
const StyledDividerContent = styled(Box)(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(0.75, 1),
 | 
			
		||||
    color: theme.palette.text.primary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallerBody,
 | 
			
		||||
    backgroundColor: theme.palette.background.elevation2,
 | 
			
		||||
    borderRadius: theme.shape.borderRadius,
 | 
			
		||||
    width: '45px',
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    top: '-10px',
 | 
			
		||||
    left: 'calc(50% - 45px)',
 | 
			
		||||
    lineHeight: 1,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHr = styled('hr')(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '1px',
 | 
			
		||||
    margin: theme.spacing(2, 0),
 | 
			
		||||
    border: 'none',
 | 
			
		||||
    background: theme.palette.background.elevation2,
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    padding: theme.spacing(6),
 | 
			
		||||
    paddingBottom: theme.spacing(12),
 | 
			
		||||
    paddingTop: theme.spacing(4),
 | 
			
		||||
    overflow: 'auto',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTitle = styled('h1')(({ theme }) => ({
 | 
			
		||||
    fontWeight: 'normal',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    paddingTop: theme.spacing(2),
 | 
			
		||||
    paddingBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledButtons = styled('div')(({ theme }) => ({
 | 
			
		||||
    bottom: 0,
 | 
			
		||||
    right: 0,
 | 
			
		||||
    left: 0,
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    padding: theme.spacing(3),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    backgroundColor: theme.palette.background.paper,
 | 
			
		||||
    justifyContent: 'end',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    paddingBottom: theme.spacing(10),
 | 
			
		||||
    borderTop: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
			
		||||
    borderTop: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
    borderBottom: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    minHeight: '60px',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    marginTop: theme.spacing(3.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDivider = styled(Divider)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTargetingHeader = styled('div')(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
    marginTop: theme.spacing(1.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    paddingTop: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledAlertBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    '& > *': {
 | 
			
		||||
        marginTop: theme.spacing(2),
 | 
			
		||||
        marginBottom: theme.spacing(2),
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledEnvironmentBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const EnvironmentIconBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    transform: 'scale(0.9)',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const EnvironmentTypography = styled(Typography)<{ enabled: boolean }>(
 | 
			
		||||
    ({ theme, enabled }) => ({
 | 
			
		||||
        fontWeight: enabled ? 'bold' : 'normal',
 | 
			
		||||
    }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({
 | 
			
		||||
    marginRight: theme.spacing(0.5),
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTab = styled(Tab)(({ theme }) => ({
 | 
			
		||||
    width: '100px',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledBadge = styled(Badge)(({ theme }) => ({
 | 
			
		||||
    marginLeft: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const feedbackCategory = 'newStrategyForm';
 | 
			
		||||
 | 
			
		||||
export const FeatureStrategyForm = ({
 | 
			
		||||
    projectId,
 | 
			
		||||
    feature,
 | 
			
		||||
@ -86,7 +202,14 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
    setSegments,
 | 
			
		||||
    errors,
 | 
			
		||||
    isChangeRequest,
 | 
			
		||||
    tab,
 | 
			
		||||
    setTab,
 | 
			
		||||
    StrategyVariants,
 | 
			
		||||
}: IFeatureStrategyFormProps) => {
 | 
			
		||||
    const { openFeedback, hasSubmittedFeedback } = useFeedback(
 | 
			
		||||
        feedbackCategory,
 | 
			
		||||
        'manual',
 | 
			
		||||
    );
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
    const [showProdGuard, setShowProdGuard] = useState(false);
 | 
			
		||||
    const hasValidConstraints = useConstraintsValidation(strategy.constraints);
 | 
			
		||||
@ -97,6 +220,39 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
        environmentId,
 | 
			
		||||
    );
 | 
			
		||||
    const { strategyDefinition } = useStrategy(strategy?.name);
 | 
			
		||||
    const newStrategyConfigurationFeedback = useUiFlag(
 | 
			
		||||
        'newStrategyConfigurationFeedback',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        trackEvent('new-strategy-form', {
 | 
			
		||||
            props: {
 | 
			
		||||
                eventType: 'seen',
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const stickiness =
 | 
			
		||||
        strategy?.parameters && 'stickiness' in strategy?.parameters
 | 
			
		||||
            ? String(strategy.parameters.stickiness)
 | 
			
		||||
            : 'default';
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setStrategy((prev) => ({
 | 
			
		||||
            ...prev,
 | 
			
		||||
            variants: (strategy.variants || []).map((variant) => ({
 | 
			
		||||
                stickiness,
 | 
			
		||||
                name: variant.name,
 | 
			
		||||
                weight: variant.weight,
 | 
			
		||||
                payload: variant.payload,
 | 
			
		||||
                weightType: variant.weightType,
 | 
			
		||||
            })),
 | 
			
		||||
        }));
 | 
			
		||||
    }, [stickiness, JSON.stringify(strategy.variants)]);
 | 
			
		||||
 | 
			
		||||
    const foundEnvironment = feature.environments.find(
 | 
			
		||||
        (environment) => environment.name === environmentId,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { data } = usePendingChangeRequests(feature.project);
 | 
			
		||||
    const { changeRequestInReviewOrApproved, alert } =
 | 
			
		||||
@ -111,11 +267,7 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        uiConfig,
 | 
			
		||||
        error: uiConfigError,
 | 
			
		||||
        loading: uiConfigLoading,
 | 
			
		||||
    } = useUiConfig();
 | 
			
		||||
    const { error: uiConfigError, loading: uiConfigLoading } = useUiConfig();
 | 
			
		||||
 | 
			
		||||
    if (uiConfigError) {
 | 
			
		||||
        throw uiConfigError;
 | 
			
		||||
@ -159,6 +311,15 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
        navigate(formatFeaturePath(feature.project, feature.name));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const createFeedbackContext = () => {
 | 
			
		||||
        openFeedback({
 | 
			
		||||
            title: 'How easy was it to work with the new strategy form?',
 | 
			
		||||
            positiveLabel: 'What do you like most about the new strategy form?',
 | 
			
		||||
            areasForImprovementsLabel:
 | 
			
		||||
                'What should be improved the new strategy form?',
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmitWithValidation = async (event: React.FormEvent) => {
 | 
			
		||||
        if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) {
 | 
			
		||||
            trackEvent('strategy-variants', {
 | 
			
		||||
@ -172,21 +333,85 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        trackEvent('new-strategy-form', {
 | 
			
		||||
            props: {
 | 
			
		||||
                eventType: 'submitted',
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (enableProdGuard && !isChangeRequest) {
 | 
			
		||||
            setShowProdGuard(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            onSubmit();
 | 
			
		||||
            await onSubmitWithFeedback();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmitWithFeedback = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await onSubmit();
 | 
			
		||||
 | 
			
		||||
            if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) {
 | 
			
		||||
                createFeedbackContext();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
 | 
			
		||||
        setTab(newValue);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getTargetingCount = () => {
 | 
			
		||||
        const constraintCount = strategy.constraints?.length || 0;
 | 
			
		||||
        const segmentCount = segments.length || 0;
 | 
			
		||||
 | 
			
		||||
        return constraintCount + segmentCount;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showVariants =
 | 
			
		||||
        strategy.parameters && 'stickiness' in strategy.parameters;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledForm onSubmit={onSubmitWithValidation}>
 | 
			
		||||
        <>
 | 
			
		||||
            <StyledHeaderBox>
 | 
			
		||||
                <StyledTitle>
 | 
			
		||||
                    {formatStrategyName(strategy.name || '')}
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={strategy.name === 'flexibleRollout'}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <Badge color='success' sx={{ marginLeft: '1rem' }}>
 | 
			
		||||
                                {strategy.parameters?.rollout}%
 | 
			
		||||
                            </Badge>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledTitle>
 | 
			
		||||
                {foundEnvironment ? (
 | 
			
		||||
                    <StyledEnvironmentBox>
 | 
			
		||||
                        <EnvironmentTypographyHeader>
 | 
			
		||||
                            Environment:
 | 
			
		||||
                        </EnvironmentTypographyHeader>
 | 
			
		||||
                        <EnvironmentIconBox>
 | 
			
		||||
                            <EnvironmentIcon
 | 
			
		||||
                                enabled={foundEnvironment.enabled}
 | 
			
		||||
                            />{' '}
 | 
			
		||||
                            <EnvironmentTypography
 | 
			
		||||
                                enabled={foundEnvironment.enabled}
 | 
			
		||||
                            >
 | 
			
		||||
                                {foundEnvironment.name}
 | 
			
		||||
                            </EnvironmentTypography>
 | 
			
		||||
                        </EnvironmentIconBox>
 | 
			
		||||
                    </StyledEnvironmentBox>
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </StyledHeaderBox>
 | 
			
		||||
 | 
			
		||||
            <StyledAlertBox>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasChangeRequestInReviewForEnvironment}
 | 
			
		||||
                    show={alert}
 | 
			
		||||
                    elseShow={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                        condition={Boolean(isChangeRequest)}
 | 
			
		||||
                            condition={isChangeRequest}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <FeatureStrategyChangeRequestAlert
 | 
			
		||||
                                    environment={environmentId}
 | 
			
		||||
@ -205,22 +430,54 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
                        show={
 | 
			
		||||
                            <Alert severity='success'>
 | 
			
		||||
                                This feature toggle is currently enabled in the{' '}
 | 
			
		||||
                            <strong>{environmentId}</strong> environment. Any
 | 
			
		||||
                            changes made here will be available to users as soon
 | 
			
		||||
                            as these changes are approved and applied.
 | 
			
		||||
                                <strong>{environmentId}</strong> environment.
 | 
			
		||||
                                Any changes made here will be available to users
 | 
			
		||||
                                as soon as these changes are approved and
 | 
			
		||||
                                applied.
 | 
			
		||||
                            </Alert>
 | 
			
		||||
                        }
 | 
			
		||||
                        elseShow={
 | 
			
		||||
                            <Alert severity='success'>
 | 
			
		||||
                                This feature toggle is currently enabled in the{' '}
 | 
			
		||||
                            <strong>{environmentId}</strong> environment. Any
 | 
			
		||||
                            changes made here will be available to users as soon
 | 
			
		||||
                            as you hit <strong>save</strong>.
 | 
			
		||||
                                <strong>{environmentId}</strong> environment.
 | 
			
		||||
                                Any changes made here will be available to users
 | 
			
		||||
                                as soon as you hit <strong>save</strong>.
 | 
			
		||||
                            </Alert>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </FeatureStrategyEnabled>
 | 
			
		||||
            <StyledHr />
 | 
			
		||||
            </StyledAlertBox>
 | 
			
		||||
 | 
			
		||||
            <StyledTabs value={tab} onChange={handleChange}>
 | 
			
		||||
                <StyledTab label='General' />
 | 
			
		||||
                <Tab
 | 
			
		||||
                    data-testid='STRATEGY_TARGETING_TAB'
 | 
			
		||||
                    label={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                            Targeting
 | 
			
		||||
                            <StyledBadge>{getTargetingCount()}</StyledBadge>
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                {showVariants && (
 | 
			
		||||
                    <Tab
 | 
			
		||||
                        data-testid='STRATEGY_VARIANTS_TAB'
 | 
			
		||||
                        label={
 | 
			
		||||
                            <Typography>
 | 
			
		||||
                                Variants
 | 
			
		||||
                                <StyledBadge>
 | 
			
		||||
                                    {strategy.variants?.length || 0}
 | 
			
		||||
                                </StyledBadge>
 | 
			
		||||
                            </Typography>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </StyledTabs>
 | 
			
		||||
            <StyledForm onSubmit={onSubmitWithValidation}>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 0}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <FeatureStrategyTitle
 | 
			
		||||
                                title={strategy.title || ''}
 | 
			
		||||
                                setTitle={(title) => {
 | 
			
		||||
@ -230,42 +487,7 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
                                    }));
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
            <FeatureStrategySegment
 | 
			
		||||
                segments={segments}
 | 
			
		||||
                setSegments={setSegments}
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
            />
 | 
			
		||||
            <FeatureStrategyConstraints
 | 
			
		||||
                projectId={feature.project}
 | 
			
		||||
                environmentId={environmentId}
 | 
			
		||||
                strategy={strategy}
 | 
			
		||||
                setStrategy={setStrategy}
 | 
			
		||||
            />
 | 
			
		||||
            <StyledHr />
 | 
			
		||||
            <FeatureStrategyType
 | 
			
		||||
                strategy={strategy}
 | 
			
		||||
                strategyDefinition={strategyDefinition}
 | 
			
		||||
                setStrategy={setStrategy}
 | 
			
		||||
                validateParameter={validateParameter}
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                hasAccess={access}
 | 
			
		||||
            />
 | 
			
		||||
            <StyledHr />
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={
 | 
			
		||||
                    strategy.parameters != null &&
 | 
			
		||||
                    'stickiness' in strategy.parameters
 | 
			
		||||
                }
 | 
			
		||||
                show={
 | 
			
		||||
                    <StrategyVariants
 | 
			
		||||
                        strategy={strategy}
 | 
			
		||||
                        setStrategy={setStrategy}
 | 
			
		||||
                        environment={environmentId}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            <StyledHr />
 | 
			
		||||
 | 
			
		||||
                            <FeatureStrategyEnabledDisabled
 | 
			
		||||
                                enabled={!strategy?.disabled}
 | 
			
		||||
                                onToggleEnabled={() =>
 | 
			
		||||
@ -275,7 +497,62 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
                                    }))
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
            <StyledHr />
 | 
			
		||||
 | 
			
		||||
                            <FeatureStrategyType
 | 
			
		||||
                                strategy={strategy}
 | 
			
		||||
                                strategyDefinition={strategyDefinition}
 | 
			
		||||
                                setStrategy={setStrategy}
 | 
			
		||||
                                validateParameter={validateParameter}
 | 
			
		||||
                                errors={errors}
 | 
			
		||||
                                hasAccess={access}
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 1}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <StyledTargetingHeader>
 | 
			
		||||
                                Segmentation and constraints allow you to set
 | 
			
		||||
                                filters on your strategies, so that they will
 | 
			
		||||
                                only be evaluated for users and applications
 | 
			
		||||
                                that match the specified preconditions.
 | 
			
		||||
                            </StyledTargetingHeader>
 | 
			
		||||
                            <FeatureStrategySegment
 | 
			
		||||
                                segments={segments}
 | 
			
		||||
                                setSegments={setSegments}
 | 
			
		||||
                                projectId={projectId}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <StyledBox>
 | 
			
		||||
                                <StyledDivider />
 | 
			
		||||
                                <StyledDividerContent>AND</StyledDividerContent>
 | 
			
		||||
                            </StyledBox>
 | 
			
		||||
                            <FeatureStrategyConstraints
 | 
			
		||||
                                projectId={feature.project}
 | 
			
		||||
                                environmentId={environmentId}
 | 
			
		||||
                                strategy={strategy}
 | 
			
		||||
                                setStrategy={setStrategy}
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 2}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={
 | 
			
		||||
                                strategy.parameters != null &&
 | 
			
		||||
                                'stickiness' in strategy.parameters
 | 
			
		||||
                            }
 | 
			
		||||
                            show={StrategyVariants}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <StyledButtons>
 | 
			
		||||
                    <PermissionButton
 | 
			
		||||
                        permission={permission}
 | 
			
		||||
@ -306,11 +583,12 @@ export const FeatureStrategyForm = ({
 | 
			
		||||
                    <FeatureStrategyProdGuard
 | 
			
		||||
                        open={showProdGuard}
 | 
			
		||||
                        onClose={() => setShowProdGuard(false)}
 | 
			
		||||
                    onClick={onSubmit}
 | 
			
		||||
                        onClick={onSubmitWithFeedback}
 | 
			
		||||
                        loading={loading}
 | 
			
		||||
                        label='Save strategy'
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledButtons>
 | 
			
		||||
            </StyledForm>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,594 +0,0 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import {
 | 
			
		||||
    Alert,
 | 
			
		||||
    Button,
 | 
			
		||||
    styled,
 | 
			
		||||
    Tabs,
 | 
			
		||||
    Tab,
 | 
			
		||||
    Box,
 | 
			
		||||
    Divider,
 | 
			
		||||
    Typography,
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import {
 | 
			
		||||
    IFeatureStrategy,
 | 
			
		||||
    IFeatureStrategyParameters,
 | 
			
		||||
    IStrategyParameter,
 | 
			
		||||
} from 'interfaces/strategy';
 | 
			
		||||
import { FeatureStrategyType } from '../FeatureStrategyType/FeatureStrategyType';
 | 
			
		||||
import { FeatureStrategyEnabled } from './FeatureStrategyEnabled/FeatureStrategyEnabled';
 | 
			
		||||
import { FeatureStrategyConstraints } from '../FeatureStrategyConstraints/FeatureStrategyConstraints';
 | 
			
		||||
import { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
 | 
			
		||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
 | 
			
		||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { IFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { validateParameterValue } from 'utils/validateParameterValue';
 | 
			
		||||
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
 | 
			
		||||
import { FeatureStrategyChangeRequestAlert } from './FeatureStrategyChangeRequestAlert/FeatureStrategyChangeRequestAlert';
 | 
			
		||||
import {
 | 
			
		||||
    FeatureStrategyProdGuard,
 | 
			
		||||
    useFeatureStrategyProdGuard,
 | 
			
		||||
} from '../FeatureStrategyProdGuard/FeatureStrategyProdGuard';
 | 
			
		||||
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewWarning';
 | 
			
		||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
			
		||||
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
 | 
			
		||||
import { FeatureStrategyTitle } from './FeatureStrategyTitle/FeatureStrategyTitle';
 | 
			
		||||
import { FeatureStrategyEnabledDisabled } from './FeatureStrategyEnabledDisabled/FeatureStrategyEnabledDisabled';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { formatStrategyName } from 'utils/strategyNames';
 | 
			
		||||
import { Badge } from 'component/common/Badge/Badge';
 | 
			
		||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
 | 
			
		||||
import { useFeedback } from 'component/feedbackNew/useFeedback';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
 | 
			
		||||
interface IFeatureStrategyFormProps {
 | 
			
		||||
    feature: IFeatureToggle;
 | 
			
		||||
    projectId: string;
 | 
			
		||||
    environmentId: string;
 | 
			
		||||
    permission: string;
 | 
			
		||||
    onSubmit: () => void;
 | 
			
		||||
    onCancel?: () => void;
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
    isChangeRequest: boolean;
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>;
 | 
			
		||||
    setStrategy: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<Partial<IFeatureStrategy>>
 | 
			
		||||
    >;
 | 
			
		||||
    segments: ISegment[];
 | 
			
		||||
    setSegments: React.Dispatch<React.SetStateAction<ISegment[]>>;
 | 
			
		||||
    errors: IFormErrors;
 | 
			
		||||
    tab: number;
 | 
			
		||||
    setTab: React.Dispatch<React.SetStateAction<number>>;
 | 
			
		||||
    StrategyVariants: JSX.Element;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledDividerContent = styled(Box)(({ theme }) => ({
 | 
			
		||||
    padding: theme.spacing(0.75, 1),
 | 
			
		||||
    color: theme.palette.text.primary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallerBody,
 | 
			
		||||
    backgroundColor: theme.palette.background.elevation2,
 | 
			
		||||
    borderRadius: theme.shape.borderRadius,
 | 
			
		||||
    width: '45px',
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    top: '-10px',
 | 
			
		||||
    left: 'calc(50% - 45px)',
 | 
			
		||||
    lineHeight: 1,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
    padding: theme.spacing(6),
 | 
			
		||||
    paddingBottom: theme.spacing(12),
 | 
			
		||||
    paddingTop: theme.spacing(4),
 | 
			
		||||
    overflow: 'auto',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTitle = styled('h1')(({ theme }) => ({
 | 
			
		||||
    fontWeight: 'normal',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    paddingTop: theme.spacing(2),
 | 
			
		||||
    paddingBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledButtons = styled('div')(({ theme }) => ({
 | 
			
		||||
    bottom: 0,
 | 
			
		||||
    right: 0,
 | 
			
		||||
    left: 0,
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    padding: theme.spacing(3),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    backgroundColor: theme.palette.background.paper,
 | 
			
		||||
    justifyContent: 'end',
 | 
			
		||||
    borderTop: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
			
		||||
    borderTop: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
    borderBottom: `1px solid ${theme.palette.divider}`,
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    minHeight: '60px',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    marginTop: theme.spacing(3.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDivider = styled(Divider)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTargetingHeader = styled('div')(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
    marginTop: theme.spacing(1.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledHeaderBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    paddingTop: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledAlertBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    paddingLeft: theme.spacing(6),
 | 
			
		||||
    paddingRight: theme.spacing(6),
 | 
			
		||||
    '& > *': {
 | 
			
		||||
        marginTop: theme.spacing(2),
 | 
			
		||||
        marginBottom: theme.spacing(2),
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledEnvironmentBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const EnvironmentIconBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    transform: 'scale(0.9)',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const EnvironmentTypography = styled(Typography)<{ enabled: boolean }>(
 | 
			
		||||
    ({ theme, enabled }) => ({
 | 
			
		||||
        fontWeight: enabled ? 'bold' : 'normal',
 | 
			
		||||
    }),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const EnvironmentTypographyHeader = styled(Typography)(({ theme }) => ({
 | 
			
		||||
    marginRight: theme.spacing(0.5),
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTab = styled(Tab)(({ theme }) => ({
 | 
			
		||||
    width: '100px',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledBadge = styled(Badge)(({ theme }) => ({
 | 
			
		||||
    marginLeft: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const feedbackCategory = 'newStrategyForm';
 | 
			
		||||
 | 
			
		||||
export const NewFeatureStrategyForm = ({
 | 
			
		||||
    projectId,
 | 
			
		||||
    feature,
 | 
			
		||||
    environmentId,
 | 
			
		||||
    permission,
 | 
			
		||||
    onSubmit,
 | 
			
		||||
    onCancel,
 | 
			
		||||
    loading,
 | 
			
		||||
    strategy,
 | 
			
		||||
    setStrategy,
 | 
			
		||||
    segments,
 | 
			
		||||
    setSegments,
 | 
			
		||||
    errors,
 | 
			
		||||
    isChangeRequest,
 | 
			
		||||
    tab,
 | 
			
		||||
    setTab,
 | 
			
		||||
    StrategyVariants,
 | 
			
		||||
}: IFeatureStrategyFormProps) => {
 | 
			
		||||
    const { openFeedback, hasSubmittedFeedback } = useFeedback(
 | 
			
		||||
        feedbackCategory,
 | 
			
		||||
        'manual',
 | 
			
		||||
    );
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
    const [showProdGuard, setShowProdGuard] = useState(false);
 | 
			
		||||
    const hasValidConstraints = useConstraintsValidation(strategy.constraints);
 | 
			
		||||
    const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
 | 
			
		||||
    const access = useHasProjectEnvironmentAccess(
 | 
			
		||||
        permission,
 | 
			
		||||
        projectId,
 | 
			
		||||
        environmentId,
 | 
			
		||||
    );
 | 
			
		||||
    const { strategyDefinition } = useStrategy(strategy?.name);
 | 
			
		||||
    const newStrategyConfigurationFeedback = useUiFlag(
 | 
			
		||||
        'newStrategyConfigurationFeedback',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        trackEvent('new-strategy-form', {
 | 
			
		||||
            props: {
 | 
			
		||||
                eventType: 'seen',
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const stickiness =
 | 
			
		||||
        strategy?.parameters && 'stickiness' in strategy?.parameters
 | 
			
		||||
            ? String(strategy.parameters.stickiness)
 | 
			
		||||
            : 'default';
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setStrategy((prev) => ({
 | 
			
		||||
            ...prev,
 | 
			
		||||
            variants: (strategy.variants || []).map((variant) => ({
 | 
			
		||||
                stickiness,
 | 
			
		||||
                name: variant.name,
 | 
			
		||||
                weight: variant.weight,
 | 
			
		||||
                payload: variant.payload,
 | 
			
		||||
                weightType: variant.weightType,
 | 
			
		||||
            })),
 | 
			
		||||
        }));
 | 
			
		||||
    }, [stickiness, JSON.stringify(strategy.variants)]);
 | 
			
		||||
 | 
			
		||||
    const foundEnvironment = feature.environments.find(
 | 
			
		||||
        (environment) => environment.name === environmentId,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { data } = usePendingChangeRequests(feature.project);
 | 
			
		||||
    const { changeRequestInReviewOrApproved, alert } =
 | 
			
		||||
        useChangeRequestInReviewWarning(data);
 | 
			
		||||
 | 
			
		||||
    const hasChangeRequestInReviewForEnvironment =
 | 
			
		||||
        changeRequestInReviewOrApproved(environmentId || '');
 | 
			
		||||
 | 
			
		||||
    const changeRequestButtonText = hasChangeRequestInReviewForEnvironment
 | 
			
		||||
        ? 'Add to existing change request'
 | 
			
		||||
        : 'Add change to draft';
 | 
			
		||||
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
    const { error: uiConfigError, loading: uiConfigLoading } = useUiConfig();
 | 
			
		||||
 | 
			
		||||
    if (uiConfigError) {
 | 
			
		||||
        throw uiConfigError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (uiConfigLoading || !strategyDefinition) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const findParameterDefinition = (name: string): IStrategyParameter => {
 | 
			
		||||
        return strategyDefinition.parameters.find((parameterDefinition) => {
 | 
			
		||||
            return parameterDefinition.name === name;
 | 
			
		||||
        })!;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateParameter = (
 | 
			
		||||
        name: string,
 | 
			
		||||
        value: IFeatureStrategyParameters[string],
 | 
			
		||||
    ): boolean => {
 | 
			
		||||
        const parameterValueError = validateParameterValue(
 | 
			
		||||
            findParameterDefinition(name),
 | 
			
		||||
            value,
 | 
			
		||||
        );
 | 
			
		||||
        if (parameterValueError) {
 | 
			
		||||
            errors.setFormError(name, parameterValueError);
 | 
			
		||||
            return false;
 | 
			
		||||
        } else {
 | 
			
		||||
            errors.removeFormError(name);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateAllParameters = (): boolean => {
 | 
			
		||||
        return strategyDefinition.parameters
 | 
			
		||||
            .map((parameter) => parameter.name)
 | 
			
		||||
            .map((name) => validateParameter(name, strategy.parameters?.[name]))
 | 
			
		||||
            .every(Boolean);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onDefaultCancel = () => {
 | 
			
		||||
        navigate(formatFeaturePath(feature.project, feature.name));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const createFeedbackContext = () => {
 | 
			
		||||
        openFeedback({
 | 
			
		||||
            title: 'How easy was it to work with the new strategy form?',
 | 
			
		||||
            positiveLabel: 'What do you like most about the new strategy form?',
 | 
			
		||||
            areasForImprovementsLabel:
 | 
			
		||||
                'What should be improved the new strategy form?',
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmitWithValidation = async (event: React.FormEvent) => {
 | 
			
		||||
        if (Array.isArray(strategy.variants) && strategy.variants?.length > 0) {
 | 
			
		||||
            trackEvent('strategy-variants', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    eventType: 'submitted',
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
        if (!validateAllParameters()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        trackEvent('new-strategy-form', {
 | 
			
		||||
            props: {
 | 
			
		||||
                eventType: 'submitted',
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (enableProdGuard && !isChangeRequest) {
 | 
			
		||||
            setShowProdGuard(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            await onSubmitWithFeedback();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmitWithFeedback = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await onSubmit();
 | 
			
		||||
 | 
			
		||||
            if (newStrategyConfigurationFeedback && !hasSubmittedFeedback) {
 | 
			
		||||
                createFeedbackContext();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
 | 
			
		||||
        setTab(newValue);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getTargetingCount = () => {
 | 
			
		||||
        const constraintCount = strategy.constraints?.length || 0;
 | 
			
		||||
        const segmentCount = segments.length || 0;
 | 
			
		||||
 | 
			
		||||
        return constraintCount + segmentCount;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const showVariants =
 | 
			
		||||
        strategy.parameters && 'stickiness' in strategy.parameters;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <StyledHeaderBox>
 | 
			
		||||
                <StyledTitle>
 | 
			
		||||
                    {formatStrategyName(strategy.name || '')}
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={strategy.name === 'flexibleRollout'}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <Badge color='success' sx={{ marginLeft: '1rem' }}>
 | 
			
		||||
                                {strategy.parameters?.rollout}%
 | 
			
		||||
                            </Badge>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledTitle>
 | 
			
		||||
                {foundEnvironment ? (
 | 
			
		||||
                    <StyledEnvironmentBox>
 | 
			
		||||
                        <EnvironmentTypographyHeader>
 | 
			
		||||
                            Environment:
 | 
			
		||||
                        </EnvironmentTypographyHeader>
 | 
			
		||||
                        <EnvironmentIconBox>
 | 
			
		||||
                            <EnvironmentIcon
 | 
			
		||||
                                enabled={foundEnvironment.enabled}
 | 
			
		||||
                            />{' '}
 | 
			
		||||
                            <EnvironmentTypography
 | 
			
		||||
                                enabled={foundEnvironment.enabled}
 | 
			
		||||
                            >
 | 
			
		||||
                                {foundEnvironment.name}
 | 
			
		||||
                            </EnvironmentTypography>
 | 
			
		||||
                        </EnvironmentIconBox>
 | 
			
		||||
                    </StyledEnvironmentBox>
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </StyledHeaderBox>
 | 
			
		||||
 | 
			
		||||
            <StyledAlertBox>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasChangeRequestInReviewForEnvironment}
 | 
			
		||||
                    show={alert}
 | 
			
		||||
                    elseShow={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={isChangeRequest}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <FeatureStrategyChangeRequestAlert
 | 
			
		||||
                                    environment={environmentId}
 | 
			
		||||
                                />
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <FeatureStrategyEnabled
 | 
			
		||||
                    projectId={feature.project}
 | 
			
		||||
                    featureId={feature.name}
 | 
			
		||||
                    environmentId={environmentId}
 | 
			
		||||
                >
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={Boolean(isChangeRequest)}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <Alert severity='success'>
 | 
			
		||||
                                This feature toggle is currently enabled in the{' '}
 | 
			
		||||
                                <strong>{environmentId}</strong> environment.
 | 
			
		||||
                                Any changes made here will be available to users
 | 
			
		||||
                                as soon as these changes are approved and
 | 
			
		||||
                                applied.
 | 
			
		||||
                            </Alert>
 | 
			
		||||
                        }
 | 
			
		||||
                        elseShow={
 | 
			
		||||
                            <Alert severity='success'>
 | 
			
		||||
                                This feature toggle is currently enabled in the{' '}
 | 
			
		||||
                                <strong>{environmentId}</strong> environment.
 | 
			
		||||
                                Any changes made here will be available to users
 | 
			
		||||
                                as soon as you hit <strong>save</strong>.
 | 
			
		||||
                            </Alert>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </FeatureStrategyEnabled>
 | 
			
		||||
            </StyledAlertBox>
 | 
			
		||||
 | 
			
		||||
            <StyledTabs value={tab} onChange={handleChange}>
 | 
			
		||||
                <StyledTab label='General' />
 | 
			
		||||
                <Tab
 | 
			
		||||
                    data-testid='STRATEGY_TARGETING_TAB'
 | 
			
		||||
                    label={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                            Targeting
 | 
			
		||||
                            <StyledBadge>{getTargetingCount()}</StyledBadge>
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                {showVariants && (
 | 
			
		||||
                    <Tab
 | 
			
		||||
                        data-testid='STRATEGY_VARIANTS_TAB'
 | 
			
		||||
                        label={
 | 
			
		||||
                            <Typography>
 | 
			
		||||
                                Variants
 | 
			
		||||
                                <StyledBadge>
 | 
			
		||||
                                    {strategy.variants?.length || 0}
 | 
			
		||||
                                </StyledBadge>
 | 
			
		||||
                            </Typography>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </StyledTabs>
 | 
			
		||||
            <StyledForm onSubmit={onSubmitWithValidation}>
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 0}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <FeatureStrategyTitle
 | 
			
		||||
                                title={strategy.title || ''}
 | 
			
		||||
                                setTitle={(title) => {
 | 
			
		||||
                                    setStrategy((prev) => ({
 | 
			
		||||
                                        ...prev,
 | 
			
		||||
                                        title,
 | 
			
		||||
                                    }));
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <FeatureStrategyEnabledDisabled
 | 
			
		||||
                                enabled={!strategy?.disabled}
 | 
			
		||||
                                onToggleEnabled={() =>
 | 
			
		||||
                                    setStrategy((strategyState) => ({
 | 
			
		||||
                                        ...strategyState,
 | 
			
		||||
                                        disabled: !strategyState.disabled,
 | 
			
		||||
                                    }))
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <FeatureStrategyType
 | 
			
		||||
                                strategy={strategy}
 | 
			
		||||
                                strategyDefinition={strategyDefinition}
 | 
			
		||||
                                setStrategy={setStrategy}
 | 
			
		||||
                                validateParameter={validateParameter}
 | 
			
		||||
                                errors={errors}
 | 
			
		||||
                                hasAccess={access}
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 1}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <StyledTargetingHeader>
 | 
			
		||||
                                Segmentation and constraints allow you to set
 | 
			
		||||
                                filters on your strategies, so that they will
 | 
			
		||||
                                only be evaluated for users and applications
 | 
			
		||||
                                that match the specified preconditions.
 | 
			
		||||
                            </StyledTargetingHeader>
 | 
			
		||||
                            <FeatureStrategySegment
 | 
			
		||||
                                segments={segments}
 | 
			
		||||
                                setSegments={setSegments}
 | 
			
		||||
                                projectId={projectId}
 | 
			
		||||
                            />
 | 
			
		||||
 | 
			
		||||
                            <StyledBox>
 | 
			
		||||
                                <StyledDivider />
 | 
			
		||||
                                <StyledDividerContent>AND</StyledDividerContent>
 | 
			
		||||
                            </StyledBox>
 | 
			
		||||
                            <FeatureStrategyConstraints
 | 
			
		||||
                                projectId={feature.project}
 | 
			
		||||
                                environmentId={environmentId}
 | 
			
		||||
                                strategy={strategy}
 | 
			
		||||
                                setStrategy={setStrategy}
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={tab === 2}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={
 | 
			
		||||
                                strategy.parameters != null &&
 | 
			
		||||
                                'stickiness' in strategy.parameters
 | 
			
		||||
                            }
 | 
			
		||||
                            show={StrategyVariants}
 | 
			
		||||
                        />
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <StyledButtons>
 | 
			
		||||
                    <PermissionButton
 | 
			
		||||
                        permission={permission}
 | 
			
		||||
                        projectId={feature.project}
 | 
			
		||||
                        environmentId={environmentId}
 | 
			
		||||
                        variant='contained'
 | 
			
		||||
                        color='primary'
 | 
			
		||||
                        type='submit'
 | 
			
		||||
                        disabled={
 | 
			
		||||
                            loading ||
 | 
			
		||||
                            !hasValidConstraints ||
 | 
			
		||||
                            errors.hasFormErrors()
 | 
			
		||||
                        }
 | 
			
		||||
                        data-testid={STRATEGY_FORM_SUBMIT_ID}
 | 
			
		||||
                    >
 | 
			
		||||
                        {isChangeRequest
 | 
			
		||||
                            ? changeRequestButtonText
 | 
			
		||||
                            : 'Save strategy'}
 | 
			
		||||
                    </PermissionButton>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        type='button'
 | 
			
		||||
                        color='primary'
 | 
			
		||||
                        onClick={onCancel ? onCancel : onDefaultCancel}
 | 
			
		||||
                        disabled={loading}
 | 
			
		||||
                    >
 | 
			
		||||
                        Cancel
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <FeatureStrategyProdGuard
 | 
			
		||||
                        open={showProdGuard}
 | 
			
		||||
                        onClose={() => setShowProdGuard(false)}
 | 
			
		||||
                        onClick={onSubmitWithFeedback}
 | 
			
		||||
                        loading={loading}
 | 
			
		||||
                        label='Save strategy'
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledButtons>
 | 
			
		||||
            </StyledForm>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -1,447 +0,0 @@
 | 
			
		||||
import { formatAddStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
 | 
			
		||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { Route, Routes } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_FEATURE_STRATEGY,
 | 
			
		||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
    UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
} from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { NewFeatureStrategyCreate } from './NewFeatureStrategyCreate';
 | 
			
		||||
import {
 | 
			
		||||
    setupProjectEndpoint,
 | 
			
		||||
    setupSegmentsEndpoint,
 | 
			
		||||
    setupStrategyEndpoint,
 | 
			
		||||
    setupFeaturesEndpoint,
 | 
			
		||||
    setupUiConfigEndpoint,
 | 
			
		||||
    setupContextEndpoint,
 | 
			
		||||
} from './featureStrategyFormTestSetup';
 | 
			
		||||
 | 
			
		||||
const featureName = 'my-new-feature';
 | 
			
		||||
 | 
			
		||||
const setupComponent = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        wrapper: render(
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                    path={
 | 
			
		||||
                        '/projects/:projectId/features/:featureId/strategies/create'
 | 
			
		||||
                    }
 | 
			
		||||
                    element={<NewFeatureStrategyCreate />}
 | 
			
		||||
                />
 | 
			
		||||
            </Routes>,
 | 
			
		||||
            {
 | 
			
		||||
                route: `/projects/default/features/${featureName}/strategies/create?environmentId=development&strategyName=flexibleRollout&defaultStrategy=true`,
 | 
			
		||||
                permissions: [
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: CREATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        expectedSegmentName: 'test',
 | 
			
		||||
        expectedGroupId: 'newGroupId',
 | 
			
		||||
        expectedVariantName: 'Blue',
 | 
			
		||||
        expectedSliderValue: '50',
 | 
			
		||||
        expectedConstraintValue: 'new value',
 | 
			
		||||
        expectedMultipleValues: '1234,4141,51515',
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
    setupProjectEndpoint();
 | 
			
		||||
    setupSegmentsEndpoint();
 | 
			
		||||
    setupStrategyEndpoint();
 | 
			
		||||
    setupFeaturesEndpoint(featureName);
 | 
			
		||||
    setupUiConfigEndpoint();
 | 
			
		||||
    setupContextEndpoint();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('NewFeatureStrategyCreate', () => {
 | 
			
		||||
    test('formatAddStrategyApiCode', () => {
 | 
			
		||||
        expect(
 | 
			
		||||
            formatAddStrategyApiCode(
 | 
			
		||||
                'projectId',
 | 
			
		||||
                'featureId',
 | 
			
		||||
                'environmentId',
 | 
			
		||||
                { id: 'strategyId' },
 | 
			
		||||
                'unleashUrl',
 | 
			
		||||
            ),
 | 
			
		||||
        ).toMatchInlineSnapshot(`
 | 
			
		||||
          "curl --location --request POST 'unleashUrl/api/admin/projects/projectId/features/featureId/environments/environmentId/strategies' \\
 | 
			
		||||
              --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
              --header 'Content-Type: application/json' \\
 | 
			
		||||
              --data-raw '{
 | 
			
		||||
            "id": "strategyId"
 | 
			
		||||
          }'"
 | 
			
		||||
        `);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should navigate tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        expect(slider).toHaveValue('100');
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const segmentsEl = await screen.findByText('Segments');
 | 
			
		||||
        expect(segmentsEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        expect(addVariantEl).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change general settings', async () => {
 | 
			
		||||
        const { expectedGroupId, expectedSliderValue } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        const groupIdInput = await screen.getByLabelText('groupId');
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue('100');
 | 
			
		||||
        expect(groupIdInput).toHaveValue(featureName);
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(slider, { target: { value: expectedSliderValue } });
 | 
			
		||||
        fireEvent.change(groupIdInput, { target: { value: expectedGroupId } });
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue(expectedSliderValue);
 | 
			
		||||
        expect(groupIdInput).toHaveValue(expectedGroupId);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change targeting settings', async () => {
 | 
			
		||||
        const { expectedConstraintValue, expectedSegmentName } =
 | 
			
		||||
            setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedConstraintValue },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEl = screen.getByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEl);
 | 
			
		||||
 | 
			
		||||
        const doneEl = screen.getByText('Done');
 | 
			
		||||
        fireEvent.click(doneEl);
 | 
			
		||||
 | 
			
		||||
        const selectElement = screen.getByPlaceholderText('Select segments');
 | 
			
		||||
        fireEvent.mouseDown(selectElement);
 | 
			
		||||
 | 
			
		||||
        const optionElement = await screen.findByText(expectedSegmentName);
 | 
			
		||||
        fireEvent.click(optionElement);
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedSegmentName)).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(expectedConstraintValue)).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change variants settings', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedVariantName },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedVariantName)).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const generalSettingsEl = screen.getByText('General');
 | 
			
		||||
        fireEvent.click(generalSettingsEl);
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            const codeSnippet = document.querySelector('pre')?.innerHTML;
 | 
			
		||||
            const variantNameMatches = (
 | 
			
		||||
                codeSnippet!.match(new RegExp(expectedVariantName, 'g')) || []
 | 
			
		||||
            ).length;
 | 
			
		||||
            const metaDataMatches = (codeSnippet!.match(/isValid/g) || [])
 | 
			
		||||
                .length;
 | 
			
		||||
            expect(variantNameMatches).toBe(1);
 | 
			
		||||
            expect(metaDataMatches).toBe(0);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change variant name after changing tab', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedVariantName },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const targetingEl = await screen.findByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        expect(addConstraintEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
        const inputElement2 = screen.getAllByRole('textbox')[0];
 | 
			
		||||
 | 
			
		||||
        expect(inputElement2).not.toBeDisabled();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should remove empty variants when changing tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const addVariantEl = await screen.findByText('Add variant');
 | 
			
		||||
        fireEvent.click(addVariantEl);
 | 
			
		||||
 | 
			
		||||
        const variants = screen.queryAllByTestId('VARIANT');
 | 
			
		||||
        expect(variants.length).toBe(1);
 | 
			
		||||
 | 
			
		||||
        const targetingEl = await screen.findByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        expect(addConstraintEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        const variants2 = screen.queryAllByTestId('VARIANT');
 | 
			
		||||
        expect(variants2.length).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should autosave constraint settings when navigating between tabs', async () => {
 | 
			
		||||
        const { expectedMultipleValues } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
        fireEvent.change(inputElement, {
 | 
			
		||||
            target: { value: expectedMultipleValues },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEl = await screen.findByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEl);
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const values = expectedMultipleValues.split(',');
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(values[0])).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(values[1])).toBeInTheDocument();
 | 
			
		||||
        expect(screen.getByText(values[2])).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should update multiple constraints correctly', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElements = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements[0], {
 | 
			
		||||
            target: { value: '123' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[1], {
 | 
			
		||||
            target: { value: '456' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[2], {
 | 
			
		||||
            target: { value: '789' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEls = await screen.findAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls[0]);
 | 
			
		||||
        fireEvent.click(addValueEls[1]);
 | 
			
		||||
        fireEvent.click(addValueEls[2]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).toBeInTheDocument();
 | 
			
		||||
        const deleteBtns = await screen.findAllByTestId('CancelIcon');
 | 
			
		||||
        fireEvent.click(deleteBtns[0]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('456')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('789')).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should update multiple constraints with the correct react key', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputElements = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements[0], {
 | 
			
		||||
            target: { value: '123' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[1], {
 | 
			
		||||
            target: { value: '456' },
 | 
			
		||||
        });
 | 
			
		||||
        fireEvent.change(inputElements[2], {
 | 
			
		||||
            target: { value: '789' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addValueEls = await screen.findAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls[0]);
 | 
			
		||||
        fireEvent.click(addValueEls[1]);
 | 
			
		||||
        fireEvent.click(addValueEls[2]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const deleteBtns = screen.getAllByTestId('DELETE_CONSTRAINT_BUTTON');
 | 
			
		||||
        fireEvent.click(deleteBtns[0]);
 | 
			
		||||
 | 
			
		||||
        const inputElements2 = screen.getAllByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputElements2[0], {
 | 
			
		||||
            target: { value: '666' },
 | 
			
		||||
        });
 | 
			
		||||
        const addValueEls2 = screen.getAllByText('Add values');
 | 
			
		||||
        fireEvent.click(addValueEls2[0]);
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('123')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('456')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('789')).toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should undo changes made to constraints', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const inputEl = screen.getByPlaceholderText(
 | 
			
		||||
            'value1, value2, value3...',
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(inputEl, {
 | 
			
		||||
            target: { value: '6, 7, 8' },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const addBtn = await screen.findByText('Add values');
 | 
			
		||||
        addBtn.click();
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('6')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('7')).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('8')).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const undoBtn = await screen.findByTestId(
 | 
			
		||||
            'UNDO_CONSTRAINT_CHANGE_BUTTON',
 | 
			
		||||
        );
 | 
			
		||||
        undoBtn.click();
 | 
			
		||||
 | 
			
		||||
        expect(screen.queryByText('6')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('7')).not.toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('8')).not.toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Should remove constraint when no valid values are set and moving between tabs', async () => {
 | 
			
		||||
        setupComponent();
 | 
			
		||||
 | 
			
		||||
        const titleEl = await screen.findByText('Gradual rollout');
 | 
			
		||||
        expect(titleEl).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const targetingEl = screen.getByText('Targeting');
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const addConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
        fireEvent.click(addConstraintEl);
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
        fireEvent.click(targetingEl);
 | 
			
		||||
 | 
			
		||||
        const seconAddConstraintEl = await screen.findByText('Add constraint');
 | 
			
		||||
 | 
			
		||||
        expect(seconAddConstraintEl).toBeInTheDocument();
 | 
			
		||||
        expect(screen.queryByText('appName')).not.toBeInTheDocument();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -1,260 +0,0 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { IFeatureStrategy, IFeatureStrategyPayload } from 'interfaces/strategy';
 | 
			
		||||
import {
 | 
			
		||||
    createStrategyPayload,
 | 
			
		||||
    featureStrategyDocsLink,
 | 
			
		||||
    featureStrategyDocsLinkLabel,
 | 
			
		||||
    featureStrategyHelp,
 | 
			
		||||
    formatFeaturePath,
 | 
			
		||||
} from '../FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { createFeatureStrategy } from 'utils/createFeatureStrategy';
 | 
			
		||||
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
 | 
			
		||||
import { useCollaborateData } from 'hooks/useCollaborateData';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
import { comparisonModerator } from '../featureStrategy.utils';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import useQueryParams from 'hooks/useQueryParams';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { useDefaultStrategy } from '../../../project/Project/ProjectSettings/ProjectDefaultStrategySettings/ProjectEnvironment/ProjectEnvironmentDefaultStrategy/EditDefaultStrategy';
 | 
			
		||||
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
 | 
			
		||||
export const NewFeatureStrategyCreate = () => {
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const environmentId = useRequiredQueryParam('environmentId');
 | 
			
		||||
    const strategyName = useRequiredQueryParam('strategyName');
 | 
			
		||||
    const { strategy: defaultStrategy, defaultStrategyFallback } =
 | 
			
		||||
        useDefaultStrategy(projectId, environmentId);
 | 
			
		||||
    const shouldUseDefaultStrategy: boolean = JSON.parse(
 | 
			
		||||
        useQueryParams().get('defaultStrategy') || 'false',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { segments: allSegments } = useSegments();
 | 
			
		||||
    const strategySegments = (allSegments || []).filter((segment) => {
 | 
			
		||||
        return defaultStrategy?.segments?.includes(segment.id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
 | 
			
		||||
 | 
			
		||||
    const [segments, setSegments] = useState<ISegment[]>(
 | 
			
		||||
        shouldUseDefaultStrategy ? strategySegments : [],
 | 
			
		||||
    );
 | 
			
		||||
    const { strategyDefinition } = useStrategy(strategyName);
 | 
			
		||||
    const errors = useFormErrors();
 | 
			
		||||
 | 
			
		||||
    const { addStrategyToFeature, loading } = useFeatureStrategyApi();
 | 
			
		||||
    const { addChange } = useChangeRequestApi();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { unleashUrl } = uiConfig;
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
    const { feature, refetchFeature } = useFeature(projectId, featureId);
 | 
			
		||||
    const ref = useRef<IFeatureToggle>(feature);
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
			
		||||
    const { refetch: refetchChangeRequests } =
 | 
			
		||||
        usePendingChangeRequests(projectId);
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
 | 
			
		||||
    const { data, staleDataNotification, forceRefreshCache } =
 | 
			
		||||
        useCollaborateData<IFeatureToggle>(
 | 
			
		||||
            {
 | 
			
		||||
                unleashGetter: useFeature,
 | 
			
		||||
                params: [projectId, featureId],
 | 
			
		||||
                dataKey: 'feature',
 | 
			
		||||
                refetchFunctionKey: 'refetchFeature',
 | 
			
		||||
                options: {},
 | 
			
		||||
            },
 | 
			
		||||
            feature,
 | 
			
		||||
            {
 | 
			
		||||
                afterSubmitAction: refetchFeature,
 | 
			
		||||
            },
 | 
			
		||||
            comparisonModerator,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (ref.current.name === '' && feature.name) {
 | 
			
		||||
            forceRefreshCache(feature);
 | 
			
		||||
            ref.current = feature;
 | 
			
		||||
        }
 | 
			
		||||
    }, [feature.name]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (shouldUseDefaultStrategy) {
 | 
			
		||||
            const strategyTemplate = defaultStrategy || defaultStrategyFallback;
 | 
			
		||||
            if (strategyTemplate.parameters?.groupId === '' && featureId) {
 | 
			
		||||
                setStrategy({
 | 
			
		||||
                    ...strategyTemplate,
 | 
			
		||||
                    parameters: {
 | 
			
		||||
                        ...strategyTemplate.parameters,
 | 
			
		||||
                        groupId: featureId,
 | 
			
		||||
                    },
 | 
			
		||||
                } as any);
 | 
			
		||||
            } else {
 | 
			
		||||
                setStrategy(strategyTemplate as any);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (strategyDefinition) {
 | 
			
		||||
            setStrategy(createFeatureStrategy(featureId, strategyDefinition));
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        featureId,
 | 
			
		||||
        JSON.stringify(strategyDefinition),
 | 
			
		||||
        shouldUseDefaultStrategy,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const onAddStrategy = async (payload: IFeatureStrategyPayload) => {
 | 
			
		||||
        await addStrategyToFeature(
 | 
			
		||||
            projectId,
 | 
			
		||||
            featureId,
 | 
			
		||||
            environmentId,
 | 
			
		||||
            payload,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        setToastData({
 | 
			
		||||
            title: 'Strategy created',
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            confetti: true,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onStrategyRequestAdd = async (payload: IFeatureStrategyPayload) => {
 | 
			
		||||
        await addChange(projectId, environmentId, {
 | 
			
		||||
            action: 'addStrategy',
 | 
			
		||||
            feature: featureId,
 | 
			
		||||
            payload,
 | 
			
		||||
        });
 | 
			
		||||
        // FIXME: segments in change requests
 | 
			
		||||
        setToastData({
 | 
			
		||||
            title: 'Strategy added to draft',
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            confetti: true,
 | 
			
		||||
        });
 | 
			
		||||
        refetchChangeRequests();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const payload = createStrategyPayload(strategy, segments);
 | 
			
		||||
 | 
			
		||||
    const onSubmit = async () => {
 | 
			
		||||
        trackEvent('strategyTitle', {
 | 
			
		||||
            props: {
 | 
			
		||||
                hasTitle: Boolean(strategy.title),
 | 
			
		||||
                on: 'create',
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (isChangeRequestConfigured(environmentId)) {
 | 
			
		||||
                await onStrategyRequestAdd(payload);
 | 
			
		||||
            } else {
 | 
			
		||||
                await onAddStrategy(payload);
 | 
			
		||||
            }
 | 
			
		||||
            refetchFeature();
 | 
			
		||||
            navigate(formatFeaturePath(projectId, featureId));
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const emptyFeature = !data || !data.project;
 | 
			
		||||
 | 
			
		||||
    if (emptyFeature) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            modal
 | 
			
		||||
            description={featureStrategyHelp}
 | 
			
		||||
            documentationLink={featureStrategyDocsLink}
 | 
			
		||||
            documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
            disablePadding
 | 
			
		||||
            formatApiCode={() =>
 | 
			
		||||
                formatAddStrategyApiCode(
 | 
			
		||||
                    projectId,
 | 
			
		||||
                    featureId,
 | 
			
		||||
                    environmentId,
 | 
			
		||||
                    payload,
 | 
			
		||||
                    unleashUrl,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        >
 | 
			
		||||
            <NewFeatureStrategyForm
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                feature={data}
 | 
			
		||||
                strategy={strategy}
 | 
			
		||||
                setStrategy={setStrategy}
 | 
			
		||||
                segments={segments}
 | 
			
		||||
                setSegments={setSegments}
 | 
			
		||||
                environmentId={environmentId}
 | 
			
		||||
                onSubmit={onSubmit}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                permission={CREATE_FEATURE_STRATEGY}
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                isChangeRequest={isChangeRequestConfigured(environmentId)}
 | 
			
		||||
                tab={tab}
 | 
			
		||||
                setTab={setTab}
 | 
			
		||||
                StrategyVariants={
 | 
			
		||||
                    <NewStrategyVariants
 | 
			
		||||
                        strategy={strategy}
 | 
			
		||||
                        setStrategy={setStrategy}
 | 
			
		||||
                        environment={environmentId}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                        editable
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            {staleDataNotification}
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatCreateStrategyPath = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
    environmentId: string,
 | 
			
		||||
    strategyName: string,
 | 
			
		||||
    defaultStrategy: boolean = false,
 | 
			
		||||
): string => {
 | 
			
		||||
    const params = new URLSearchParams({
 | 
			
		||||
        environmentId,
 | 
			
		||||
        strategyName,
 | 
			
		||||
        defaultStrategy: String(defaultStrategy),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return `/projects/${projectId}/features/${featureId}/strategies/create?${params}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatAddStrategyApiCode = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
    environmentId: string,
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>,
 | 
			
		||||
    unleashUrl?: string,
 | 
			
		||||
): string => {
 | 
			
		||||
    if (!unleashUrl) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies`;
 | 
			
		||||
    const payload = JSON.stringify(strategy, undefined, 2);
 | 
			
		||||
 | 
			
		||||
    return `curl --location --request POST '${url}' \\
 | 
			
		||||
    --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
    --header 'Content-Type: application/json' \\
 | 
			
		||||
    --data-raw '${payload}'`;
 | 
			
		||||
};
 | 
			
		||||
@ -1,173 +0,0 @@
 | 
			
		||||
import { formatUpdateStrategyApiCode } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { IFeatureStrategy, IStrategy } from 'interfaces/strategy';
 | 
			
		||||
import { screen, waitFor, fireEvent } from '@testing-library/react';
 | 
			
		||||
import { render } from 'utils/testRenderer';
 | 
			
		||||
import { Route, Routes } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_FEATURE_STRATEGY,
 | 
			
		||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
    UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
} from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { NewFeatureStrategyEdit } from './NewFeatureStrategyEdit';
 | 
			
		||||
import {
 | 
			
		||||
    setupContextEndpoint,
 | 
			
		||||
    setupFeaturesEndpoint,
 | 
			
		||||
    setupProjectEndpoint,
 | 
			
		||||
    setupSegmentsEndpoint,
 | 
			
		||||
    setupStrategyEndpoint,
 | 
			
		||||
    setupUiConfigEndpoint,
 | 
			
		||||
} from '../NewFeatureStrategyCreate/featureStrategyFormTestSetup';
 | 
			
		||||
import userEvent from '@testing-library/user-event';
 | 
			
		||||
 | 
			
		||||
const featureName = 'my-new-feature';
 | 
			
		||||
const variantName = 'Blue';
 | 
			
		||||
 | 
			
		||||
const setupComponent = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        wrapper: render(
 | 
			
		||||
            <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                    path={
 | 
			
		||||
                        '/projects/:projectId/features/:featureId/strategies/edit'
 | 
			
		||||
                    }
 | 
			
		||||
                    element={<NewFeatureStrategyEdit />}
 | 
			
		||||
                />
 | 
			
		||||
            </Routes>,
 | 
			
		||||
            {
 | 
			
		||||
                route: `/projects/default/features/${featureName}/strategies/edit?environmentId=development&strategyId=1`,
 | 
			
		||||
                permissions: [
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: CREATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_STRATEGY,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        permission: UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
                        project: 'default',
 | 
			
		||||
                        environment: 'development',
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        expectedGroupId: 'newGroupId',
 | 
			
		||||
        expectedVariantName: variantName,
 | 
			
		||||
        expectedSliderValue: '75',
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
    setupProjectEndpoint();
 | 
			
		||||
    setupSegmentsEndpoint();
 | 
			
		||||
    setupStrategyEndpoint();
 | 
			
		||||
    setupFeaturesEndpoint(featureName, variantName);
 | 
			
		||||
    setupUiConfigEndpoint();
 | 
			
		||||
    setupContextEndpoint();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('NewFeatureStrategyEdit', () => {
 | 
			
		||||
    test('formatUpdateStrategyApiCode', () => {
 | 
			
		||||
        const strategy: IFeatureStrategy = {
 | 
			
		||||
            id: 'a',
 | 
			
		||||
            name: 'b',
 | 
			
		||||
            parameters: {
 | 
			
		||||
                c: 1,
 | 
			
		||||
                b: 2,
 | 
			
		||||
                a: 3,
 | 
			
		||||
            },
 | 
			
		||||
            constraints: [],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const strategyDefinition: IStrategy = {
 | 
			
		||||
            name: 'c',
 | 
			
		||||
            displayName: 'd',
 | 
			
		||||
            description: 'e',
 | 
			
		||||
            editable: false,
 | 
			
		||||
            deprecated: false,
 | 
			
		||||
            parameters: [
 | 
			
		||||
                { name: 'a', description: '', type: '', required: false },
 | 
			
		||||
                { name: 'b', description: '', type: '', required: false },
 | 
			
		||||
                { name: 'c', description: '', type: '', required: false },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        expect(
 | 
			
		||||
            formatUpdateStrategyApiCode(
 | 
			
		||||
                'projectId',
 | 
			
		||||
                'featureId',
 | 
			
		||||
                'environmentId',
 | 
			
		||||
                'strategyId',
 | 
			
		||||
                strategy,
 | 
			
		||||
                strategyDefinition,
 | 
			
		||||
                'unleashUrl',
 | 
			
		||||
            ),
 | 
			
		||||
        ).toMatchInlineSnapshot(`
 | 
			
		||||
      "curl --location --request PUT 'unleashUrl/api/admin/projects/projectId/features/featureId/environments/environmentId/strategies/strategyId' \\
 | 
			
		||||
          --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
          --header 'Content-Type: application/json' \\
 | 
			
		||||
          --data-raw '{
 | 
			
		||||
        "id": "a",
 | 
			
		||||
        "name": "b",
 | 
			
		||||
        "parameters": {
 | 
			
		||||
          "a": 3,
 | 
			
		||||
          "b": 2,
 | 
			
		||||
          "c": 1
 | 
			
		||||
        },
 | 
			
		||||
        "constraints": []
 | 
			
		||||
      }'"
 | 
			
		||||
    `);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should change general settings', async () => {
 | 
			
		||||
        const { expectedGroupId, expectedSliderValue, wrapper } =
 | 
			
		||||
            setupComponent();
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            expect(screen.getByText('Gradual rollout')).toBeInTheDocument();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const slider = await screen.findByRole('slider', { name: /rollout/i });
 | 
			
		||||
        const groupIdInput = await screen.getByLabelText('groupId');
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue('50');
 | 
			
		||||
        expect(groupIdInput).toHaveValue(featureName);
 | 
			
		||||
        const defaultStickiness = await screen.findByText('default');
 | 
			
		||||
        userEvent.click(defaultStickiness);
 | 
			
		||||
        const randomStickiness = await screen.findByText('random');
 | 
			
		||||
        userEvent.click(randomStickiness);
 | 
			
		||||
 | 
			
		||||
        fireEvent.change(slider, { target: { value: expectedSliderValue } });
 | 
			
		||||
        fireEvent.change(groupIdInput, { target: { value: expectedGroupId } });
 | 
			
		||||
 | 
			
		||||
        expect(slider).toHaveValue(expectedSliderValue);
 | 
			
		||||
        expect(groupIdInput).toHaveValue(expectedGroupId);
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            const codeSnippet = document.querySelector('pre')?.innerHTML;
 | 
			
		||||
            const count = (codeSnippet!.match(/random/g) || []).length;
 | 
			
		||||
            // strategy stickiness and variant stickiness
 | 
			
		||||
            expect(count).toBe(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('should not change variant names', async () => {
 | 
			
		||||
        const { expectedVariantName } = setupComponent();
 | 
			
		||||
 | 
			
		||||
        await waitFor(() => {
 | 
			
		||||
            expect(screen.getByText('Gradual rollout')).toBeInTheDocument();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const variantsEl = screen.getByText('Variants');
 | 
			
		||||
        fireEvent.click(variantsEl);
 | 
			
		||||
 | 
			
		||||
        expect(screen.getByText(expectedVariantName)).toBeInTheDocument();
 | 
			
		||||
 | 
			
		||||
        const inputElement = screen.getAllByRole('textbox')[0];
 | 
			
		||||
        expect(inputElement).toBeDisabled();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@ -1,373 +0,0 @@
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { useRequiredQueryParam } from 'hooks/useRequiredQueryParam';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import {
 | 
			
		||||
    IFeatureStrategy,
 | 
			
		||||
    IFeatureStrategyPayload,
 | 
			
		||||
    IStrategy,
 | 
			
		||||
} from 'interfaces/strategy';
 | 
			
		||||
import { UPDATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import { ISegment } from 'interfaces/segment';
 | 
			
		||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
 | 
			
		||||
import { useFormErrors } from 'hooks/useFormErrors';
 | 
			
		||||
import { useStrategy } from 'hooks/api/getters/useStrategy/useStrategy';
 | 
			
		||||
import { sortStrategyParameters } from 'utils/sortStrategyParameters';
 | 
			
		||||
import { useCollaborateData } from 'hooks/useCollaborateData';
 | 
			
		||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
			
		||||
import { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
import { comparisonModerator } from '../featureStrategy.utils';
 | 
			
		||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
			
		||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
			
		||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
			
		||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
			
		||||
import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm';
 | 
			
		||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
 | 
			
		||||
import { constraintId } from 'component/common/ConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import { useScheduledChangeRequestsWithStrategy } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
 | 
			
		||||
import {
 | 
			
		||||
    getChangeRequestConflictCreatedData,
 | 
			
		||||
    getChangeRequestConflictCreatedDataFromScheduleData,
 | 
			
		||||
} from './change-request-conflict-data';
 | 
			
		||||
 | 
			
		||||
const useTitleTracking = () => {
 | 
			
		||||
    const [previousTitle, setPreviousTitle] = useState<string>('');
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
 | 
			
		||||
    const trackTitle = (title: string = '') => {
 | 
			
		||||
        // don't expose the title, just if it was added, removed, or edited
 | 
			
		||||
        if (title === previousTitle) {
 | 
			
		||||
            trackEvent('strategyTitle', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    action: 'none',
 | 
			
		||||
                    on: 'edit',
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (previousTitle === '' && title !== '') {
 | 
			
		||||
            trackEvent('strategyTitle', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    action: 'added',
 | 
			
		||||
                    on: 'edit',
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (previousTitle !== '' && title === '') {
 | 
			
		||||
            trackEvent('strategyTitle', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    action: 'removed',
 | 
			
		||||
                    on: 'edit',
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        if (previousTitle !== '' && title !== '' && title !== previousTitle) {
 | 
			
		||||
            trackEvent('strategyTitle', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    action: 'edited',
 | 
			
		||||
                    on: 'edit',
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        setPreviousTitle,
 | 
			
		||||
        trackTitle,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const addIdSymbolToConstraints = (strategy?: IFeatureStrategy) => {
 | 
			
		||||
    if (!strategy) return;
 | 
			
		||||
 | 
			
		||||
    return strategy?.constraints.map((constraint) => {
 | 
			
		||||
        return { ...constraint, [constraintId]: uuidv4() };
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const NewFeatureStrategyEdit = () => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const featureId = useRequiredPathParam('featureId');
 | 
			
		||||
    const environmentId = useRequiredQueryParam('environmentId');
 | 
			
		||||
    const strategyId = useRequiredQueryParam('strategyId');
 | 
			
		||||
    const [tab, setTab] = useState(0);
 | 
			
		||||
 | 
			
		||||
    const [strategy, setStrategy] = useState<Partial<IFeatureStrategy>>({});
 | 
			
		||||
    const [segments, setSegments] = useState<ISegment[]>([]);
 | 
			
		||||
    const { updateStrategyOnFeature, loading } = useFeatureStrategyApi();
 | 
			
		||||
    const { strategyDefinition } = useStrategy(strategy.name);
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const errors = useFormErrors();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { unleashUrl } = uiConfig;
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const { addChange } = useChangeRequestApi();
 | 
			
		||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
			
		||||
    const { refetch: refetchChangeRequests, data: pendingChangeRequests } =
 | 
			
		||||
        usePendingChangeRequests(projectId);
 | 
			
		||||
    const { setPreviousTitle } = useTitleTracking();
 | 
			
		||||
 | 
			
		||||
    const { feature, refetchFeature } = useFeature(projectId, featureId);
 | 
			
		||||
 | 
			
		||||
    const ref = useRef<IFeatureToggle>(feature);
 | 
			
		||||
 | 
			
		||||
    const { data, staleDataNotification, forceRefreshCache } =
 | 
			
		||||
        useCollaborateData<IFeatureToggle>(
 | 
			
		||||
            {
 | 
			
		||||
                unleashGetter: useFeature,
 | 
			
		||||
                params: [projectId, featureId],
 | 
			
		||||
                dataKey: 'feature',
 | 
			
		||||
                refetchFunctionKey: 'refetchFeature',
 | 
			
		||||
                options: {},
 | 
			
		||||
            },
 | 
			
		||||
            feature,
 | 
			
		||||
            {
 | 
			
		||||
                afterSubmitAction: refetchFeature,
 | 
			
		||||
            },
 | 
			
		||||
            comparisonModerator,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (ref.current.name === '' && feature.name) {
 | 
			
		||||
            forceRefreshCache(feature);
 | 
			
		||||
            ref.current = feature;
 | 
			
		||||
        }
 | 
			
		||||
    }, [feature]);
 | 
			
		||||
 | 
			
		||||
    const { trackEvent } = usePlausibleTracker();
 | 
			
		||||
    const { changeRequests: scheduledChangeRequestThatUseStrategy } =
 | 
			
		||||
        useScheduledChangeRequestsWithStrategy(projectId, strategyId);
 | 
			
		||||
 | 
			
		||||
    const pendingCrsUsingThisStrategy = getChangeRequestConflictCreatedData(
 | 
			
		||||
        pendingChangeRequests,
 | 
			
		||||
        featureId,
 | 
			
		||||
        strategyId,
 | 
			
		||||
        uiConfig,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const scheduledCrsUsingThisStrategy =
 | 
			
		||||
        getChangeRequestConflictCreatedDataFromScheduleData(
 | 
			
		||||
            scheduledChangeRequestThatUseStrategy,
 | 
			
		||||
            uiConfig,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const emitConflictsCreatedEvents = (): void =>
 | 
			
		||||
        [
 | 
			
		||||
            ...pendingCrsUsingThisStrategy,
 | 
			
		||||
            ...scheduledCrsUsingThisStrategy,
 | 
			
		||||
        ].forEach((data) =>
 | 
			
		||||
            trackEvent('change_request', {
 | 
			
		||||
                props: {
 | 
			
		||||
                    ...data,
 | 
			
		||||
                    action: 'edit-strategy',
 | 
			
		||||
                    eventType: 'conflict-created',
 | 
			
		||||
                },
 | 
			
		||||
            }),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        segments: savedStrategySegments,
 | 
			
		||||
        refetchSegments: refetchSavedStrategySegments,
 | 
			
		||||
    } = useSegments(strategyId);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const savedStrategy = data?.environments
 | 
			
		||||
            .flatMap((environment) => environment.strategies)
 | 
			
		||||
            .find((strategy) => strategy.id === strategyId);
 | 
			
		||||
 | 
			
		||||
        const constraintsWithId = addIdSymbolToConstraints(savedStrategy);
 | 
			
		||||
 | 
			
		||||
        const formattedStrategy = {
 | 
			
		||||
            ...savedStrategy,
 | 
			
		||||
            constraints: constraintsWithId,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        setStrategy((prev) => ({ ...prev, ...formattedStrategy }));
 | 
			
		||||
        setPreviousTitle(savedStrategy?.title || '');
 | 
			
		||||
    }, [strategyId, data]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Fill in the selected segments once they've been fetched.
 | 
			
		||||
        savedStrategySegments && setSegments(savedStrategySegments);
 | 
			
		||||
    }, [JSON.stringify(savedStrategySegments)]);
 | 
			
		||||
 | 
			
		||||
    const payload = createStrategyPayload(strategy, segments);
 | 
			
		||||
 | 
			
		||||
    const onStrategyEdit = async (payload: IFeatureStrategyPayload) => {
 | 
			
		||||
        await updateStrategyOnFeature(
 | 
			
		||||
            projectId,
 | 
			
		||||
            featureId,
 | 
			
		||||
            environmentId,
 | 
			
		||||
            strategyId,
 | 
			
		||||
            payload,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await refetchSavedStrategySegments();
 | 
			
		||||
        setToastData({
 | 
			
		||||
            title: 'Strategy updated',
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            confetti: true,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onStrategyRequestEdit = async (payload: IFeatureStrategyPayload) => {
 | 
			
		||||
        await addChange(projectId, environmentId, {
 | 
			
		||||
            action: 'updateStrategy',
 | 
			
		||||
            feature: featureId,
 | 
			
		||||
            payload: { ...payload, id: strategyId },
 | 
			
		||||
        });
 | 
			
		||||
        // FIXME: segments in change requests
 | 
			
		||||
        setToastData({
 | 
			
		||||
            title: 'Change added to draft',
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            confetti: true,
 | 
			
		||||
        });
 | 
			
		||||
        refetchChangeRequests();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmit = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            if (isChangeRequestConfigured(environmentId)) {
 | 
			
		||||
                await onStrategyRequestEdit(payload);
 | 
			
		||||
            } else {
 | 
			
		||||
                await onStrategyEdit(payload);
 | 
			
		||||
            }
 | 
			
		||||
            emitConflictsCreatedEvents();
 | 
			
		||||
            refetchFeature();
 | 
			
		||||
            navigate(formatFeaturePath(projectId, featureId));
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!strategy.id || !strategyDefinition) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!data) return null;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            modal
 | 
			
		||||
            disablePadding
 | 
			
		||||
            description={featureStrategyHelp}
 | 
			
		||||
            documentationLink={featureStrategyDocsLink}
 | 
			
		||||
            documentationLinkLabel={featureStrategyDocsLinkLabel}
 | 
			
		||||
            formatApiCode={() =>
 | 
			
		||||
                formatUpdateStrategyApiCode(
 | 
			
		||||
                    projectId,
 | 
			
		||||
                    featureId,
 | 
			
		||||
                    environmentId,
 | 
			
		||||
                    strategyId,
 | 
			
		||||
                    payload,
 | 
			
		||||
                    strategyDefinition,
 | 
			
		||||
                    unleashUrl,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        >
 | 
			
		||||
            <NewFeatureStrategyForm
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                feature={data}
 | 
			
		||||
                strategy={strategy}
 | 
			
		||||
                setStrategy={setStrategy}
 | 
			
		||||
                segments={segments}
 | 
			
		||||
                setSegments={setSegments}
 | 
			
		||||
                environmentId={environmentId}
 | 
			
		||||
                onSubmit={onSubmit}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                permission={UPDATE_FEATURE_STRATEGY}
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                isChangeRequest={isChangeRequestConfigured(environmentId)}
 | 
			
		||||
                tab={tab}
 | 
			
		||||
                setTab={setTab}
 | 
			
		||||
                StrategyVariants={
 | 
			
		||||
                    <NewStrategyVariants
 | 
			
		||||
                        strategy={strategy}
 | 
			
		||||
                        setStrategy={setStrategy}
 | 
			
		||||
                        environment={environmentId}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            {staleDataNotification}
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createStrategyPayload = (
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>,
 | 
			
		||||
    segments: ISegment[],
 | 
			
		||||
): IFeatureStrategyPayload => ({
 | 
			
		||||
    name: strategy.name,
 | 
			
		||||
    title: strategy.title,
 | 
			
		||||
    constraints: strategy.constraints ?? [],
 | 
			
		||||
    parameters: strategy.parameters ?? {},
 | 
			
		||||
    variants: strategy.variants ?? [],
 | 
			
		||||
    segments: segments.map((segment) => segment.id),
 | 
			
		||||
    disabled: strategy.disabled ?? false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const formatFeaturePath = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
): string => {
 | 
			
		||||
    return `/projects/${projectId}/features/${featureId}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatEditStrategyPath = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
    environmentId: string,
 | 
			
		||||
    strategyId: string,
 | 
			
		||||
): string => {
 | 
			
		||||
    const params = new URLSearchParams({ environmentId, strategyId });
 | 
			
		||||
 | 
			
		||||
    return `/projects/${projectId}/features/${featureId}/strategies/edit?${params}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const formatUpdateStrategyApiCode = (
 | 
			
		||||
    projectId: string,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
    environmentId: string,
 | 
			
		||||
    strategyId: string,
 | 
			
		||||
    strategy: Partial<IFeatureStrategy>,
 | 
			
		||||
    strategyDefinition: IStrategy,
 | 
			
		||||
    unleashUrl?: string,
 | 
			
		||||
): string => {
 | 
			
		||||
    if (!unleashUrl) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sort the strategy parameters payload so that they match
 | 
			
		||||
    // the order of the input fields in the form, for usability.
 | 
			
		||||
    const sortedStrategy = {
 | 
			
		||||
        ...strategy,
 | 
			
		||||
        parameters: sortStrategyParameters(
 | 
			
		||||
            strategy.parameters ?? {},
 | 
			
		||||
            strategyDefinition,
 | 
			
		||||
        ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const url = `${unleashUrl}/api/admin/projects/${projectId}/features/${featureId}/environments/${environmentId}/strategies/${strategyId}`;
 | 
			
		||||
    const payload = JSON.stringify(sortedStrategy, undefined, 2);
 | 
			
		||||
 | 
			
		||||
    return `curl --location --request PUT '${url}' \\
 | 
			
		||||
    --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
    --header 'Content-Type: application/json' \\
 | 
			
		||||
    --data-raw '${payload}'`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const featureStrategyHelp = `
 | 
			
		||||
    An activation strategy will only run when a feature toggle is enabled and provides a way to control who will get access to the feature.
 | 
			
		||||
    If any of a feature toggle's activation strategies returns true, the user will get access.
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const featureStrategyDocsLink =
 | 
			
		||||
    'https://docs.getunleash.io/reference/activation-strategies';
 | 
			
		||||
 | 
			
		||||
export const featureStrategyDocsLinkLabel = 'Strategies documentation';
 | 
			
		||||
@ -1,113 +0,0 @@
 | 
			
		||||
import { IUiConfig } from 'interfaces/uiConfig';
 | 
			
		||||
import {
 | 
			
		||||
    getChangeRequestConflictCreatedData,
 | 
			
		||||
    getChangeRequestConflictCreatedDataFromScheduleData,
 | 
			
		||||
} from './change-request-conflict-data';
 | 
			
		||||
 | 
			
		||||
const uiConfig: Pick<IUiConfig, 'baseUriPath' | 'versionInfo'> = {
 | 
			
		||||
    baseUriPath: '/some-base-uri',
 | 
			
		||||
};
 | 
			
		||||
const unleashIdentifier = uiConfig.baseUriPath;
 | 
			
		||||
const featureId = 'flag-with-deleted-scheduler';
 | 
			
		||||
const strategyId = 'ed2ffa14-004c-4ed1-931b-78761681c54a';
 | 
			
		||||
 | 
			
		||||
const changeRequestWithStrategy = {
 | 
			
		||||
    id: 105,
 | 
			
		||||
    features: [
 | 
			
		||||
        {
 | 
			
		||||
            name: featureId,
 | 
			
		||||
            changes: [
 | 
			
		||||
                {
 | 
			
		||||
                    action: 'updateStrategy' as const,
 | 
			
		||||
                    payload: {
 | 
			
		||||
                        id: strategyId,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    state: 'In review' as const,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const changeRequestWithoutStrategy = {
 | 
			
		||||
    id: 106,
 | 
			
		||||
    features: [
 | 
			
		||||
        {
 | 
			
		||||
            name: featureId,
 | 
			
		||||
            changes: [
 | 
			
		||||
                {
 | 
			
		||||
                    action: 'deleteStrategy' as const,
 | 
			
		||||
                    payload: {
 | 
			
		||||
                        id: strategyId,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: featureId,
 | 
			
		||||
            changes: [
 | 
			
		||||
                {
 | 
			
		||||
                    action: 'addStrategy' as const,
 | 
			
		||||
                    payload: {},
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    state: 'In review' as const,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
test('it finds crs that update a strategy', () => {
 | 
			
		||||
    const results = getChangeRequestConflictCreatedData(
 | 
			
		||||
        [changeRequestWithStrategy],
 | 
			
		||||
        featureId,
 | 
			
		||||
        strategyId,
 | 
			
		||||
        uiConfig,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(results).toStrictEqual([
 | 
			
		||||
        {
 | 
			
		||||
            state: changeRequestWithStrategy.state,
 | 
			
		||||
            changeRequest: `${unleashIdentifier}#${changeRequestWithStrategy.id}`,
 | 
			
		||||
        },
 | 
			
		||||
    ]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('it does not return crs that do not update a strategy', () => {
 | 
			
		||||
    const results = getChangeRequestConflictCreatedData(
 | 
			
		||||
        [changeRequestWithoutStrategy],
 | 
			
		||||
        featureId,
 | 
			
		||||
        strategyId,
 | 
			
		||||
        uiConfig,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(results).toStrictEqual([]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('it maps scheduled change request data', () => {
 | 
			
		||||
    const scheduledChanges = [
 | 
			
		||||
        {
 | 
			
		||||
            id: 103,
 | 
			
		||||
            environment: 'development',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            id: 104,
 | 
			
		||||
            environment: 'development',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const results = getChangeRequestConflictCreatedDataFromScheduleData(
 | 
			
		||||
        scheduledChanges,
 | 
			
		||||
        uiConfig,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(results).toStrictEqual([
 | 
			
		||||
        {
 | 
			
		||||
            state: 'Scheduled',
 | 
			
		||||
            changeRequest: `${unleashIdentifier}#103`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            state: 'Scheduled',
 | 
			
		||||
            changeRequest: `${unleashIdentifier}#104`,
 | 
			
		||||
        },
 | 
			
		||||
    ]);
 | 
			
		||||
});
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
    ChangeRequestState,
 | 
			
		||||
    ChangeRequestType,
 | 
			
		||||
    IChangeRequestFeature,
 | 
			
		||||
    IFeatureChange,
 | 
			
		||||
} from 'component/changeRequest/changeRequest.types';
 | 
			
		||||
import { ScheduledChangeRequestViewModel } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
 | 
			
		||||
import { IUiConfig } from 'interfaces/uiConfig';
 | 
			
		||||
import { getUniqueChangeRequestId } from 'utils/unique-change-request-id';
 | 
			
		||||
 | 
			
		||||
type ChangeRequestConflictCreatedData = {
 | 
			
		||||
    changeRequest: string;
 | 
			
		||||
    state: ChangeRequestState;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getChangeRequestConflictCreatedData = (
 | 
			
		||||
    changeRequests:
 | 
			
		||||
        | {
 | 
			
		||||
              state: ChangeRequestType['state'];
 | 
			
		||||
              id: ChangeRequestType['id'];
 | 
			
		||||
              features: {
 | 
			
		||||
                  name: IChangeRequestFeature['name'];
 | 
			
		||||
                  changes: (Pick<IFeatureChange, 'action'> & {
 | 
			
		||||
                      payload: { id?: number | string };
 | 
			
		||||
                  })[];
 | 
			
		||||
              }[];
 | 
			
		||||
          }[]
 | 
			
		||||
        | undefined,
 | 
			
		||||
    featureId: string,
 | 
			
		||||
    strategyId: string,
 | 
			
		||||
    uiConfig: Pick<IUiConfig, 'baseUriPath' | 'versionInfo'>,
 | 
			
		||||
): ChangeRequestConflictCreatedData[] =>
 | 
			
		||||
    changeRequests
 | 
			
		||||
        ?.filter((cr) =>
 | 
			
		||||
            cr.features
 | 
			
		||||
                .find((feature) => feature.name === featureId)
 | 
			
		||||
                ?.changes.some(
 | 
			
		||||
                    (change) =>
 | 
			
		||||
                        change.action === 'updateStrategy' &&
 | 
			
		||||
                        change.payload.id === strategyId,
 | 
			
		||||
                ),
 | 
			
		||||
        )
 | 
			
		||||
        .map((cr) => ({
 | 
			
		||||
            changeRequest: getUniqueChangeRequestId(uiConfig, cr.id),
 | 
			
		||||
            state: cr.state,
 | 
			
		||||
        })) ?? [];
 | 
			
		||||
 | 
			
		||||
export const getChangeRequestConflictCreatedDataFromScheduleData = (
 | 
			
		||||
    changeRequests: Pick<ScheduledChangeRequestViewModel, 'id'>[] | undefined,
 | 
			
		||||
    uiConfig: Pick<IUiConfig, 'baseUriPath' | 'versionInfo'>,
 | 
			
		||||
): ChangeRequestConflictCreatedData[] =>
 | 
			
		||||
    changeRequests?.map((cr) => ({
 | 
			
		||||
        changeRequest: getUniqueChangeRequestId(uiConfig, cr.id),
 | 
			
		||||
        state: 'Scheduled' as const,
 | 
			
		||||
    })) ?? [];
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData';
 | 
			
		||||
import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments';
 | 
			
		||||
import { Route, Routes, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
 | 
			
		||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
 | 
			
		||||
import { formatFeaturePath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
@ -9,9 +8,8 @@ import { usePageTitle } from 'hooks/usePageTitle';
 | 
			
		||||
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
 | 
			
		||||
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
 | 
			
		||||
import { styled } from '@mui/material';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { NewFeatureStrategyCreate } from 'component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate';
 | 
			
		||||
import { NewFeatureStrategyEdit } from 'component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit';
 | 
			
		||||
import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
 | 
			
		||||
import { FeatureStrategyEdit } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
@ -61,7 +59,7 @@ const FeatureOverview = () => {
 | 
			
		||||
                            onClose={onSidebarClose}
 | 
			
		||||
                            open
 | 
			
		||||
                        >
 | 
			
		||||
                            <NewFeatureStrategyCreate />
 | 
			
		||||
                            <FeatureStrategyCreate />
 | 
			
		||||
                        </SidebarModal>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
@ -73,7 +71,7 @@ const FeatureOverview = () => {
 | 
			
		||||
                            onClose={onSidebarClose}
 | 
			
		||||
                            open
 | 
			
		||||
                        >
 | 
			
		||||
                            <NewFeatureStrategyEdit />
 | 
			
		||||
                            <FeatureStrategyEdit />
 | 
			
		||||
                        </SidebarModal>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user