From cb9fa72de173d9d9cb5682758e458c09152011eb Mon Sep 17 00:00:00 2001 From: FredrikOseberg Date: Wed, 22 Oct 2025 10:39:38 +0200 Subject: [PATCH] refactor: separate component --- .../Change/ConsolidatedProgressionChanges.tsx | 189 ++++++++++++++++++ .../Changes/Change/ProgressionChange.tsx | 4 +- .../Changes/Change/ReleasePlanChange.tsx | 177 +--------------- .../Changes/Change/applyProgressionChanges.ts | 6 +- .../Changes/Change/useModifiedReleasePlan.ts | 55 ----- 5 files changed, 197 insertions(+), 234 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ConsolidatedProgressionChanges.tsx delete mode 100644 frontend/src/component/changeRequest/ChangeRequest/Changes/Change/useModifiedReleasePlan.ts diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ConsolidatedProgressionChanges.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ConsolidatedProgressionChanges.tsx new file mode 100644 index 0000000000..bdb2eff001 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ConsolidatedProgressionChanges.tsx @@ -0,0 +1,189 @@ +import type { FC } from 'react'; +import { Alert, styled } from '@mui/material'; +import type { + ChangeRequestState, + IChangeRequestCreateMilestoneProgression, + IChangeRequestUpdateMilestoneProgression, + IChangeRequestDeleteMilestoneProgression, + IChangeRequestFeature, +} from 'component/changeRequest/changeRequest.types'; +import type { IReleasePlan } from 'interfaces/releasePlans'; +import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx'; +import { + Added, + ChangeItemInfo, + ChangeItemWrapper, + Deleted, +} from './Change.styles.tsx'; +import type { UpdateMilestoneProgressionSchema } from 'openapi'; +import { MilestoneListRenderer } from './MilestoneListRenderer.tsx'; +import { applyProgressionChanges } from './applyProgressionChanges'; +import { EventDiff } from 'component/events/EventDiff/EventDiff'; + +const StyledTabs = styled(Tabs)(({ theme }) => ({ + display: 'flex', + flexFlow: 'column', + gap: theme.spacing(1), +})); + +export const ConsolidatedProgressionChanges: FC<{ + feature: IChangeRequestFeature; + currentReleasePlan?: IReleasePlan; + changeRequestState: ChangeRequestState; + onUpdateChangeRequestSubmit?: ( + sourceMilestoneId: string, + payload: UpdateMilestoneProgressionSchema, + ) => Promise; + onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => Promise; +}> = ({ + feature, + currentReleasePlan, + changeRequestState, + onUpdateChangeRequestSubmit, + onDeleteChangeRequestSubmit, +}) => { + // Get all progression changes for this feature + const progressionChanges = feature.changes.filter( + ( + change, + ): change is + | IChangeRequestCreateMilestoneProgression + | IChangeRequestUpdateMilestoneProgression + | IChangeRequestDeleteMilestoneProgression => + change.action === 'createMilestoneProgression' || + change.action === 'updateMilestoneProgression' || + change.action === 'deleteMilestoneProgression', + ); + + if (progressionChanges.length === 0) return null; + + // Use snapshot from first change if available, otherwise use current release plan + const firstChangeWithSnapshot = + progressionChanges.find( + (change) => + change.payload?.snapshot && + (change.action === 'createMilestoneProgression' || + change.action === 'updateMilestoneProgression'), + ) || progressionChanges.find((change) => change.payload?.snapshot); + const basePlan = + firstChangeWithSnapshot?.payload?.snapshot || currentReleasePlan; + + if (!basePlan) { + return ( + + Unable to load release plan data. Please refresh the page. + + ); + } + + // Apply all progression changes + const modifiedPlan = applyProgressionChanges(basePlan, progressionChanges); + + // Collect milestone IDs with automation (modified or original) + const milestonesWithAutomation = new Set( + progressionChanges + .filter( + (change) => + change.action === 'createMilestoneProgression' || + change.action === 'updateMilestoneProgression', + ) + .map((change) => + change.action === 'createMilestoneProgression' + ? change.payload.sourceMilestone + : change.payload.sourceMilestoneId || + change.payload.sourceMilestone, + ) + .filter((id): id is string => Boolean(id)), + ); + + const milestonesWithDeletedAutomation = new Set( + progressionChanges + .filter((change) => change.action === 'deleteMilestoneProgression') + .map( + (change) => + change.payload.sourceMilestoneId || + change.payload.sourceMilestone, + ) + .filter((id): id is string => Boolean(id)), + ); + + const changeDescriptions = progressionChanges.map((change) => { + const sourceId = + change.action === 'createMilestoneProgression' + ? change.payload.sourceMilestone + : change.payload.sourceMilestoneId || + change.payload.sourceMilestone; + const sourceName = + basePlan.milestones.find((milestone) => milestone.id === sourceId) + ?.name || sourceId; + const action = + change.action === 'createMilestoneProgression' + ? 'Adding' + : change.action === 'deleteMilestoneProgression' + ? 'Deleting' + : 'Updating'; + return `${action} automation for ${sourceName}`; + }); + + // For diff view, we need to use basePlan with deleted automations shown + const basePlanWithDeletedAutomations: IReleasePlan = { + ...basePlan, + milestones: basePlan.milestones.map((milestone) => { + // If this milestone is being deleted, ensure it has its transition condition + if (milestonesWithDeletedAutomation.has(milestone.id)) { + return milestone; + } + return milestone; + }), + }; + + return ( + + + + {progressionChanges.map((change, index) => { + const Component = + change.action === 'deleteMilestoneProgression' + ? Deleted + : Added; + return ( + + {changeDescriptions[index]} + + ); + })} + +
+ + View change + View diff + +
+
+ + 0 + ? basePlanWithDeletedAutomations + : modifiedPlan + } + changeRequestState={changeRequestState} + milestonesWithAutomation={milestonesWithAutomation} + milestonesWithDeletedAutomation={ + milestonesWithDeletedAutomation + } + onUpdateAutomation={onUpdateChangeRequestSubmit} + onDeleteAutomation={onDeleteChangeRequestSubmit} + /> + + + + +
+ ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ProgressionChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ProgressionChange.tsx index c23aea943d..811ce54152 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ProgressionChange.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ProgressionChange.tsx @@ -17,7 +17,7 @@ import { } from './Change.styles.tsx'; import { styled } from '@mui/material'; import { MilestoneListRenderer } from './MilestoneListRenderer.tsx'; -import { useModifiedReleasePlan } from './useModifiedReleasePlan.ts'; +import { applyProgressionChanges } from './applyProgressionChanges'; const StyledTabs = styled(Tabs)(({ theme }) => ({ display: 'flex', @@ -69,7 +69,7 @@ export const ProgressionChange: FC = ({ )?.name || change.payload.targetMilestone : undefined; - const modifiedPlan = useModifiedReleasePlan(basePlan, [change]); + const modifiedPlan = applyProgressionChanges(basePlan, [change]); const previousMilestone = sourceMilestone; const newMilestone = modifiedPlan.milestones.find( diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ReleasePlanChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ReleasePlanChange.tsx index d29d8c23d7..a4f8012f0f 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ReleasePlanChange.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/ReleasePlanChange.tsx @@ -1,5 +1,5 @@ import { useRef, useState, type FC, type ReactNode } from 'react'; -import { Alert, styled, Typography } from '@mui/material'; +import { styled, Typography } from '@mui/material'; import type { ChangeRequestState, IChangeRequestAddReleasePlan, @@ -8,7 +8,6 @@ import type { IChangeRequestCreateMilestoneProgression, IChangeRequestUpdateMilestoneProgression, IChangeRequestDeleteMilestoneProgression, - IChangeRequestFeature, } from 'component/changeRequest/changeRequest.types'; import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview'; import { useFeatureReleasePlans } from 'hooks/api/getters/useFeatureReleasePlans/useFeatureReleasePlans'; @@ -29,9 +28,8 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import useToast from 'hooks/useToast'; import type { UpdateMilestoneProgressionSchema } from 'openapi'; -import { MilestoneListRenderer } from './MilestoneListRenderer.tsx'; -import { useModifiedReleasePlan } from './useModifiedReleasePlan.ts'; import { ProgressionChange } from './ProgressionChange.tsx'; +import { ConsolidatedProgressionChanges } from './ConsolidatedProgressionChanges.tsx'; const StyledTabs = styled(Tabs)(({ theme }) => ({ display: 'flex', @@ -239,177 +237,6 @@ const AddReleasePlan: FC<{ ); }; - -const ConsolidatedProgressionChanges: FC<{ - feature: IChangeRequestFeature; - currentReleasePlan?: IReleasePlan; - changeRequestState: ChangeRequestState; - onUpdateChangeRequestSubmit?: ( - sourceMilestoneId: string, - payload: UpdateMilestoneProgressionSchema, - ) => void; - onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void; -}> = ({ - feature, - currentReleasePlan, - changeRequestState, - onUpdateChangeRequestSubmit, - onDeleteChangeRequestSubmit, -}) => { - // Get all progression changes for this feature - const progressionChanges = feature.changes.filter( - ( - change, - ): change is - | IChangeRequestCreateMilestoneProgression - | IChangeRequestUpdateMilestoneProgression - | IChangeRequestDeleteMilestoneProgression => - change.action === 'createMilestoneProgression' || - change.action === 'updateMilestoneProgression' || - change.action === 'deleteMilestoneProgression', - ); - - if (progressionChanges.length === 0) return null; - - // Use snapshot from first change if available, otherwise use current release plan - const firstChangeWithSnapshot = - progressionChanges.find( - (change) => - change.payload?.snapshot && - (change.action === 'createMilestoneProgression' || - change.action === 'updateMilestoneProgression'), - ) || progressionChanges.find((change) => change.payload?.snapshot); - const basePlan = - firstChangeWithSnapshot?.payload?.snapshot || currentReleasePlan; - - if (!basePlan) { - console.error( - '[ConsolidatedProgressionChanges] No release plan data available', - { - hasSnapshot: !!firstChangeWithSnapshot, - hasCurrentPlan: !!currentReleasePlan, - progressionChanges, - }, - ); - return ( - - Unable to load release plan data. Please refresh the page. - - ); - } - - // Apply all progression changes using our hook - const modifiedPlan = useModifiedReleasePlan(basePlan, progressionChanges); - - // Collect milestone IDs with automation (modified or original) - const milestonesWithAutomation = new Set( - progressionChanges - .filter( - (change) => - change.action === 'createMilestoneProgression' || - change.action === 'updateMilestoneProgression', - ) - .map((change) => - change.action === 'createMilestoneProgression' - ? change.payload.sourceMilestone - : change.payload.sourceMilestoneId || - change.payload.sourceMilestone, - ) - .filter((id): id is string => Boolean(id)), - ); - - const milestonesWithDeletedAutomation = new Set( - progressionChanges - .filter((change) => change.action === 'deleteMilestoneProgression') - .map( - (change) => - change.payload.sourceMilestoneId || - change.payload.sourceMilestone, - ) - .filter((id): id is string => Boolean(id)), - ); - - const changeDescriptions = progressionChanges.map((change) => { - const sourceId = - change.action === 'createMilestoneProgression' - ? change.payload.sourceMilestone - : change.payload.sourceMilestoneId || - change.payload.sourceMilestone; - const sourceName = - basePlan.milestones.find((milestone) => milestone.id === sourceId) - ?.name || sourceId; - const action = - change.action === 'createMilestoneProgression' - ? 'Adding' - : change.action === 'deleteMilestoneProgression' - ? 'Deleting' - : 'Updating'; - return `${action} automation for ${sourceName}`; - }); - - // For diff view, we need to use basePlan with deleted automations shown - const basePlanWithDeletedAutomations: IReleasePlan = { - ...basePlan, - milestones: basePlan.milestones.map((milestone) => { - // If this milestone is being deleted, ensure it has its transition condition - if (milestonesWithDeletedAutomation.has(milestone.id)) { - return milestone; - } - return milestone; - }), - }; - - return ( - - - - {progressionChanges.map((change, index) => { - const Component = - change.action === 'deleteMilestoneProgression' - ? Deleted - : Added; - return ( - - {changeDescriptions[index]} - - ); - })} - -
- - View change - View diff - -
-
- - 0 - ? basePlanWithDeletedAutomations - : modifiedPlan - } - changeRequestState={changeRequestState} - milestonesWithAutomation={milestonesWithAutomation} - milestonesWithDeletedAutomation={ - milestonesWithDeletedAutomation - } - onUpdateAutomation={onUpdateChangeRequestSubmit} - onDeleteAutomation={onDeleteChangeRequestSubmit} - /> - - - - -
- ); -}; - export const ReleasePlanChange: FC<{ actions?: ReactNode; change: diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/applyProgressionChanges.ts b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/applyProgressionChanges.ts index 107ae33285..b3c9666d84 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/applyProgressionChanges.ts +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/applyProgressionChanges.ts @@ -25,12 +25,14 @@ export const applyProgressionChanges = ( const updateChange = progressionChanges.find( (change): change is IChangeRequestUpdateMilestoneProgression => change.action === 'updateMilestoneProgression' && - change.payload.sourceMilestoneId === milestone.id, + (change.payload.sourceMilestoneId === milestone.id || + change.payload.sourceMilestone === milestone.id), ); const deleteChange = progressionChanges.find( (change): change is IChangeRequestDeleteMilestoneProgression => change.action === 'deleteMilestoneProgression' && - change.payload.sourceMilestoneId === milestone.id, + (change.payload.sourceMilestoneId === milestone.id || + change.payload.sourceMilestone === milestone.id), ); if (deleteChange) { diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/useModifiedReleasePlan.ts b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/useModifiedReleasePlan.ts deleted file mode 100644 index db301e5db5..0000000000 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/useModifiedReleasePlan.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { IReleasePlan } from 'interfaces/releasePlans'; -import type { - IChangeRequestCreateMilestoneProgression, - IChangeRequestUpdateMilestoneProgression, - IChangeRequestDeleteMilestoneProgression, -} from 'component/changeRequest/changeRequest.types'; - -type ProgressionChange = - | IChangeRequestCreateMilestoneProgression - | IChangeRequestUpdateMilestoneProgression - | IChangeRequestDeleteMilestoneProgression; - -export const useModifiedReleasePlan = ( - basePlan: IReleasePlan, - progressionChanges: ProgressionChange[], -): IReleasePlan => { - return { - ...basePlan, - milestones: basePlan.milestones.map((milestone) => { - const createChange = progressionChanges.find( - (change): change is IChangeRequestCreateMilestoneProgression => - change.action === 'createMilestoneProgression' && - change.payload.sourceMilestone === milestone.id, - ); - const updateChange = progressionChanges.find( - (change): change is IChangeRequestUpdateMilestoneProgression => - change.action === 'updateMilestoneProgression' && - (change.payload.sourceMilestoneId === milestone.id || - change.payload.sourceMilestone === milestone.id), - ); - const deleteChange = progressionChanges.find( - (change): change is IChangeRequestDeleteMilestoneProgression => - change.action === 'deleteMilestoneProgression' && - (change.payload.sourceMilestoneId === milestone.id || - change.payload.sourceMilestone === milestone.id), - ); - - if (deleteChange) { - return { - ...milestone, - transitionCondition: null, - }; - } - - const change = updateChange || createChange; - if (change) { - return { - ...milestone, - transitionCondition: change.payload.transitionCondition, - }; - } - return milestone; - }), - }; -};