1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-18 11:14:57 +02:00
unleash.unleash/frontend/src/component/user/Profile/ProfileTab/ProfileTab.tsx
Thomas Heartman 7cc3c32eb2
fix: date localizations for chart (#10581)
Adds date localization to the charts on the analytics page. In doing so,
I have extracted the default locales that we allow the user to set into
constants, so that we can reference it from other places. I have also
sorted the list and added my personal favorite format (ja) to it.

Because we have multiple charts on the analytics page, it felt weird
that only one chart should follow your preferred format. It also aligns
the existing charts' tooltip date format with the new one (`P` instead
of `PPP`).

In short: previously, the charts would show you only your system locale
(I think), which for me defaults to en-US, regardless of what setting
you'd set in your profile. Now we respect your setting as long as it's
one of the default ones.

Before (date formatting is en-US):
<img width="1444" height="1658" alt="image"
src="https://github.com/user-attachments/assets/99a893c7-efb6-4e55-b47c-9df66bf97636"
/>


After (date formatting is sv-SE):
<img width="1383" height="1653" alt="image"
src="https://github.com/user-attachments/assets/d408afd9-a8a7-46f3-8c13-9f7fde608cc4"
/>
2025-09-01 09:29:45 +00:00

276 lines
10 KiB
TypeScript

import { useEffect, useId, useState } from 'react';
import {
Box,
FormControl,
InputLabel,
Select,
type SelectChangeEvent,
styled,
Tooltip,
Typography,
} from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
import { useLocationSettings } from 'hooks/useLocationSettings';
import type { IUser } from 'interfaces/user';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { Link } from 'react-router-dom';
import { PageContent } from 'component/common/PageContent/PageContent';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { RoleBadge } from 'component/common/RoleBadge/RoleBadge';
import { useUiFlag } from 'hooks/useUiFlag';
import { ProductivityEmailSubscription } from './ProductivityEmailSubscription.tsx';
import { formatDateYMDHM } from 'utils/formatDate.ts';
import { defaultLocales } from '../../../../constants/defaultLocales.ts';
const StyledHeader = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: theme.spacing(6),
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.alternative,
color: theme.palette.primary.contrastText,
marginBottom: theme.spacing(3),
boxShadow: theme.boxShadows.primaryHeader,
}));
const StyledInfo = styled('div')(() => ({
flexGrow: 1,
}));
const StyledInfoName = styled(Typography)(({ theme }) => ({
fontSize: theme.spacing(3.75),
}));
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
width: theme.spacing(9.5),
height: theme.spacing(9.5),
marginRight: theme.spacing(3),
}));
const StyledSectionLabel = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
marginBottom: theme.spacing(3),
}));
const StyledAccess = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(2),
'& > div > p': {
marginBottom: theme.spacing(1.5),
},
}));
const StyledBadgeLink = styled(Link)(({ theme }) => ({
':hover,:focus-visible': {
outline: 'none',
'> *': {
outline: `2px solid ${theme.palette.primary.main}`,
},
},
}));
const StyledDivider = styled('div')(({ theme }) => ({
width: '100%',
height: '1px',
backgroundColor: theme.palette.divider,
margin: theme.spacing(3, 0),
}));
const StyledFormControl = styled(FormControl)(({ theme }) => ({
width: theme.spacing(30),
}));
const StyledInputLabel = styled(InputLabel)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
}));
interface IProfileTabProps {
user: IUser;
}
const ProjectList = styled('ul')(({ theme }) => ({
listStyle: 'none',
padding: 0,
display: 'flex',
flexFlow: 'row wrap',
gap: theme.spacing(1),
}));
const exampleDateString = '2014-09-29T14:50:46';
const exampleDate = new Date(exampleDateString);
const LocaleSelector = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1.5),
display: 'flex',
flexFlow: 'row wrap',
gap: theme.spacing(1),
alignItems: 'center',
}));
export const ProfileTab = ({ user }: IProfileTabProps) => {
const { profile, refetchProfile } = useProfile();
const { locationSettings, setLocationSettings } = useLocationSettings();
const [currentLocale, setCurrentLocale] = useState<string>();
const exampleDateId = useId();
const [possibleLocales, setPossibleLocales] = useState<string[]>([
...defaultLocales,
]);
useEffect(() => {
const found = possibleLocales.find((locale) =>
locale
.toLowerCase()
.includes(locationSettings.locale.toLowerCase()),
);
setCurrentLocale(found);
if (!found) {
setPossibleLocales((prev) => [...prev, locationSettings.locale]);
}
}, [locationSettings]);
const changeLocale = (e: SelectChangeEvent) => {
const locale = e.target.value;
setCurrentLocale(locale);
setLocationSettings({ locale });
};
const productivityReportEmailEnabled = useUiFlag('productivityReportEmail');
return (
<>
<StyledHeader>
<StyledAvatar user={user} />
<StyledInfo>
<StyledInfoName>
{user.name || user.email || user.username}
</StyledInfoName>
<Typography variant='body1'>{user.email}</Typography>
</StyledInfo>
</StyledHeader>
<PageContent>
<StyledSectionLabel>Access</StyledSectionLabel>
<StyledAccess>
<Box sx={{ width: '50%' }}>
<ConditionallyRender
condition={Boolean(profile?.rootRole)}
show={() => (
<>
<Typography variant='body2'>
Your root role
</Typography>
<RoleBadge roleId={profile?.rootRole.id!}>
{profile?.rootRole.name}
</RoleBadge>
</>
)}
/>
</Box>
<Box>
<Typography variant='body2'>Projects</Typography>
<ConditionallyRender
condition={Boolean(profile?.projects.length)}
show={
<ProjectList>
{profile?.projects.map((project) => (
<li key={project}>
<Tooltip
title='View project'
arrow
placement='bottom-end'
describeChild
>
<StyledBadgeLink
to={`/projects/${project}`}
>
<Badge
color='secondary'
icon={
<TopicOutlinedIcon />
}
>
{project}
</Badge>
</StyledBadgeLink>
</Tooltip>
</li>
))}
</ProjectList>
}
elseShow={
<Tooltip
title='You are not assigned to any projects'
arrow
describeChild
>
<Badge tabIndex={0}>No projects</Badge>
</Tooltip>
}
/>
</Box>
</StyledAccess>
<StyledDivider />
<StyledSectionLabel>Date/Time Settings</StyledSectionLabel>
<Typography variant='body2'>
This is the format used across the system for time and date
</Typography>
<LocaleSelector>
<StyledFormControl variant='outlined' size='small'>
<StyledInputLabel htmlFor='locale-select'>
Date/Time formatting
</StyledInputLabel>
<Select
aria-details={exampleDateId}
id='locale-select'
value={currentLocale || ''}
native
onChange={changeLocale}
MenuProps={{
style: {
zIndex: 9999,
},
}}
>
{possibleLocales.map((locale) => {
return (
<option key={locale} value={locale}>
{locale}
</option>
);
})}
</Select>
</StyledFormControl>
<Typography id={exampleDateId}>
Example:{' '}
<time dateTime={exampleDateString}>
{formatDateYMDHM(exampleDate, currentLocale)}
</time>
</Typography>
</LocaleSelector>
{productivityReportEmailEnabled ? (
<>
<StyledDivider />
<StyledSectionLabel>Email Settings</StyledSectionLabel>
{profile?.subscriptions && (
<ProductivityEmailSubscription
status={
profile.subscriptions.includes(
'productivity-report',
)
? 'subscribed'
: 'unsubscribed'
}
onChange={refetchProfile}
/>
)}
</>
) : null}
</PageContent>
</>
);
};