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

refactor: better type safety

This commit is contained in:
FredrikOseberg 2025-10-23 10:44:47 +02:00
parent e1d21adc9f
commit a3e4c1095d
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
5 changed files with 123 additions and 45 deletions

View File

@ -15,7 +15,10 @@ import {
Deleted, Deleted,
} from './Change.styles.tsx'; } from './Change.styles.tsx';
import type { ChangeMilestoneProgressionSchema } from 'openapi'; import type { ChangeMilestoneProgressionSchema } from 'openapi';
import { MilestoneListRenderer } from './MilestoneListRenderer.tsx'; import {
ReadonlyMilestoneListRenderer,
EditableMilestoneListRenderer,
} from './MilestoneListRenderer.tsx';
import { applyProgressionChanges } from './applyProgressionChanges.js'; import { applyProgressionChanges } from './applyProgressionChanges.js';
import { EventDiff } from 'component/events/EventDiff/EventDiff'; import { EventDiff } from 'component/events/EventDiff/EventDiff';
@ -72,11 +75,11 @@ export const ConsolidatedProgressionChanges: FC<{
feature: IChangeRequestFeature; feature: IChangeRequestFeature;
currentReleasePlan?: IReleasePlan; currentReleasePlan?: IReleasePlan;
changeRequestState: ChangeRequestState; changeRequestState: ChangeRequestState;
onUpdateChangeRequestSubmit?: ( onUpdateChangeRequestSubmit: (
sourceMilestoneId: string, sourceMilestoneId: string,
payload: ChangeMilestoneProgressionSchema, payload: ChangeMilestoneProgressionSchema,
) => Promise<void>; ) => Promise<void>;
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => Promise<void>; onDeleteChangeRequestSubmit: (sourceMilestoneId: string) => Promise<void>;
}> = ({ }> = ({
feature, feature,
currentReleasePlan, currentReleasePlan,
@ -113,6 +116,9 @@ export const ConsolidatedProgressionChanges: FC<{
basePlan, basePlan,
); );
const readonly =
changeRequestState === 'Applied' || changeRequestState === 'Cancelled';
return ( return (
<StyledTabs> <StyledTabs>
<ChangeItemWrapper> <ChangeItemWrapper>
@ -137,9 +143,17 @@ export const ConsolidatedProgressionChanges: FC<{
</div> </div>
</ChangeItemWrapper> </ChangeItemWrapper>
<TabPanel> <TabPanel>
<MilestoneListRenderer {readonly ? (
<ReadonlyMilestoneListRenderer
plan={modifiedPlan}
milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={
milestonesWithDeletedAutomation
}
/>
) : (
<EditableMilestoneListRenderer
plan={modifiedPlan} plan={modifiedPlan}
changeRequestState={changeRequestState}
milestonesWithAutomation={milestonesWithAutomation} milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={ milestonesWithDeletedAutomation={
milestonesWithDeletedAutomation milestonesWithDeletedAutomation
@ -147,6 +161,7 @@ export const ConsolidatedProgressionChanges: FC<{
onUpdateAutomation={onUpdateChangeRequestSubmit} onUpdateAutomation={onUpdateChangeRequestSubmit}
onDeleteAutomation={onDeleteChangeRequestSubmit} onDeleteAutomation={onDeleteChangeRequestSubmit}
/> />
)}
</TabPanel> </TabPanel>
<TabPanel variant='diff'> <TabPanel variant='diff'>
<EventDiff <EventDiff

View File

@ -1,7 +1,6 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import type { IReleasePlan } from 'interfaces/releasePlans'; import type { IReleasePlan } from 'interfaces/releasePlans';
import type { ChangeMilestoneProgressionSchema } from 'openapi'; import type { ChangeMilestoneProgressionSchema } from 'openapi';
import type { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone'; import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone';
import { MilestoneAutomationSection } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx'; import { MilestoneAutomationSection } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx';
import { MilestoneTransitionDisplay } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneTransitionDisplay.tsx'; import { MilestoneTransitionDisplay } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
@ -15,29 +14,26 @@ const StyledConnection = styled('div')(({ theme }) => ({
marginLeft: theme.spacing(3.25), marginLeft: theme.spacing(3.25),
})); }));
interface MilestoneListRendererProps { interface MilestoneListRendererCoreProps {
plan: IReleasePlan; plan: IReleasePlan;
changeRequestState: ChangeRequestState; readonly: boolean;
milestonesWithAutomation?: Set<string>; milestonesWithAutomation: Set<string>;
milestonesWithDeletedAutomation?: Set<string>; milestonesWithDeletedAutomation: Set<string>;
onUpdateAutomation?: ( onUpdateAutomation: (
sourceMilestoneId: string, sourceMilestoneId: string,
payload: ChangeMilestoneProgressionSchema, payload: ChangeMilestoneProgressionSchema,
) => Promise<void>; ) => Promise<void>;
onDeleteAutomation?: (sourceMilestoneId: string) => void; onDeleteAutomation: (sourceMilestoneId: string) => void;
} }
export const MilestoneListRenderer = ({ const MilestoneListRendererCore = ({
plan, plan,
changeRequestState, readonly,
milestonesWithAutomation = new Set(), milestonesWithAutomation,
milestonesWithDeletedAutomation = new Set(), milestonesWithDeletedAutomation,
onUpdateAutomation, onUpdateAutomation,
onDeleteAutomation, onDeleteAutomation,
}: MilestoneListRendererProps) => { }: MilestoneListRendererCoreProps) => {
// TODO: Split into read and write model at the type level to avoid having optional handlers
const readonly =
changeRequestState === 'Applied' || changeRequestState === 'Cancelled';
const status: MilestoneStatus = 'not-started'; const status: MilestoneStatus = 'not-started';
return ( return (
@ -70,14 +66,14 @@ export const MilestoneListRenderer = ({
} }
targetMilestoneId={nextMilestoneId} targetMilestoneId={nextMilestoneId}
onSave={async (payload) => { onSave={async (payload) => {
await onUpdateAutomation?.( await onUpdateAutomation(
milestone.id, milestone.id,
payload, payload,
); );
return { shouldReset: true }; return { shouldReset: true };
}} }}
onDelete={() => onDelete={() =>
onDeleteAutomation?.(milestone.id) onDeleteAutomation(milestone.id)
} }
milestoneName={milestone.name} milestoneName={milestone.name}
status={status} status={status}
@ -102,3 +98,56 @@ export const MilestoneListRenderer = ({
</> </>
); );
}; };
interface ReadonlyMilestoneListRendererProps {
plan: IReleasePlan;
milestonesWithAutomation?: Set<string>;
milestonesWithDeletedAutomation?: Set<string>;
}
export const ReadonlyMilestoneListRenderer = ({
plan,
milestonesWithAutomation = new Set(),
milestonesWithDeletedAutomation = new Set(),
}: ReadonlyMilestoneListRendererProps) => {
return (
<MilestoneListRendererCore
plan={plan}
readonly={true}
milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={milestonesWithDeletedAutomation}
onUpdateAutomation={async () => {}}
onDeleteAutomation={() => {}}
/>
);
};
interface EditableMilestoneListRendererProps {
plan: IReleasePlan;
milestonesWithAutomation?: Set<string>;
milestonesWithDeletedAutomation?: Set<string>;
onUpdateAutomation: (
sourceMilestoneId: string,
payload: ChangeMilestoneProgressionSchema,
) => Promise<void>;
onDeleteAutomation: (sourceMilestoneId: string) => void;
}
export const EditableMilestoneListRenderer = ({
plan,
milestonesWithAutomation = new Set(),
milestonesWithDeletedAutomation = new Set(),
onUpdateAutomation,
onDeleteAutomation,
}: EditableMilestoneListRendererProps) => {
return (
<MilestoneListRendererCore
plan={plan}
readonly={false}
milestonesWithAutomation={milestonesWithAutomation}
milestonesWithDeletedAutomation={milestonesWithDeletedAutomation}
onUpdateAutomation={onUpdateAutomation}
onDeleteAutomation={onDeleteAutomation}
/>
);
};

View File

@ -10,7 +10,10 @@ import { EventDiff } from 'component/events/EventDiff/EventDiff';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx'; import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
import { Action, ChangeItemInfo, ChangeItemWrapper } from './Change.styles.tsx'; import { Action, ChangeItemInfo, ChangeItemWrapper } from './Change.styles.tsx';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { MilestoneListRenderer } from './MilestoneListRenderer.tsx'; import {
ReadonlyMilestoneListRenderer,
EditableMilestoneListRenderer,
} from './MilestoneListRenderer.tsx';
import { applyProgressionChanges } from './applyProgressionChanges.ts'; import { applyProgressionChanges } from './applyProgressionChanges.ts';
const StyledTabs = styled(Tabs)(({ theme }) => ({ const StyledTabs = styled(Tabs)(({ theme }) => ({
@ -24,11 +27,11 @@ interface ProgressionChangeProps {
currentReleasePlan?: IReleasePlan; currentReleasePlan?: IReleasePlan;
actions?: ReactNode; actions?: ReactNode;
changeRequestState: ChangeRequestState; changeRequestState: ChangeRequestState;
onUpdateChangeRequestSubmit?: ( onUpdateChangeRequestSubmit: (
sourceMilestoneId: string, sourceMilestoneId: string,
payload: ChangeMilestoneProgressionSchema, payload: ChangeMilestoneProgressionSchema,
) => Promise<void>; ) => Promise<void>;
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void; onDeleteChangeRequestSubmit: (sourceMilestoneId: string) => void;
} }
export const ProgressionChange: FC<ProgressionChangeProps> = ({ export const ProgressionChange: FC<ProgressionChangeProps> = ({
@ -62,6 +65,9 @@ export const ProgressionChange: FC<ProgressionChangeProps> = ({
(milestone) => milestone.id === sourceId, (milestone) => milestone.id === sourceId,
); );
const readonly =
changeRequestState === 'Applied' || changeRequestState === 'Cancelled';
return ( return (
<StyledTabs> <StyledTabs>
<ChangeItemWrapper> <ChangeItemWrapper>
@ -80,15 +86,23 @@ export const ProgressionChange: FC<ProgressionChangeProps> = ({
</div> </div>
</ChangeItemWrapper> </ChangeItemWrapper>
<TabPanel> <TabPanel>
<MilestoneListRenderer {readonly ? (
<ReadonlyMilestoneListRenderer
plan={modifiedPlan}
milestonesWithAutomation={
new Set([sourceId].filter(Boolean))
}
/>
) : (
<EditableMilestoneListRenderer
plan={modifiedPlan} plan={modifiedPlan}
changeRequestState={changeRequestState}
milestonesWithAutomation={ milestonesWithAutomation={
new Set([sourceId].filter(Boolean)) new Set([sourceId].filter(Boolean))
} }
onUpdateAutomation={onUpdateChangeRequestSubmit} onUpdateAutomation={onUpdateChangeRequestSubmit}
onDeleteAutomation={onDeleteChangeRequestSubmit} onDeleteAutomation={onDeleteChangeRequestSubmit}
/> />
)}
</TabPanel> </TabPanel>
<TabPanel variant='diff'> <TabPanel variant='diff'>
<EventDiff <EventDiff

View File

@ -8,7 +8,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { calculateMilestoneStatus } from './milestoneStatusUtils.js'; import { calculateMilestoneStatus } from './milestoneStatusUtils.js';
import { usePendingProgressionChanges } from './usePendingProgressionChanges.js'; import { getPendingProgressionData } from './pendingProgressionChanges.js';
import { MilestoneAutomation } from './MilestoneAutomation.tsx'; import { MilestoneAutomation } from './MilestoneAutomation.tsx';
const StyledConnection = styled('div', { const StyledConnection = styled('div', {
@ -130,7 +130,7 @@ export const ReleasePlanMilestoneItem = ({
); );
const { pendingProgressionChange, effectiveTransitionCondition } = const { pendingProgressionChange, effectiveTransitionCondition } =
usePendingProgressionChanges(milestone, getPendingProgressionChange); getPendingProgressionData(milestone, getPendingProgressionChange);
const shouldShowAutomation = const shouldShowAutomation =
isNotLastMilestone && milestoneProgressionsEnabled; isNotLastMilestone && milestoneProgressionsEnabled;

View File

@ -9,7 +9,7 @@ interface PendingProgressionChangeResult {
effectiveTransitionCondition: IReleasePlanMilestone['transitionCondition']; effectiveTransitionCondition: IReleasePlanMilestone['transitionCondition'];
} }
export const usePendingProgressionChanges = ( export const getPendingProgressionData = (
milestone: IReleasePlanMilestone, milestone: IReleasePlanMilestone,
getPendingProgressionChange: IReleasePlanMilestoneItemProps['getPendingProgressionChange'], getPendingProgressionChange: IReleasePlanMilestoneItemProps['getPendingProgressionChange'],
): PendingProgressionChangeResult => { ): PendingProgressionChangeResult => {