diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx index 850bcefee8..3b9f8b4365 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/Change.tsx @@ -10,6 +10,7 @@ import { Alert, Box, styled } from '@mui/material'; import { ToggleStatusChange } from './ToggleStatusChange'; import { StrategyChange } from './StrategyChange'; import { VariantPatch } from './VariantPatch/VariantPatch'; +import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder'; const StyledSingleChangeBox = styled(Box, { shouldForwardProp: (prop: string) => !prop.startsWith('$'), @@ -108,6 +109,15 @@ export const Change: FC<{ discard={discard} /> )} + {change.action === 'reorderStrategy' && ( + + )} ); diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder.tsx new file mode 100644 index 0000000000..c6cc35710d --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder.tsx @@ -0,0 +1,114 @@ +import { IChangeRequestReorderStrategy } from '../../../../changeRequest.types'; +import { ReactNode } from 'react'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; +import { Box, styled } from '@mui/material'; +import { EnvironmentStrategyOrderDiff } from './EnvironmentStrategyOrderDiff'; +import { StrategyExecution } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution'; +import { formatStrategyName } from '../../../../../../utils/strategyNames'; + +const ChangeItemInfo = styled(Box)({ + display: 'flex', + flexDirection: 'column', +}); + +const StyledChangeHeader = styled(Box)(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'start', + marginBottom: theme.spacing(2), + lineHeight: theme.spacing(3), +})); +const StyledStrategyExecutionWrapper = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing(2), + marginTop: theme.spacing(2), + lineHeight: theme.spacing(3), + gap: theme.spacing(1), +})); + +const StyledStrategyContainer = styled('div')(({ theme }) => ({ + flexDirection: 'row', + marginBottom: theme.spacing(2), +})); + +interface IEnvironmentStrategyExecutionOrderProps { + feature: string; + project: string; + environment: string; + change: IChangeRequestReorderStrategy; + discard?: ReactNode; +} + +export const EnvironmentStrategyExecutionOrder = ({ + feature, + environment, + change, + project, + discard, +}: IEnvironmentStrategyExecutionOrderProps) => { + const { feature: featureData } = useFeature(project, feature); + const featureEnvironment = featureData.environments.find( + ({ name }) => environment === name + ); + const environmentStrategies = featureEnvironment?.strategies || []; + + const preData = { + strategyIds: + environmentStrategies + .sort((strategy1, strategy2) => { + if ( + typeof strategy1.sortOrder === 'number' && + typeof strategy2.sortOrder === 'number' + ) { + return strategy1.sortOrder - strategy2.sortOrder; + } + return 0; + }) + .map(strategy => strategy.id) ?? [], + }; + + const updatedStrategies = change.payload.map(({ id }) => { + return environmentStrategies.find(s => s.id === id); + }); + + const data = { + strategyIds: updatedStrategies.map(strategy => strategy!.id), + }; + + return ( + + + + } + tooltipProps={{ + maxWidth: 500, + maxHeight: 600, + }} + > + Updating strategy execution order to: + + {discard} + + + {updatedStrategies.map((strategy, index) => ( + + {`${index + 1}: `} + {formatStrategyName(strategy?.name || '')} + {strategy?.title && ` - ${strategy.title}`} + + + ))} + + + ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyOrderDiff.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyOrderDiff.tsx new file mode 100644 index 0000000000..834dfc8489 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/EnvironmentStrategyExecutionOrder/EnvironmentStrategyOrderDiff.tsx @@ -0,0 +1,30 @@ +import { styled } from '@mui/material'; +import EventDiff from 'component/events/EventDiff/EventDiff'; + +const StyledCodeSection = styled('div')(({ theme }) => ({ + overflowX: 'auto', + '& code': { + wordWrap: 'break-word', + whiteSpace: 'pre-wrap', + fontFamily: 'monospace', + lineHeight: 1.5, + fontSize: theme.fontSizes.smallBody, + }, +})); +type StrategyIds = { strategyIds: string[] }; +interface IDiffProps { + preData: StrategyIds; + data: StrategyIds; +} + +export const EnvironmentStrategyOrderDiff = ({ preData, data }: IDiffProps) => ( + + a.index - b.index} + /> + +); diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/Diff.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/VariantDiff.tsx similarity index 93% rename from frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/Diff.tsx rename to frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/VariantDiff.tsx index 8feb877909..eb6a546f0b 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/Diff.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/VariantPatch/VariantDiff.tsx @@ -24,7 +24,7 @@ const variantsArrayToObject = (variants: IFeatureVariant[]) => {} ); -export const Diff = ({ preData, data }: IDiffProps) => ( +export const VariantDiff = ({ preData, data }: IDiffProps) => ( diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 50d7779eab..1530412b8c 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -1,6 +1,7 @@ import { IFeatureVariant } from 'interfaces/featureToggle'; import { IFeatureStrategy } from '../../interfaces/strategy'; import { IUser } from '../../interfaces/user'; +import { SetStrategySortOrderSchema } from '../../openapi'; export interface IChangeRequest { id: number; @@ -64,7 +65,8 @@ type ChangeRequestPayload = | ChangeRequestAddStrategy | ChangeRequestEditStrategy | ChangeRequestDeleteStrategy - | ChangeRequestVariantPatch; + | ChangeRequestVariantPatch + | SetStrategySortOrderSchema; export interface IChangeRequestAddStrategy extends IChangeRequestBase { action: 'addStrategy'; @@ -91,12 +93,18 @@ export interface IChangeRequestPatchVariant extends IChangeRequestBase { payload: ChangeRequestVariantPatch; } +export interface IChangeRequestReorderStrategy extends IChangeRequestBase { + action: 'reorderStrategy'; + payload: SetStrategySortOrderSchema; +} + export type IChange = | IChangeRequestAddStrategy | IChangeRequestDeleteStrategy | IChangeRequestUpdateStrategy | IChangeRequestEnabled - | IChangeRequestPatchVariant; + | IChangeRequestPatchVariant + | IChangeRequestReorderStrategy; type ChangeRequestVariantPatch = { variants: IFeatureVariant[]; @@ -123,4 +131,5 @@ export type ChangeRequestAction = | 'addStrategy' | 'updateStrategy' | 'deleteStrategy' - | 'patchVariant'; + | 'patchVariant' + | 'reorderStrategy'; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx index 38a274b104..9b2336e732 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/EnvironmentAccordionBody.tsx @@ -9,6 +9,9 @@ import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { FeatureStrategyEmpty } from 'component/feature/FeatureStrategy/FeatureStrategyEmpty/FeatureStrategyEmpty'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; +import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; +import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; interface IEnvironmentAccordionBodyProps { isDisabled: boolean; @@ -36,6 +39,10 @@ const EnvironmentAccordionBody = ({ const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { setStrategiesSortOrder } = useFeatureStrategyApi(); + const { addChange } = useChangeRequestApi(); + const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); + const { refetch: refetchChangeRequests } = + usePendingChangeRequests(projectId); const { setToastData, setToastApiError } = useToast(); const { refetchFeature } = useFeature(projectId, featureId); const [strategies, setStrategies] = useState( @@ -73,6 +80,37 @@ const EnvironmentAccordionBody = ({ } }; + const onChangeRequestReorder = async ( + payload: { id: string; sortOrder: number }[] + ) => { + await addChange(projectId, featureEnvironment.name, { + action: 'reorderStrategy', + feature: featureId, + payload, + }); + + setToastData({ + title: 'Strategy execution order added to draft', + type: 'success', + confetti: true, + }); + refetchChangeRequests(); + }; + + const onStrategyReorder = async ( + payload: { id: string; sortOrder: number }[] + ) => { + try { + if (isChangeRequestConfigured(featureEnvironment.name)) { + await onChangeRequestReorder(payload); + } else { + await onReorder(payload); + } + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + const onDragStartRef = ( ref: RefObject, @@ -129,7 +167,7 @@ const EnvironmentAccordionBody = ({ const onDragEnd = () => { setDragItem(null); - onReorder( + onStrategyReorder( strategies.map((strategy, sortOrder) => ({ id: strategy.id, sortOrder, diff --git a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts index 4fbc9569f5..baafb8c2d0 100644 --- a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts +++ b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts @@ -8,7 +8,8 @@ export interface IChangeSchema { | 'addStrategy' | 'updateStrategy' | 'deleteStrategy' - | 'patchVariant'; + | 'patchVariant' + | 'reorderStrategy'; payload: string | boolean | object | number; } diff --git a/frontend/src/interfaces/strategy.ts b/frontend/src/interfaces/strategy.ts index 395ce1c927..6ac90c33dd 100644 --- a/frontend/src/interfaces/strategy.ts +++ b/frontend/src/interfaces/strategy.ts @@ -14,6 +14,7 @@ export interface IFeatureStrategy { environment?: string; segments?: number[]; disabled?: boolean; + sortOrder?: number; } export interface IFeatureStrategyParameters {