1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-18 11:14:57 +02:00
unleash.unleash/frontend/src/component/personalDashboard/MyFlags.tsx
Thomas Heartman 134c32589a
fix(1-3375): Fix unintended scroll on dashboard (#9316)
Fixes a bug where the dashboard would scroll you down from the top of
the page on load if your window was too short too see both the
selected flag and the selected project.

This solves it by immediately scrolling to the top of the page after
scrolling your selected element into view. Because this hook only runs
on page load, it shouldn't be safe. (At least I couldn't make this
misbehave with manual testing).

It also changes the list scroll behavior to scroll your selected item
to the top of the list instead of to the bottom (effectively). During
testing, that seems like a better solution to me.

## Background (or why do we auto-scroll here?)

The dashboard's flag and projects panels stores your last selection,
so that when you return to the page you'll be shown what you were
looking at last. This is especially useful if you have a lot of flags
but you're focusing on one in particular.

However, if you **do** have a lot of flags, then it's also quite
likely that your selection will be "below the fold" of the panel, and
you won't see your selected flag/project immediately in the
list (without scrolling).

It seemed like a nice UI affordance to automatically bring your
selected item into view (especially because without it, there's no way
to see what flag/project) you're looking at, so I added the

[`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView)
hook.

What I didn't realize, however, is that it scrolls all scrollable
ancestor containers, which means that if your screen is too short,
it'll scroll you down the page.

From my reading of the docs and some local testing, I don't think
there is a way to limit the scrolling to only the nearest ancestor, so
the easiest way to ensure that we're always at the top seemed to be to
just scroll to the immediately after.
2025-02-17 12:07:35 +01:00

181 lines
5.9 KiB
TypeScript

import { type FC, useEffect, useRef } from 'react';
import {
ContentGridContainer,
FlagGrid,
ListItemBox,
SpacedGridItem,
StyledCardTitle,
StyledList,
listItemStyle,
} from './SharedComponents';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
import {
Alert,
IconButton,
Link,
ListItem,
ListItemButton,
Typography,
styled,
} from '@mui/material';
import LinkIcon from '@mui/icons-material/ArrowForward';
import React from 'react';
import type { PersonalDashboardSchemaFlagsItem } from 'openapi';
const NoActiveFlagsInfo = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'column',
gap: theme.spacing(2),
}));
const FlagListItem: FC<{
flag: { name: string; project: string; type: string };
selected: boolean;
onClick: () => void;
}> = ({ flag, selected, onClick }) => {
const activeFlagRef = useRef<HTMLLIElement>(null);
const { trackEvent } = usePlausibleTracker();
useEffect(() => {
if (activeFlagRef.current) {
activeFlagRef.current.scrollIntoView({
block: 'start',
inline: 'start',
});
window.scrollTo({ top: 0 });
}
}, []);
const IconComponent = getFeatureTypeIcons(flag.type);
const flagLink = `projects/${flag.project}/features/${flag.name}`;
return (
<ListItem
key={flag.name}
disablePadding={true}
sx={{ mb: 1 }}
ref={selected ? activeFlagRef : null}
>
<ListItemButton
sx={listItemStyle}
selected={selected}
onClick={onClick}
>
<ListItemBox>
<IconComponent color='primary' />
<StyledCardTitle>{flag.name}</StyledCardTitle>
<IconButton
component={Link}
href={flagLink}
onClick={() => {
trackEvent('personal-dashboard', {
props: {
eventType: `Go to flag from list`,
},
});
}}
size='small'
sx={{ ml: 'auto' }}
>
<LinkIcon titleAccess={flagLink} />
</IconButton>
</ListItemBox>
</ListItemButton>
</ListItem>
);
};
type FlagData =
| {
state: 'flags';
flags: PersonalDashboardSchemaFlagsItem[];
activeFlag?: PersonalDashboardSchemaFlagsItem;
}
| {
state: 'no flags';
};
type Props = {
hasProjects: boolean;
flagData: FlagData;
setActiveFlag: (flag: PersonalDashboardSchemaFlagsItem) => void;
refetchDashboard: () => void;
};
export const MyFlags: FC<Props> = ({
hasProjects,
flagData,
setActiveFlag,
refetchDashboard,
}) => {
return (
<ContentGridContainer>
<FlagGrid>
<SpacedGridItem gridArea='flags'>
{flagData.state === 'flags' ? (
<StyledList
disablePadding={true}
sx={{
height: '100%',
}}
>
{flagData.flags.map((flag) => (
<FlagListItem
key={flag.name}
flag={flag}
selected={
flag.name === flagData.activeFlag?.name
}
onClick={() => setActiveFlag(flag)}
/>
))}
</StyledList>
) : hasProjects ? (
<NoActiveFlagsInfo>
<Typography>
You have not created or favorited any feature
flags. Once you do, they will show up here.
</Typography>
<Typography>
To create a new flag, go to one of your
projects.
</Typography>
</NoActiveFlagsInfo>
) : (
<Alert severity='info'>
You need to create or join a project to be able to
add a flag, or you must be given the rights by your
admin to add feature flags.
</Alert>
)}
</SpacedGridItem>
<SpacedGridItem gridArea='chart'>
{flagData.state === 'flags' && flagData.activeFlag ? (
<FlagMetricsChart
flag={flagData.activeFlag}
onArchive={refetchDashboard}
/>
) : (
<PlaceholderFlagMetricsChart
label={
'Metrics for your feature flags will be shown here'
}
/>
)}
</SpacedGridItem>
</FlagGrid>
</ContentGridContainer>
);
};
const FlagMetricsChart = React.lazy(() =>
import('./FlagMetricsChart').then((module) => ({
default: module.FlagMetricsChart,
})),
);
const PlaceholderFlagMetricsChart = React.lazy(() =>
import('./FlagMetricsChart').then((module) => ({
default: module.PlaceholderFlagMetricsChartWithWrapper,
})),
);