1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

chore: prepend changes with "change:" (#10276)

Adds "change:" to the beginning of all changes and does some work to
align the use of compononents and structure across them (supersedes
https://github.com/Unleash/unleash/pull/10260).

In doing so, I have also added new and legacy variants for all different
change components, because this has required some hierarchy
restructuring every now and then. A reason for doing that was adding the
correct wrapping behavior for components, such that on smaller screens,
we wouldn't entirely blow out and make the kebab menu invisible and
inaccessible.

It also makes it so that we switch to full-width change view earlier (at
breakpoint md instead of sm), because at sm, a lot of stuff got hidden
before we switched to full-width.

Most changes are trivial updates; I've called out bits of the code that
are not in comments.

Rendered, it looks like this:
<img width="1203" alt="image"
src="https://github.com/user-attachments/assets/36bed974-99da-4d8d-a881-ea9df7797210"
/>

One interesting and potentially quite useful side-effect, is that all
change types now use the exact same set of components in the same
fashion, as evidenced by this screenie where I've added outlines to the
hierarchy:

<img width="1020" alt="image"
src="https://github.com/user-attachments/assets/685fefcc-af7e-4697-b8f3-8260af1e2a84"
/>

The one difference is that components without a diff place the "more"
kebab menu one layer further inside to facilitate prettier wrapping (the
kebab menu can stay on the same line as the other text when wrapping):
<img width="238" alt="image"
src="https://github.com/user-attachments/assets/2b8d3174-06a8-4ad4-b366-cea97720deda"
/>
This commit is contained in:
Thomas Heartman 2025-07-03 11:14:04 +02:00 committed by GitHub
parent 89cff9d533
commit 2dc7cbaa31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 362 additions and 208 deletions

View File

@ -1,22 +1,38 @@
import type { FC, ReactNode } from 'react';
import { Box, styled } from '@mui/material';
import { ChangeItemWrapper } from './StrategyChange.tsx';
import { Action, ChangeItemInfo, ChangeItemWrapper } from './Change.styles.tsx';
import { styled } from '@mui/material';
import { ChangeItemWrapper as LegacyChangeItemWrapper } from './LegacyStrategyChange.tsx';
const ArchiveBox = styled(Box)(({ theme }) => ({
type ArchiveFeatureChange = {
actions?: ReactNode;
};
const ArchiveBox = styled('span')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
color: theme.palette.error.main,
}));
interface IArchiveFeatureChange {
actions?: ReactNode;
}
export const ArchiveFeatureChange: FC<IArchiveFeatureChange> = ({
/**
* Deprecated: use ArchiveFeatureChange instead; remove with flag crDiffView
* @deprecated
*/
export const LegacyArchiveFeatureChange: FC<ArchiveFeatureChange> = ({
actions,
}) => (
<LegacyChangeItemWrapper>
<ChangeItemInfo>
<ArchiveBox>Archiving flag</ArchiveBox>
{actions}
</ChangeItemInfo>
</LegacyChangeItemWrapper>
);
export const ArchiveFeatureChange: FC<ArchiveFeatureChange> = ({ actions }) => (
<ChangeItemWrapper>
<ArchiveBox>Archiving flag</ArchiveBox>
{actions}
<ChangeItemInfo>
<Action color='error.main'>Archiving flag</Action>
{actions}
</ChangeItemInfo>
</ChangeItemWrapper>
);

View File

@ -1,9 +1,53 @@
import { styled, Typography } from '@mui/material';
import { styled, Typography, type TypographyProps } from '@mui/material';
import type { FC, PropsWithChildren } from 'react';
export const ChangeItemInfo = styled(Typography)(({ theme }) => ({
export const Action: FC<TypographyProps> = ({ children, ...props }) => (
<Typography component='span' {...props}>
{children}
</Typography>
);
export const Deleted = styled(Action)(({ theme }) => ({
color: theme.palette.error.main,
'::before': { content: '"- "' },
}));
export const Added = styled(Action)(({ theme }) => ({
'::before': { content: '"+ "' },
color: theme.palette.success.dark,
}));
export const AddedStrategy = styled(Added, {
shouldForwardProp: (prop) => prop !== 'disabled',
})<{ disabled?: boolean }>(({ theme, disabled }) => ({
color: disabled ? theme.palette.text.secondary : undefined,
}));
const Change = styled('span')({
fontWeight: 'bold',
});
export const ChangeItemInfo = styled(
({ children, ...props }: PropsWithChildren) => (
<Typography {...props}>
<Change>Change: </Change>
{children}
</Typography>
),
)(({ theme }) => ({
display: 'flex',
flexFlow: 'row',
justifyItems: 'flex-start',
flexFlow: 'row wrap',
alignItems: 'center',
flex: 'auto',
gap: `1ch`,
columnGap: `1ch`,
rowGap: theme.spacing(0.5),
}));
export const ChangeItemWrapper = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
justifyContent: 'space-between',
alignItems: 'center',
gap: theme.spacing(1),
}));

View File

@ -28,6 +28,7 @@ import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import MoreVert from '@mui/icons-material/MoreVert';
import { EditChange } from './EditChange.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
const useShowActions = (changeRequest: ChangeRequestType, change: IChange) => {
const { isChangeRequestConfigured } = useChangeRequestsEnabled(
@ -60,6 +61,10 @@ const StyledPopover = styled(Popover)(({ theme }) => ({
padding: theme.spacing(1, 1.5),
}));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
marginLeft: 'auto',
}));
export const ChangeActions: FC<{
changeRequest: ChangeRequestType;
feature: string;
@ -69,6 +74,9 @@ export const ChangeActions: FC<{
const { showDiscard, showEdit } = useShowActions(changeRequest, change);
const { discardChange } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const useNewCrView = useUiFlag('crDiffView');
const ButtonComponent = useNewCrView ? StyledIconButton : IconButton;
const [editOpen, setEditOpen] = useState(false);
@ -113,7 +121,7 @@ export const ChangeActions: FC<{
show={
<>
<Tooltip title='Change request actions' arrow describeChild>
<IconButton
<ButtonComponent
id={id}
aria-controls={open ? menuId : undefined}
aria-haspopup='true'
@ -122,7 +130,7 @@ export const ChangeActions: FC<{
type='button'
>
<MoreVert />
</IconButton>
</ButtonComponent>
</Tooltip>
<StyledPopover
id={menuId}

View File

@ -1,24 +0,0 @@
import type { FC } from 'react';
import { styled } from '@mui/material';
import { textTruncated } from 'themes/themeStyles';
import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
type ChangeSegmentNameProps = {
name?: string;
previousName?: string;
};
const Truncated = styled('div')(() => ({
...textTruncated,
maxWidth: 500,
display: 'flex',
}));
export const ChangeSegmentName: FC<ChangeSegmentNameProps> = ({
name,
previousName,
}) => (
<Truncated>
<NameWithChangeInfo previousName={previousName} newName={name} />
</Truncated>
);

View File

@ -41,11 +41,15 @@ export const Tab = styled(({ children }: ButtonProps) => (
textTransform: 'uppercase',
}));
export const Tabs = ({ children }: PropsWithChildren) => (
export const Tabs = ({
className,
children,
}: PropsWithChildren<{ className?: string }>) => (
<MuiTabs
aria-label='View rendered change or JSON diff'
selectionFollowsFocus
defaultValue={0}
className={className}
>
{children}
</MuiTabs>

View File

@ -1,11 +1,17 @@
import type { ReactNode, VFC } from 'react';
import type { FC, ReactNode } from 'react';
import { Box, styled, Typography } from '@mui/material';
import type {
IChangeRequestAddDependency,
IChangeRequestDeleteDependency,
} from 'component/changeRequest/changeRequest.types';
import { Link } from 'react-router-dom';
import { ChangeItemWrapper } from './StrategyChange.tsx';
import {
Added,
ChangeItemInfo,
ChangeItemWrapper,
Deleted,
} from './Change.styles';
import { ChangeItemWrapper as LegacyChangeItemWrapper } from './LegacyStrategyChange.tsx';
const StyledLink = styled(Link)(({ theme }) => ({
maxWidth: '100%',
@ -15,13 +21,55 @@ const StyledLink = styled(Link)(({ theme }) => ({
},
}));
export const DependencyChange: FC<{
actions?: ReactNode;
change: IChangeRequestAddDependency | IChangeRequestDeleteDependency;
projectId: string;
onNavigate?: () => void;
}> = ({ actions, change, projectId, onNavigate }) => {
if (change.action === 'addDependency') {
return (
<ChangeItemWrapper>
<ChangeItemInfo>
<Added>Adding dependency</Added>
<StyledLink
to={`/projects/${projectId}/features/${change.payload.feature}`}
onClick={onNavigate}
>
{change.payload.feature}
</StyledLink>
{!change.payload.enabled ? ' (disabled)' : null}
{change.payload.variants?.length
? `(${change.payload.variants?.join(', ')})`
: null}
{actions}
</ChangeItemInfo>
</ChangeItemWrapper>
);
}
if (change.action === 'deleteDependency') {
return (
<ChangeItemWrapper>
<ChangeItemInfo>
<Deleted>Deleting dependencies</Deleted>
{actions}
</ChangeItemInfo>
</ChangeItemWrapper>
);
}
};
const AddDependencyWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}));
export const DependencyChange: VFC<{
/**
* @deprecated use DependencyChange instead; remove with flag crDiffView
*/
export const LegacyDependencyChange: FC<{
actions?: ReactNode;
change: IChangeRequestAddDependency | IChangeRequestDeleteDependency;
projectId: string;
@ -31,7 +79,7 @@ export const DependencyChange: VFC<{
<>
{change.action === 'addDependency' && (
<>
<ChangeItemWrapper>
<LegacyChangeItemWrapper>
<AddDependencyWrapper>
<Typography color={'success.dark'}>
+ Adding dependency
@ -48,7 +96,7 @@ export const DependencyChange: VFC<{
: null}
</AddDependencyWrapper>
{actions}
</ChangeItemWrapper>
</LegacyChangeItemWrapper>
</>
)}
{change.action === 'deleteDependency' && (

View File

@ -9,12 +9,21 @@ import { formatStrategyName } from '../../../../../../utils/strategyNames.tsx';
import type { IFeatureStrategy } from 'interfaces/strategy.ts';
import { Tab, TabList, TabPanel, Tabs } from '../ChangeTabComponents.tsx';
import { useUiFlag } from 'hooks/useUiFlag.ts';
import {
ChangeItemInfo as NewChangeItemInfo,
ChangeItemWrapper,
Action,
} from '../Change.styles.tsx';
const ChangeItemInfo = styled(Box)({
display: 'flex',
flexDirection: 'column',
});
const ChangeContent = styled(ChangeItemInfo)(({ theme }) => ({
gap: theme.spacing(2),
}));
const StyledChangeHeader = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
@ -91,9 +100,13 @@ export const EnvironmentStrategyExecutionOrder = ({
if (useDiffableComponent) {
return (
<Tabs>
<ChangeItemInfo>
<StyledChangeHeader>
<p>Updating strategy execution order to</p>
<ChangeContent>
<ChangeItemWrapper>
<NewChangeItemInfo>
<Action>
Updating strategy execution order to
</Action>
</NewChangeItemInfo>
<div>
<TabList>
<Tab>Change</Tab>
@ -101,7 +114,7 @@ export const EnvironmentStrategyExecutionOrder = ({
</TabList>
{actions}
</div>
</StyledChangeHeader>
</ChangeItemWrapper>
<TabPanel>
<StyledStrategyExecutionWrapper>
{updatedStrategies.map((strategy, index) => (
@ -120,7 +133,7 @@ export const EnvironmentStrategyExecutionOrder = ({
data={data}
/>
</TabPanel>
</ChangeItemInfo>
</ChangeContent>
</Tabs>
);
}

View File

@ -7,12 +7,21 @@ import type {
import { objectId } from 'utils/objectId';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert, Box, styled } from '@mui/material';
import { ToggleStatusChange } from './ToggleStatusChange.tsx';
import {
LegacyToggleStatusChange,
ToggleStatusChange,
} from './ToggleStatusChange.tsx';
import { LegacyStrategyChange } from './LegacyStrategyChange.tsx';
import { VariantPatch } from './VariantPatch/VariantPatch.tsx';
import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder.tsx';
import { ArchiveFeatureChange } from './ArchiveFeatureChange.tsx';
import { DependencyChange } from './DependencyChange.tsx';
import {
ArchiveFeatureChange,
LegacyArchiveFeatureChange,
} from './ArchiveFeatureChange.tsx';
import {
DependencyChange,
LegacyDependencyChange,
} from './DependencyChange.tsx';
import { Link } from 'react-router-dom';
import { LegacyReleasePlanChange } from './LegacyReleasePlanChange.tsx';
import { ReleasePlanChange } from './ReleasePlanChange.tsx';
@ -100,6 +109,18 @@ export const FeatureChange: FC<{
? ReleasePlanChange
: LegacyReleasePlanChange;
const ArchiveFlagComponent = useDiffableChangeComponent
? ArchiveFeatureChange
: LegacyArchiveFeatureChange;
const DependencyChangeComponent = useDiffableChangeComponent
? DependencyChange
: LegacyDependencyChange;
const StatusChangeComponent = useDiffableChangeComponent
? ToggleStatusChange
: LegacyToggleStatusChange;
return (
<StyledSingleChangeBox
key={objectId(change)}
@ -159,7 +180,7 @@ export const FeatureChange: FC<{
<ChangeInnerBox>
{(change.action === 'addDependency' ||
change.action === 'deleteDependency') && (
<DependencyChange
<DependencyChangeComponent
actions={actions}
change={change}
projectId={changeRequest.project}
@ -167,13 +188,13 @@ export const FeatureChange: FC<{
/>
)}
{change.action === 'updateEnabled' && (
<ToggleStatusChange
<StatusChangeComponent
enabled={change.payload.enabled}
actions={actions}
/>
)}
{change.action === 'archiveFeature' && (
<ArchiveFeatureChange actions={actions} />
<ArchiveFlagComponent actions={actions} />
)}
{change.action === 'addStrategy' ||

View File

@ -19,7 +19,7 @@ test.each(['', undefined])(
).toBeNull();
// expect ins element with new strategy name
await screen.findByText(newName, { selector: 'p' });
await screen.findByText(newName, { selector: 'ins' });
},
);
@ -35,7 +35,9 @@ test.each(['', undefined])(
);
// expect no ins elements
expect(screen.queryByText(newName || '', { selector: 'p' })).toBeNull();
expect(
screen.queryByText(newName || '', { selector: 'ins' }),
).toBeNull();
// expect del element with old strategy name
await screen.findByText(previousName, { selector: 'del' });
@ -53,13 +55,25 @@ test('Should render the old name as deleted and the new name as inserted if the
await screen.findByText(previousName, { selector: 'del' });
// expect ins element with new strategy name
await screen.findByText(newName, { selector: 'p' });
await screen.findByText(newName, { selector: 'ins' });
});
test('Should render the name in a span if it has not changed', async () => {
const name = 'name';
render(<NameWithChangeInfo newName={name} previousName={name} />);
// expect no del or ins elements
expect(screen.queryByText(name, { selector: 'ins' })).toBeNull();
expect(screen.queryByText(name, { selector: 'del' })).toBeNull();
// expect span element with the strategy name
await screen.findByText(name, { selector: 'span' });
});
test('Should render nothing if there was no name and there is still no name', async () => {
render(<NameWithChangeInfo newName={undefined} previousName={undefined} />);
expect(screen.queryByText('', { selector: 'p' })).toBeNull();
expect(screen.queryByText('', { selector: 'ins' })).toBeNull();
expect(screen.queryByText('', { selector: 'del' })).toBeNull();
expect(screen.queryByText('', { selector: 'span' })).toBeNull();
});

View File

@ -1,18 +1,26 @@
import type { FC } from 'react';
import { Typography, styled } from '@mui/material';
import { Typography, type TypographyProps, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { textTruncated } from 'themes/themeStyles';
const Truncated = styled('div')(() => ({
const Truncated = styled('span')(() => ({
...textTruncated,
maxWidth: 500,
display: 'block',
}));
const NewName = styled(Typography)<TypographyProps>({
textDecoration: 'none',
});
export const NameWithChangeInfo: FC<{
newName: string | undefined;
previousName: string | undefined;
newName?: string;
previousName?: string;
}> = ({ newName, previousName }) => {
const titleHasChanged = Boolean(previousName && previousName !== newName);
const titleHasChangedOrBeenAdded = Boolean(
titleHasChanged || (!previousName && newName),
);
return (
<>
@ -30,7 +38,13 @@ export const NameWithChangeInfo: FC<{
condition={Boolean(newName)}
show={
<Truncated>
<Typography>{newName}</Typography>
<NewName
component={
titleHasChangedOrBeenAdded ? 'ins' : 'span'
}
>
{newName}
</NewName>
</Truncated>
}
/>

View File

@ -1,5 +1,5 @@
import { useRef, useState, type FC, type ReactNode } from 'react';
import { Box, styled, Typography } from '@mui/material';
import { styled, Typography } from '@mui/material';
import type {
ChangeRequestState,
IChangeRequestAddReleasePlan,
@ -14,22 +14,18 @@ import { ReleasePlan } from 'component/feature/FeatureView/FeatureOverview/Relea
import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone';
import type { IReleasePlan } from 'interfaces/releasePlans';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
import { ChangeItemInfo } from './Change.styles.tsx';
import {
Action,
Added,
ChangeItemInfo,
ChangeItemWrapper,
Deleted,
} from './Change.styles.tsx';
export const ChangeItemWrapper = styled(Box)({
const StyledTabs = styled(Tabs)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
const ChangeItemCreateEditDeleteWrapper = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: 'auto auto',
justifyContent: 'space-between',
flexFlow: 'column',
gap: theme.spacing(1),
alignItems: 'center',
marginBottom: theme.spacing(2),
width: '100%',
}));
const DeleteReleasePlan: FC<{
@ -47,19 +43,13 @@ const DeleteReleasePlan: FC<{
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography
sx={(theme) => ({
color: theme.palette.error.main,
})}
>
- Deleting release plan
</Typography>
<Typography>{releasePlan.name}</Typography>
<Deleted>Deleting release plan</Deleted>
<Typography component='span'>{releasePlan.name}</Typography>
{actions}
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<ReleasePlan plan={releasePlan} readonly />
</>
);
@ -89,13 +79,13 @@ const StartMilestone: FC<{
if (!newMilestone) return;
return (
<Tabs>
<ChangeItemCreateEditDeleteWrapper>
<StyledTabs>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography color='success.dark'>
+ Start milestone
<Added>Start milestone</Added>
<Typography component='span'>
{newMilestone.name}
</Typography>
<Typography>{newMilestone.name}</Typography>
</ChangeItemInfo>
<div>
<TabList>
@ -104,7 +94,7 @@ const StartMilestone: FC<{
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<TabPanel>
<ReleasePlanMilestone readonly milestone={newMilestone} />
</TabPanel>
@ -116,7 +106,7 @@ const StartMilestone: FC<{
}}
/>
</TabPanel>
</Tabs>
</StyledTabs>
);
};
@ -162,25 +152,25 @@ const AddReleasePlan: FC<{
if (!currentReleasePlan) {
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography color='success.dark'>
+ Adding release plan
<Added>Adding release plan</Added>
<Typography component='span'>
{planPreview.name}
</Typography>
<Typography>{planPreview.name}</Typography>
{actions}
</ChangeItemInfo>
<div>{actions}</div>
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<ReleasePlan plan={planPreview} readonly />
</>
);
}
return (
<Tabs>
<ChangeItemCreateEditDeleteWrapper>
<StyledTabs>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography>
<Action>
Replacing{' '}
<TooltipLink
tooltip={
@ -207,9 +197,8 @@ const AddReleasePlan: FC<{
current
</span>
</TooltipLink>{' '}
release plan with
</Typography>
<Typography>{planPreview.name}</Typography>
release plan with {planPreview.name}
</Action>
</ChangeItemInfo>
<div>
<TabList>
@ -218,7 +207,7 @@ const AddReleasePlan: FC<{
</TabList>
{actions}
</div>
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<TabPanel>
<ReleasePlan plan={planPreview} readonly />
</TabPanel>
@ -230,7 +219,7 @@ const AddReleasePlan: FC<{
}}
/>
</TabPanel>
</Tabs>
</StyledTabs>
);
};

View File

@ -1,5 +1,5 @@
import type { FC, ReactNode } from 'react';
import { Box, styled, Typography } from '@mui/material';
import { Box, styled } from '@mui/material';
import type {
ChangeRequestState,
IChangeRequestDeleteSegment,
@ -10,34 +10,31 @@ import { ViewableConstraintsList } from 'component/common/NewConstraintAccordion
import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning.tsx';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
import { ChangeItemInfo } from './Change.styles.tsx';
import { ChangeSegmentName } from './ChangeSegmentName.tsx';
import {
Action,
ChangeItemInfo,
ChangeItemWrapper,
Deleted,
} from './Change.styles.tsx';
import { SegmentDiff } from './SegmentDiff.tsx';
import { NameWithChangeInfo } from './NameWithChangeInfo/NameWithChangeInfo.tsx';
const ChangeItemCreateEditWrapper = styled(Box)(({ theme }) => ({
const ActionsContainer = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
flexFlow: 'row wrap',
alignItems: 'center',
width: '100%',
margin: theme.spacing(0, 0, 1, 0),
columnGap: theme.spacing(1),
}));
export const ChangeItemWrapper = styled(Box)({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
const SegmentContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'conflict',
})<{ conflict: string | undefined }>(({ theme, conflict }) => ({
borderLeft: '1px solid',
borderRight: '1px solid',
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: conflict
? theme.palette.warning.border
: theme.palette.divider,
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(1),
border: `1px solid ${
conflict ? theme.palette.warning.border : theme.palette.divider
}`,
borderTopColor: theme.palette.divider,
padding: theme.spacing(3),
borderRadius: `0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`,
@ -58,13 +55,13 @@ export const SegmentChangeDetails: FC<{
changeRequestState === 'Applied' ? snapshotSegment : currentSegment;
const actionsWithTabs = (
<>
<ActionsContainer>
<TabList>
<Tab>Change</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</>
</ActionsContainer>
);
return (
@ -74,22 +71,16 @@ export const SegmentChangeDetails: FC<{
<>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography
sx={(theme) => ({
color: theme.palette.error.main,
})}
>
- Deleting segment
</Typography>
<ChangeSegmentName
name={change.payload.name}
<Deleted>Deleting segment</Deleted>
<NameWithChangeInfo
newName={change.payload.name}
previousName={previousName}
/>
</ChangeItemInfo>
{actionsWithTabs}
</ChangeItemWrapper>
<TabPanel />
<TabPanel sx={{ display: 'contents' }} />
<TabPanel sx={{ mt: 1 }} variant='diff'>
<SegmentDiff
change={change}
@ -108,13 +99,15 @@ export const SegmentChangeDetails: FC<{
}}
changeRequestState={changeRequestState}
/>
<ChangeItemCreateEditWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography>Editing segment</Typography>
<ChangeSegmentName name={change.payload.name} />
<Action>Editing segment</Action>
<NameWithChangeInfo
newName={change.payload.name}
/>
</ChangeItemInfo>
{actionsWithTabs}
</ChangeItemCreateEditWrapper>
</ChangeItemWrapper>
<TabPanel>
<ViewableConstraintsList

View File

@ -242,7 +242,7 @@ test('Deleting strategy before change request is applied diffs against current s
{ route: `/projects/${projectId}` },
);
await screen.findByText('- Deleting strategy');
await screen.findByText('Deleting strategy');
await screen.findByText('Gradual rollout');
await screen.findByText('current_title');
@ -298,7 +298,7 @@ test('Deleting strategy after change request is applied diffs against the snapsh
{ route: `/projects/${projectId}` },
);
await screen.findByText('- Deleting strategy');
await screen.findByText('Deleting strategy');
await screen.findByText('Gradual rollout');
await screen.findByText('snapshot_title');
expect(screen.queryByText('current_title')).not.toBeInTheDocument();
@ -352,7 +352,7 @@ test('Adding strategy always diffs against undefined strategy', async () => {
{ route: `/projects/${projectId}` },
);
await screen.findByText('+ Adding strategy');
await screen.findByText('Adding strategy');
await screen.findByText('change_request_title');
const variants = await screen.findAllByText('change_variant');

View File

@ -20,22 +20,13 @@ import type { IFeatureStrategy } from 'interfaces/strategy';
import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
import { ChangeStrategyName } from './ChangeStrategyName.tsx';
import { StrategyDiff } from './StrategyDiff.tsx';
import { ChangeItemInfo } from './Change.styles.tsx';
export const ChangeItemWrapper = styled(Box)({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
const ChangeItemCreateEditDeleteWrapper = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(1),
alignItems: 'center',
marginBottom: theme.spacing(1),
width: '100%',
}));
import {
Action,
AddedStrategy,
ChangeItemInfo,
ChangeItemWrapper,
Deleted,
} from './Change.styles.tsx';
const StyledBox: FC<{ children?: React.ReactNode }> = styled(Box)(
({ theme }) => ({
@ -60,7 +51,7 @@ const DisabledEnabledState: FC<{ show?: boolean; disabled: boolean }> = ({
if (disabled) {
return (
<Tooltip
title='This strategy will not be taken into account when evaluating feature flag.'
title='This strategy will not be taken into account when evaluating the feature flag.'
arrow
sx={{ cursor: 'pointer' }}
>
@ -73,7 +64,7 @@ const DisabledEnabledState: FC<{ show?: boolean; disabled: boolean }> = ({
return (
<Tooltip
title='This was disabled before and with this change it will be taken into account when evaluating feature flag.'
title='This strategy was disabled before. With this change, it will be taken into account when evaluating the feature flag.'
arrow
sx={{ cursor: 'pointer' }}
>
@ -89,18 +80,18 @@ const EditHeader: FC<{
willBeDisabled?: boolean;
}> = ({ wasDisabled = false, willBeDisabled = false }) => {
if (wasDisabled && willBeDisabled) {
return <Typography color='text.secondary'>Editing strategy</Typography>;
return <Action color='text.secondary'>Editing strategy</Action>;
}
if (!wasDisabled && willBeDisabled) {
return <Typography color='error.dark'>Editing strategy</Typography>;
return <Action color='error.dark'>Editing strategy</Action>;
}
if (wasDisabled && !willBeDisabled) {
return <Typography color='success.dark'>Editing strategy</Typography>;
return <Action color='success.dark'>Editing strategy</Action>;
}
return <Typography>Editing strategy</Typography>;
return <Action>Editing strategy</Action>;
};
const hasDiff = (object: unknown, objectToCompare: unknown) =>
@ -127,19 +118,13 @@ const DeleteStrategy: FC<{
return (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography
sx={(theme) => ({
color: theme.palette.error.main,
})}
>
- Deleting strategy
</Typography>
<Deleted>Deleting strategy</Deleted>
<ChangeStrategyName name={name || ''} title={title} />
</ChangeItemInfo>
{actions}
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<TabPanel>
{referenceStrategy && (
<StrategyExecution strategy={referenceStrategy} />
@ -184,7 +169,7 @@ const UpdateStrategy: FC<{
}}
changeRequestState={changeRequestState}
/>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<EditHeader
wasDisabled={currentStrategy?.disabled}
@ -197,7 +182,7 @@ const UpdateStrategy: FC<{
/>
</ChangeItemInfo>
{actions}
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<ConditionallyRender
condition={
change.payload?.disabled !== currentStrategy?.disabled
@ -257,17 +242,11 @@ const AddStrategy: FC<{
actions?: ReactNode;
}> = ({ change, actions }) => (
<>
<ChangeItemCreateEditDeleteWrapper>
<ChangeItemWrapper>
<ChangeItemInfo>
<Typography
color={
change.payload?.disabled
? 'text.secondary'
: 'success.dark'
}
>
+ Adding strategy
</Typography>
<AddedStrategy disabled={change.payload?.disabled}>
Adding strategy
</AddedStrategy>
<ChangeStrategyName
name={change.payload.name}
title={change.payload.title}
@ -278,7 +257,7 @@ const AddStrategy: FC<{
/>
</ChangeItemInfo>
{actions}
</ChangeItemCreateEditDeleteWrapper>
</ChangeItemWrapper>
<TabPanel>
<StrategyExecution strategy={change.payload} />
{change.payload.variants?.length ? (
@ -298,6 +277,19 @@ const AddStrategy: FC<{
</>
);
const ActionsContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
alignItems: 'center',
columnGap: theme.spacing(1),
}));
const StyledTabs = styled(Tabs)(({ theme }) => ({
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(1),
}));
export const StrategyChange: FC<{
actions?: ReactNode;
change:
@ -324,17 +316,17 @@ export const StrategyChange: FC<{
);
const actionsWithTabs = (
<>
<ActionsContainer>
<TabList>
<Tab>Change</Tab>
<Tab>View diff</Tab>
</TabList>
{actions}
</>
</ActionsContainer>
);
return (
<Tabs>
<StyledTabs>
{change.action === 'addStrategy' && (
<AddStrategy change={change} actions={actionsWithTabs} />
)}
@ -354,6 +346,6 @@ export const StrategyChange: FC<{
actions={actionsWithTabs}
/>
)}
</Tabs>
</StyledTabs>
);
};

View File

@ -1,19 +1,23 @@
import type { ReactNode, VFC } from 'react';
import type { FC, ReactNode } from 'react';
import { Box } from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import { ChangeItemWrapper } from './StrategyChange.tsx';
import { ChangeItemWrapper as LegacyChangeItemWrapper } from './LegacyStrategyChange.tsx';
import { Action, ChangeItemInfo, ChangeItemWrapper } from './Change.styles';
interface IToggleStatusChange {
enabled: boolean;
actions?: ReactNode;
}
export const ToggleStatusChange: VFC<IToggleStatusChange> = ({
/**
* @deprecated use ToggleStatusChange instead; remove with flag crDiffView
*/
export const LegacyToggleStatusChange: FC<IToggleStatusChange> = ({
enabled,
actions,
}) => {
return (
<ChangeItemWrapper>
<LegacyChangeItemWrapper>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
New status
<Badge
@ -24,6 +28,23 @@ export const ToggleStatusChange: VFC<IToggleStatusChange> = ({
</Badge>
</Box>
{actions}
</LegacyChangeItemWrapper>
);
};
export const ToggleStatusChange: FC<IToggleStatusChange> = ({
enabled,
actions,
}) => {
return (
<ChangeItemWrapper>
<ChangeItemInfo>
<Action>New status</Action>
<Badge color={enabled ? 'success' : 'error'}>
{enabled ? ' Enabled' : 'Disabled'}
</Badge>
{actions}
</ChangeItemInfo>
</ChangeItemWrapper>
);
};

View File

@ -37,23 +37,23 @@ import { useActionableChangeRequests } from 'hooks/api/getters/useActionableChan
import { useUiFlag } from 'hooks/useUiFlag.ts';
import { ChangeRequestRequestedApprovers } from './ChangeRequestRequestedApprovers/ChangeRequestRequestedApprovers.tsx';
const breakpoint = 'md';
const StyledAsideBox = styled(Box)(({ theme }) => ({
width: '30%',
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.down('sm')]: {
[theme.breakpoints.down(breakpoint)]: {
width: '100%',
},
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
marginTop: theme.spacing(2),
marginLeft: theme.spacing(2),
width: '70%',
padding: theme.spacing(1, 2),
borderRadius: theme.shape.borderRadiusLarge,
[theme.breakpoints.down('sm')]: {
marginLeft: 0,
[theme.breakpoints.down(breakpoint)]: {
width: '100%',
},
}));
@ -74,7 +74,8 @@ const StyledButton = styled(Button)(({ theme }) => ({
const ChangeRequestBody = styled(Box)(({ theme }) => ({
display: 'flex',
[theme.breakpoints.down('sm')]: {
columnGap: theme.spacing(2),
[theme.breakpoints.down(breakpoint)]: {
flexDirection: 'column',
},
}));