1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

refactor: separate component

This commit is contained in:
FredrikOseberg 2025-10-22 10:39:38 +02:00
parent 8e612e47f4
commit cb9fa72de1
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
5 changed files with 197 additions and 234 deletions

View File

@ -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<void>;
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => Promise<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) {
return (
<Alert severity='error'>
Unable to load release plan data. Please refresh the page.
</Alert>
);
}
// 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 (
<StyledTabs>
<ChangeItemWrapper>
<ChangeItemInfo>
{progressionChanges.map((change, index) => {
const Component =
change.action === 'deleteMilestoneProgression'
? Deleted
: Added;
return (
<Component key={index}>
{changeDescriptions[index]}
</Component>
);
})}
</ChangeItemInfo>
<div>
<TabList>
<Tab>View change</Tab>
<Tab>View diff</Tab>
</TabList>
</div>
</ChangeItemWrapper>
<TabPanel>
<MilestoneListRenderer
plan={
milestonesWithDeletedAutomation.size > 0
? basePlanWithDeletedAutomations
: modifiedPlan
}
changeRequestState={changeRequestState}
milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={
milestonesWithDeletedAutomation
}
onUpdateAutomation={onUpdateChangeRequestSubmit}
onDeleteAutomation={onDeleteChangeRequestSubmit}
/>
</TabPanel>
<TabPanel variant='diff'>
<EventDiff
entry={{
preData: basePlan,
data: modifiedPlan,
}}
/>
</TabPanel>
</StyledTabs>
);
};

View File

@ -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<ProgressionChangeProps> = ({
)?.name || change.payload.targetMilestone
: undefined;
const modifiedPlan = useModifiedReleasePlan(basePlan, [change]);
const modifiedPlan = applyProgressionChanges(basePlan, [change]);
const previousMilestone = sourceMilestone;
const newMilestone = modifiedPlan.milestones.find(

View File

@ -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 (
<Alert severity='error'>
Unable to load release plan data. Please refresh the page.
</Alert>
);
}
// 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 (
<StyledTabs>
<ChangeItemWrapper>
<ChangeItemInfo>
{progressionChanges.map((change, index) => {
const Component =
change.action === 'deleteMilestoneProgression'
? Deleted
: Added;
return (
<Component key={index}>
{changeDescriptions[index]}
</Component>
);
})}
</ChangeItemInfo>
<div>
<TabList>
<Tab>View change</Tab>
<Tab>View diff</Tab>
</TabList>
</div>
</ChangeItemWrapper>
<TabPanel>
<MilestoneListRenderer
plan={
milestonesWithDeletedAutomation.size > 0
? basePlanWithDeletedAutomations
: modifiedPlan
}
changeRequestState={changeRequestState}
milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={
milestonesWithDeletedAutomation
}
onUpdateAutomation={onUpdateChangeRequestSubmit}
onDeleteAutomation={onDeleteChangeRequestSubmit}
/>
</TabPanel>
<TabPanel variant='diff'>
<EventDiff
entry={{
preData: basePlan,
data: modifiedPlan,
}}
/>
</TabPanel>
</StyledTabs>
);
};
export const ReleasePlanChange: FC<{
actions?: ReactNode;
change:

View File

@ -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) {

View File

@ -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;
}),
};
};