diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EditChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EditChange.tsx index 769bf358f73d..c6dc7bd24f8c 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EditChange.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EditChange.tsx @@ -22,6 +22,9 @@ import { } from 'component/changeRequest/changeRequest.types'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { NewFeatureStrategyForm } from 'component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm'; interface IEditChangeProps { change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy; @@ -44,6 +47,8 @@ export const EditChange = ({ }: IEditChangeProps) => { const projectId = useRequiredPathParam('projectId'); const { editChange } = useChangeRequestApi(); + const [tab, setTab] = useState(0); + const newStrategyConfiguration = useUiFlag('newStrategyConfiguration'); const [strategy, setStrategy] = useState>( change.payload, @@ -146,21 +151,50 @@ export const EditChange = ({ ) } > - + } + elseShow={ + + } /> + {staleDataNotification} diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx new file mode 100644 index 000000000000..b6179c574335 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyForm/NewFeatureStrategyForm.tsx @@ -0,0 +1,349 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Alert, Button, styled, Tabs, Tab } 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 { StrategyVariants } from 'component/feature/StrategyTypes/StrategyVariants'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; + +interface IFeatureStrategyFormProps { + feature: IFeatureToggle; + projectId: string; + environmentId: string; + permission: string; + onSubmit: () => void; + onCancel?: () => void; + loading: boolean; + isChangeRequest?: boolean; + strategy: Partial; + setStrategy: React.Dispatch< + React.SetStateAction> + >; + segments: ISegment[]; + setSegments: React.Dispatch>; + errors: IFormErrors; + tab: number; + setTab: React.Dispatch>; +} + +const StyledForm = styled('form')(({ theme }) => ({ + display: 'grid', + gap: theme.spacing(2), +})); + +const StyledHr = styled('hr')(({ theme }) => ({ + width: '100%', + height: '1px', + margin: theme.spacing(2, 0), + border: 'none', + background: theme.palette.background.elevation2, +})); + +const StyledButtons = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'end', + gap: theme.spacing(2), + paddingBottom: theme.spacing(10), +})); + +export const NewFeatureStrategyForm = ({ + projectId, + feature, + environmentId, + permission, + onSubmit, + onCancel, + loading, + strategy, + setStrategy, + segments, + setSegments, + errors, + isChangeRequest, + tab, + setTab, +}: IFeatureStrategyFormProps) => { + 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 { 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 { + uiConfig, + 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 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; + } + + if (enableProdGuard && !isChangeRequest) { + setShowProdGuard(true); + } else { + onSubmit(); + } + }; + + const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { + setTab(newValue); + }; + + return ( + + + + + + + + + } + /> + } + /> + + + This feature toggle is currently enabled + in the {environmentId}{' '} + environment. Any changes made here will + be available to users as soon as these + changes are approved and applied. + + } + elseShow={ + + This feature toggle is currently enabled + in the {environmentId}{' '} + environment. Any changes made here will + be available to users as soon as you hit{' '} + save. + + } + /> + + + { + setStrategy((prev) => ({ + ...prev, + title, + })); + }} + /> + + + setStrategy((strategyState) => ({ + ...strategyState, + disabled: !strategyState.disabled, + })) + } + /> + + } + /> + + + + + + } + /> + + + } + /> + } + /> + + + + {isChangeRequest + ? changeRequestButtonText + : 'Save strategy'} + + + setShowProdGuard(false)} + onClick={onSubmit} + loading={loading} + label='Save strategy' + /> + + + ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate.tsx b/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate.tsx index 59b9a834401a..6724fc1ee16d 100644 --- a/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate.tsx +++ b/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyCreate/NewFeatureStrategyCreate.tsx @@ -33,8 +33,10 @@ 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'; export const NewFeatureStrategyCreate = () => { + const [tab, setTab] = useState(0); const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const environmentId = useRequiredQueryParam('environmentId'); @@ -192,8 +194,7 @@ export const NewFeatureStrategyCreate = () => { ) } > -

NEW CREATE FORM

- { permission={CREATE_FEATURE_STRATEGY} errors={errors} isChangeRequest={isChangeRequestConfigured(environmentId)} + tab={tab} + setTab={setTab} /> {staleDataNotification} diff --git a/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit.tsx b/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit.tsx index 6c3dddcc6056..218d416247f0 100644 --- a/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit.tsx +++ b/frontend/src/component/feature/FeatureStrategy/NewFeatureStrategyEdit/NewFeatureStrategyEdit.tsx @@ -28,6 +28,7 @@ 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'; const useTitleTracking = () => { const [previousTitle, setPreviousTitle] = useState(''); @@ -80,6 +81,7 @@ export const NewFeatureStrategyEdit = () => { const featureId = useRequiredPathParam('featureId'); const environmentId = useRequiredQueryParam('environmentId'); const strategyId = useRequiredQueryParam('strategyId'); + const [tab, setTab] = useState(0); const [strategy, setStrategy] = useState>({}); const [segments, setSegments] = useState([]); @@ -214,8 +216,7 @@ export const NewFeatureStrategyEdit = () => { ) } > -

NEW EDIT FORM

- { permission={UPDATE_FEATURE_STRATEGY} errors={errors} isChangeRequest={isChangeRequestConfigured(environmentId)} + tab={tab} + setTab={setTab} /> {staleDataNotification}