diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx index c11b69bcae..58135ddbf9 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyDraggableItem.tsx @@ -117,7 +117,12 @@ const renderHeaderChildren = ( ); if (draftChange) { - badges.push(); + badges.push( + , + ); } const scheduledChanges = changes.filter( @@ -127,6 +132,7 @@ const renderHeaderChildren = ( if (scheduledChanges.length > 0) { badges.push( scheduledChange.changeRequestId, )} diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.test.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.test.tsx new file mode 100644 index 0000000000..b84b425b91 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.test.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { render } from 'utils/testRenderer'; +import { screen } from '@testing-library/react'; +import { + FeatureStrategyRemoveDialogue, + SuggestFeatureStrategyRemoveDialogue, +} from './DialogStrategyRemove'; +import { + ChangeRequestState, + IChangeRequestFeature, + IFeatureChange, +} from 'component/changeRequest/changeRequest.types'; + +const strategyId = 'c81e3a1d-e91c-4083-bd0f-75bb8a9e32a2'; +const projectId = 'default'; +const environment = 'development'; +const featureId = 'bb1d79e0-95b0-4393-b248-64d1e0294ee3'; + +describe('Use in scheduled change requests', () => { + it.each(['enabled', 'disabled'])( + 'should show usage in scheduled change requests with change requests %s for the project', + async (changeRequestsEnabled) => { + const changeRequestWithTitle = { id: 1, title: 'My CR' }; + const changeRequestWithoutTitle = { id: 2 }; + const scheduledChangeRequests = [ + changeRequestWithTitle, + changeRequestWithoutTitle, + ]; + + if (changeRequestsEnabled === 'enabled') { + render( + {}} + onClose={() => {}} + isOpen={true} + scheduledChangeRequestsForStrategy={{ + projectId, + changeRequests: scheduledChangeRequests, + }} + />, + ); + } else { + render( + {}} + onClose={() => {}} + isOpen={true} + scheduledChangeRequestsForStrategy={{ + projectId, + changeRequests: scheduledChangeRequests, + }} + />, + ); + } + + const alerts = await screen.findAllByRole('alert'); + + expect( + alerts.find((alert) => + alert.textContent!.startsWith('This strategy'), + ), + ).toBeTruthy(); + + const links = await screen.findAllByRole('link'); + + expect(links).toHaveLength(scheduledChangeRequests.length); + + const [link1, link2] = links; + + expect(link1).toHaveTextContent('#1 (My CR)'); + expect(link1).toHaveAccessibleDescription('Change request 1'); + expect(link1).toHaveAttribute( + 'href', + `/projects/default/change-requests/1`, + ); + + expect(link2).toHaveTextContent('#2'); + expect(link2).toHaveAccessibleDescription('Change request 2'); + expect(link2).toHaveAttribute( + 'href', + `/projects/default/change-requests/2`, + ); + }, + ); + + it('should not render scheduled change requests warning when there are no scheduled change requests', async () => { + render( + {}} + onClose={() => {}} + isOpen={true} + scheduledChangeRequestsForStrategy={{ + projectId, + changeRequests: [], + }} + />, + ); + + const alerts = await screen.findAllByRole('alert'); + + expect( + alerts.find((alert) => + alert.textContent!.startsWith('This strategy'), + ), + ).toBeFalsy(); + + expect(alerts).toHaveLength(1); + + const link = screen.queryByRole('link'); + + expect(link).toBe(null); + }); + + it("It should render a warning saying there might be scheduled change requests if it doesn't get a successful API response", async () => { + render( + {}} + onClose={() => {}} + isOpen={true} + scheduledChangeRequestsForStrategy={{ + projectId, + changeRequests: undefined, + }} + />, + ); + + const alerts = await screen.findAllByRole('alert'); + + expect( + alerts.find((alert) => + alert.textContent!.startsWith('This strategy'), + ), + ).toBeTruthy(); + + expect(alerts).toHaveLength(2); + + const link = screen.queryByRole('link'); + + expect(link).toBe(null); + }); +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.tsx index 0394023418..3f25d59534 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/MenuStrategyRemove/DialogStrategyRemove.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import useFeatureStrategyApi from 'hooks/api/actions/useFeatureStrategyApi/useFeatureStrategyApi'; import { formatUnknownError } from 'utils/formatUnknownError'; -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import useToast from 'hooks/useToast'; import { formatFeaturePath } from '../../../../../../../../FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; @@ -10,7 +10,8 @@ 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'; - +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useScheduledChangeRequestsWithStrategy } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy'; interface IFeatureStrategyRemoveProps { projectId: string; featureId: string; @@ -18,13 +19,23 @@ interface IFeatureStrategyRemoveProps { strategyId: string; disabled?: boolean; icon?: boolean; - text?: boolean; } +type ChangeRequest = { + id: number; + title?: string; +}; + +type ScheduledChangeRequestData = { + changeRequests?: ChangeRequest[]; + projectId: string; +}; + interface IFeatureStrategyRemoveDialogueProps { onRemove: (event: React.FormEvent) => Promise; onClose: () => void; isOpen: boolean; + scheduledChangeRequestsForStrategy: ScheduledChangeRequestData; } const RemoveAlert: FC = () => ( @@ -34,30 +45,103 @@ const RemoveAlert: FC = () => ( ); -const FeatureStrategyRemoveDialogue: FC = - ({ onRemove, onClose, isOpen }) => { +const AlertContainer = styled('div')(({ theme }) => ({ + '> * + *': { + marginTop: theme.spacing(1), + }, +})); + +const StrategyInScheduledChangeRequestsWarning: FC<{ + changeRequests?: ChangeRequest[]; + projectId: string; +}> = ({ changeRequests, projectId }) => { + if (changeRequests && changeRequests.length > 0) { return ( - - - + +

