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

Hack together a tab-switchable change card.

This commit is contained in:
Thomas Heartman 2025-06-19 13:57:13 +02:00
parent ba32e91520
commit 58b12250fd
2 changed files with 174 additions and 5 deletions

View File

@ -1,11 +1,11 @@
import type { VFC } from 'react'; import type { VFC } from 'react';
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import type { ChangeRequestType } from '../changeRequest.types'; import type { ChangeRequestType } from '../changeRequest.types';
import { FeatureToggleChanges } from './Changes/FeatureToggleChanges.tsx';
import { FeatureChange } from './Changes/Change/FeatureChange.tsx'; import { FeatureChange } from './Changes/Change/FeatureChange.tsx';
import { ChangeActions } from './Changes/Change/ChangeActions.tsx'; import { ChangeActions } from './Changes/Change/ChangeActions.tsx';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { SegmentChange } from './Changes/Change/SegmentChange.tsx'; import { SegmentChange } from './Changes/Change/SegmentChange.tsx';
import { SwitchableChangeCard } from './Changes/SwitchableChangeCard.tsx';
interface IChangeRequestProps { interface IChangeRequestProps {
changeRequest: ChangeRequestType; changeRequest: ChangeRequestType;
@ -54,10 +54,13 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
} }
/> />
{changeRequest.features?.map((feature) => ( {changeRequest.features?.map((feature) => (
<FeatureToggleChanges <SwitchableChangeCard
key={feature.name} key={feature.name}
featureName={feature.name} flag={{
projectId={changeRequest.project} name: feature.name,
projectId: changeRequest.project,
}}
resourceType='Feature flag'
onNavigate={onNavigate} onNavigate={onNavigate}
conflict={feature.conflict} conflict={feature.conflict}
> >
@ -98,7 +101,7 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
feature={feature} feature={feature}
/> />
) : null} ) : null}
</FeatureToggleChanges> </SwitchableChangeCard>
))} ))}
</Box> </Box>
); );

View File

@ -0,0 +1,166 @@
import type React from 'react';
import { type PropsWithChildren, useState, type FC, useId } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Box, Card, Typography, Link, styled, Tabs, Tab } from '@mui/material';
import { ConflictWarning } from './Change/ConflictWarning.tsx';
type SegmentProps = {
resourceType: 'Segment';
segment: {
id: number;
name: string;
};
};
type FlagProps = {
resourceType: 'Feature flag';
flag: {
projectId: string;
name: string;
};
};
type Props = {
conflict?: string;
onNavigate?: () => void;
children?: React.ReactNode;
diff?: React.ReactNode;
} & (SegmentProps | FlagProps);
const HeaderGroup = styled('hgroup', {
shouldForwardProp: (prop) => prop !== 'conflict',
})<{ conflict?: string }>(({ theme, conflict }) => ({
display: 'flex',
paddingTop: theme.spacing(conflict ? 0 : 2),
paddingBottom: theme.spacing(2),
paddingInline: theme.spacing(3),
}));
const BottomRow = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}));
const TabPanel: FC<
PropsWithChildren<{
index: number;
value: number;
id: string;
'aria-labelledby': string;
}>
> = ({ children, index, value, id, 'aria-labelledby': ariaLabelledBy }) => {
return (
<div
role='tabpanel'
hidden={value !== index}
id={id}
aria-labelledby={ariaLabelledBy}
>
{value === index ? children : null}
</div>
);
};
const tabA11yProps = (baseId: string) => (index: number) => ({
id: `${baseId}-tab-${index}`,
'aria-controls': `${baseId}-${index}`,
});
export const SwitchableChangeCard: FC<Props> = ({
conflict,
onNavigate,
children,
diff,
resourceType,
...resourceProps
}) => {
const [tabIndex, setTabIndex] = useState(0);
const baseId = useId();
const allyProps = tabA11yProps(baseId);
const [url, name] =
'segment' in resourceProps
? [
`/segments/edit/${resourceProps.segment.id}`,
resourceProps.segment.name,
]
: [
`/projects/${resourceProps.flag.projectId}/features/${resourceProps.flag.name}`,
resourceProps.flag.name,
];
return (
<Card
elevation={0}
sx={(theme) => ({
marginTop: theme.spacing(2),
overflow: 'hidden',
})}
>
<Box
sx={(theme) => ({
backgroundColor: theme.palette.neutral.light,
borderRadius: (theme) =>
`${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
border: '1px solid',
borderColor: (theme) =>
conflict
? theme.palette.warning.border
: theme.palette.divider,
borderBottom: 'none',
overflow: 'hidden',
})}
>
<ConflictWarning conflict={conflict} />
<BottomRow>
<HeaderGroup conflict={conflict}>
<Typography>{resourceType}:</Typography>
<Typography component='h3'>
<Link
component={RouterLink}
to={url}
color='primary'
underline='hover'
sx={{
marginLeft: 1,
'& :hover': {
textDecoration: 'underline',
},
}}
onClick={onNavigate}
>
<strong>{name}</strong>
</Link>
</Typography>
</HeaderGroup>
<Tabs
selectionFollowsFocus
aria-label='Choose view'
value={tabIndex}
onChange={(_, newValue) => setTabIndex(newValue)}
>
<Tab label='Change' {...allyProps(0)} />
<Tab label='Diff' {...allyProps(1)} />
</Tabs>
</BottomRow>
</Box>
<TabPanel
id={`${baseId}-${0}`}
aria-labelledby={`${baseId}-tab-${0}`}
value={tabIndex}
index={0}
>
{children}
</TabPanel>
<TabPanel
id={`${baseId}-${1}`}
aria-labelledby={`${baseId}-tab-${1}`}
value={tabIndex}
index={1}
>
{diff}
</TabPanel>
</Card>
);
};