+ This strategy is in use by at least one scheduled change + request. If you remove it, those change requests can no + longer be applied. +

+

+ The following scheduled change requests use this strategy: +

+
    + {changeRequests.map(({ id, title }) => { + const text = title ? `#${id} (${title})` : `#${id}`; + return ( +
  • + + {text} + +
  • + ); + })} +
+
); - }; + } else if (changeRequests === undefined) { + return ( + +

+ This strategy may be in use by one or more scheduled change + requests. If you remove it, those change requests can no + longer be applied. +

+
+ ); + } + + // all good, we have nothing to show + return null; +}; + +const Alerts: FC<{ + scheduledChangeRequestsForStrategy: ScheduledChangeRequestData; +}> = ({ scheduledChangeRequestsForStrategy }) => ( + + + + +); + +export const FeatureStrategyRemoveDialogue: FC< + IFeatureStrategyRemoveDialogueProps +> = ({ onRemove, onClose, isOpen, scheduledChangeRequestsForStrategy }) => { + return ( + + + + ); +}; const MsgContainer = styled('div')(({ theme }) => ({ marginTop: theme.spacing(3), marginBottom: theme.spacing(1), })); -const SuggestFeatureStrategyRemoveDialogue: FC< +export const SuggestFeatureStrategyRemoveDialogue: FC< IFeatureStrategyRemoveDialogueProps -> = ({ onRemove, onClose, isOpen }) => { +> = ({ onRemove, onClose, isOpen, scheduledChangeRequestsForStrategy }) => { return ( - + Your suggestion: @@ -155,7 +243,6 @@ export const DialogStrategyRemove = ({ featureId, environmentId, strategyId, - text, isOpen, onClose, }: IFeatureStrategyRemoveProps & { @@ -164,6 +251,16 @@ export const DialogStrategyRemove = ({ }) => { const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); + const { changeRequests } = useScheduledChangeRequestsWithStrategy( + projectId, + strategyId, + ); + + const changeRequestData = { + changeRequests, + projectId, + }; + const onRemove = useOnRemove({ featureId, projectId, @@ -186,6 +283,7 @@ export const DialogStrategyRemove = ({ await onSuggestRemove(e); onClose(); }} + scheduledChangeRequestsForStrategy={changeRequestData} /> ); } @@ -195,6 +293,7 @@ export const DialogStrategyRemove = ({ isOpen={isOpen} onClose={() => onClose()} onRemove={onRemove} + scheduledChangeRequestsForStrategy={changeRequestData} /> ); }; diff --git a/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy.ts b/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy.ts new file mode 100644 index 0000000000..ad99deb816 --- /dev/null +++ b/frontend/src/hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy.ts @@ -0,0 +1,30 @@ +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { IChangeRequest } from 'component/changeRequest/changeRequest.types'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('ChangeRequest')) + .then((res) => res.json()); +}; + +export const useScheduledChangeRequestsWithStrategy = ( + project: string, + strategyId: string, +) => { + const { data, error, mutate } = useEnterpriseSWR( + [], + formatApiPath( + `api/admin/projects/${project}/change-requests/scheduled/with-strategy/${strategyId}`, + ), + fetcher, + ); + + return { + changeRequests: data, + loading: !error && !data, + refetch: mutate, + error, + }; +};