Skip to content

Commit 8ccdc83

Browse files
committed
Convert User page to tab list and separate out profile from projects
1 parent c463908 commit 8ccdc83

File tree

2 files changed

+176
-124
lines changed

2 files changed

+176
-124
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Box, Skeleton, Typography } from '@mui/material';
2+
import { useInterval } from 'ahooks';
3+
import { DateTime } from 'luxon';
4+
import { useState } from 'react';
5+
import { RoleLabels } from '~/api/schema.graphql';
6+
import { labelsFrom } from '~/common';
7+
import {
8+
DisplaySimpleProperty,
9+
DisplaySimplePropertyProps,
10+
} from '~/components/DisplaySimpleProperty';
11+
import { PartnerListItemCard } from '~/components/PartnerListItemCard';
12+
import { UserQuery } from '../../UserDetail.graphql';
13+
14+
interface UserDetailProfileProps {
15+
user: UserQuery['user'] | undefined;
16+
}
17+
18+
export const UserDetailProfile = ({ user }: UserDetailProfileProps) => {
19+
return (
20+
<Box
21+
sx={{
22+
overflowY: 'auto',
23+
padding: 4,
24+
maxWidth: 'md',
25+
'& > *:not(:last-child)': {
26+
marginBottom: 3,
27+
},
28+
}}
29+
>
30+
<DisplayProperty
31+
label="Email"
32+
value={user?.email.value}
33+
loading={!user}
34+
/>
35+
<DisplayProperty
36+
label="Title"
37+
value={user?.title.value}
38+
loading={!user}
39+
/>
40+
<DisplayProperty
41+
label="Roles"
42+
value={labelsFrom(RoleLabels)(user?.roles.value)}
43+
loading={!user}
44+
/>
45+
<DisplayProperty
46+
label="Local Time"
47+
value={
48+
user?.timezone.value?.name ? (
49+
<LocalTime timezone={user.timezone.value.name} />
50+
) : null
51+
}
52+
loading={!user}
53+
/>
54+
<DisplayProperty
55+
label="Phone"
56+
value={user?.phone.value}
57+
loading={!user}
58+
/>
59+
<DisplayProperty
60+
label="About"
61+
value={user?.about.value}
62+
loading={!user}
63+
/>
64+
65+
{!!user?.partners.items.length && (
66+
<>
67+
<Typography variant="h3">Partners</Typography>
68+
<Box sx={{ marginTop: 1 }}>
69+
{user.partners.items.map((item) => (
70+
<Box key={item.id} sx={{ marginBottom: 2 }}>
71+
<PartnerListItemCard partner={item} />
72+
</Box>
73+
))}
74+
</Box>
75+
</>
76+
)}
77+
</Box>
78+
);
79+
};
80+
81+
const LocalTime = ({ timezone }: { timezone?: string }) => {
82+
const now = useNow();
83+
const formatted = now.toLocaleString({
84+
timeZone: timezone,
85+
...DateTime.TIME_SIMPLE,
86+
timeZoneName: 'short',
87+
});
88+
return <>{formatted}</>;
89+
};
90+
91+
const useNow = (updateInterval = 1_000) => {
92+
const [now, setNow] = useState(() => DateTime.local());
93+
useInterval(() => {
94+
setNow(DateTime.local());
95+
}, updateInterval);
96+
return now;
97+
};
98+
99+
const DisplayProperty = (props: DisplaySimplePropertyProps) =>
100+
!props.value && !props.loading ? null : (
101+
<DisplaySimpleProperty
102+
variant="body1"
103+
{...{ component: 'div' }}
104+
{...props}
105+
loading={
106+
props.loading ? (
107+
<>
108+
<Typography variant="body2">
109+
<Skeleton width="10%" />
110+
</Typography>
111+
<Typography variant="body1">
112+
<Skeleton width="40%" />
113+
</Typography>
114+
</>
115+
) : null
116+
}
117+
LabelProps={{
118+
color: 'textSecondary',
119+
variant: 'body2',
120+
...props.LabelProps,
121+
}}
122+
ValueProps={{
123+
color: 'textPrimary',
124+
...props.ValueProps,
125+
}}
126+
/>
127+
);
Lines changed: 49 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
import { useQuery } from '@apollo/client';
22
import { Edit } from '@mui/icons-material';
3-
import { Box, Skeleton, Stack, Tooltip, Typography } from '@mui/material';
4-
import { useInterval } from 'ahooks';
5-
import { DateTime } from 'luxon';
6-
import { useState } from 'react';
3+
import { TabContext, TabList, TabPanel } from '@mui/lab';
4+
import { Box, Skeleton, Stack, Tab, Tooltip, Typography } from '@mui/material';
75
import { Helmet } from 'react-helmet-async';
86
import { useParams } from 'react-router-dom';
97
import { PartialDeep } from 'type-fest';
10-
import { RoleLabels } from '~/api/schema.graphql';
11-
import { canEditAny, labelsFrom } from '~/common';
8+
import { canEditAny } from '~/common';
129
import { ToggleCommentsButton } from '~/components/Comments/ToggleCommentButton';
10+
import { useDialog } from '~/components/Dialog';
11+
import { Error } from '~/components/Error';
12+
import { IconButton } from '~/components/IconButton';
13+
import { Redacted } from '~/components/Redacted';
14+
import { TabsContainer } from '~/components/Tabs';
15+
import { TogglePinButton } from '~/components/TogglePinButton';
16+
import { EnumParam, makeQueryHandler, withDefault } from '~/hooks';
1317
import { useComments } from '../../../components/Comments/CommentsContext';
14-
import { useDialog } from '../../../components/Dialog';
15-
import {
16-
DisplaySimpleProperty,
17-
DisplaySimplePropertyProps,
18-
} from '../../../components/DisplaySimpleProperty';
19-
import { IconButton } from '../../../components/IconButton';
20-
import { PartnerListItemCard } from '../../../components/PartnerListItemCard';
21-
import { Redacted } from '../../../components/Redacted';
22-
import { TogglePinButton } from '../../../components/TogglePinButton';
2318
import { EditUser } from '../Edit';
2419
import { UsersQueryVariables } from '../List/users.graphql';
2520
import { ImpersonationToggle } from './ImpersonationToggle';
21+
import { UserDetailProfile } from './Tabs/Profile/UserDetailProfile';
22+
import { UserDetailProjects } from './Tabs/Projects/UserDetailProjects';
2623
import { UserDocument } from './UserDetail.graphql';
2724

25+
const useUserDetailsFilters = makeQueryHandler({
26+
tab: withDefault(EnumParam(['profile', 'projects']), 'profile'),
27+
});
28+
2829
export const UserDetail = () => {
2930
const { userId = '' } = useParams();
3031
const { data, error } = useQuery(UserDocument, {
3132
variables: { userId },
3233
});
3334
useComments(userId);
34-
35-
const [editUserState, editUser] = useDialog();
35+
const [filters, setFilters] = useUserDetailsFilters();
3636

3737
const user = data?.user;
38-
38+
const [editUserState, editUser] = useDialog();
3939
const canEditAnyFields = canEditAny(user);
4040

4141
return (
@@ -45,26 +45,31 @@ export const UserDetail = () => {
4545
overflowY: 'auto',
4646
p: 4,
4747
gap: 3,
48-
maxWidth: (theme) => theme.breakpoints.values.md,
48+
flex: 1,
49+
maxWidth: (theme) => theme.breakpoints.values.xl,
4950
}}
5051
>
5152
<Helmet title={user?.fullName ?? undefined} />
52-
{error ? (
53-
<Typography variant="h4">Error loading person</Typography>
54-
) : (
53+
54+
<Error error={error}>
55+
{{
56+
NotFound: 'Could not find user',
57+
Default: 'Error loading user',
58+
}}
59+
</Error>
60+
{!error && (
5561
<>
5662
<Box
5763
sx={{
58-
flex: 1,
5964
display: 'flex',
6065
gap: 1,
6166
}}
6267
>
6368
<Typography
6469
variant="h2"
6570
sx={{
66-
mr: 2, // a little extra between text and buttons
67-
lineHeight: 'inherit', // centers text with buttons better
71+
mr: 2,
72+
lineHeight: 'inherit',
6873
}}
6974
>
7075
{!user ? (
@@ -96,107 +101,27 @@ export const UserDetail = () => {
96101
<ToggleCommentsButton loading={!user} />
97102
<ImpersonationToggle user={user} />
98103
</Box>
99-
<DisplayProperty
100-
label="Status"
101-
value={user?.status.value}
102-
loading={!user}
103-
/>
104-
<DisplayProperty
105-
label="Email"
106-
value={user?.email.value}
107-
loading={!user}
108-
/>
109-
<DisplayProperty
110-
label="Title"
111-
value={user?.title.value}
112-
loading={!user}
113-
/>
114-
<DisplayProperty
115-
label="Roles"
116-
value={labelsFrom(RoleLabels)(user?.roles.value)}
117-
loading={!user}
118-
/>
119-
<DisplayProperty
120-
label="Local Time"
121-
value={
122-
user?.timezone.value?.name ? (
123-
<LocalTime timezone={user.timezone.value.name} />
124-
) : null
125-
}
126-
loading={!user}
127-
/>
128-
<DisplayProperty
129-
label="Phone"
130-
value={user?.phone.value}
131-
loading={!user}
132-
/>
133-
<DisplayProperty
134-
label="About"
135-
value={user?.about.value}
136-
loading={!user}
137-
/>
138-
{user ? <EditUser user={user} {...editUserState} /> : null}
139-
140-
{!!user?.partners.items.length && (
141-
<>
142-
<Typography variant="h3">Partners</Typography>
143-
<Stack sx={{ mt: 1, gap: 2 }}>
144-
{user.partners.items.map((item) => (
145-
<PartnerListItemCard key={item.id} partner={item} />
146-
))}
147-
</Stack>
148-
</>
149-
)}
104+
<TabsContainer>
105+
<TabContext value={filters.tab}>
106+
<TabList
107+
onChange={(_e, tab) => setFilters({ ...filters, tab })}
108+
aria-label="user navigation tabs"
109+
variant="scrollable"
110+
>
111+
<Tab label="Profile" value="profile" />
112+
<Tab label="Projects" value="projects" />
113+
</TabList>
114+
<TabPanel value="profile">
115+
<UserDetailProfile user={user} />
116+
</TabPanel>
117+
<TabPanel value="projects">
118+
<UserDetailProjects />
119+
</TabPanel>
120+
</TabContext>
121+
</TabsContainer>
150122
</>
151123
)}
124+
{user ? <EditUser user={user} {...editUserState} /> : null}
152125
</Stack>
153126
);
154127
};
155-
156-
const LocalTime = ({ timezone }: { timezone?: string }) => {
157-
const now = useNow();
158-
const formatted = now.toLocaleString({
159-
timeZone: timezone,
160-
...DateTime.TIME_SIMPLE,
161-
timeZoneName: 'short',
162-
});
163-
return <>{formatted}</>;
164-
};
165-
166-
const useNow = (updateInterval = 1_000) => {
167-
const [now, setNow] = useState(() => DateTime.local());
168-
useInterval(() => {
169-
setNow(DateTime.local());
170-
}, updateInterval);
171-
return now;
172-
};
173-
174-
const DisplayProperty = (props: DisplaySimplePropertyProps) =>
175-
!props.value && !props.loading ? null : (
176-
<DisplaySimpleProperty
177-
variant="body1"
178-
{...{ component: 'div' }}
179-
{...props}
180-
loading={
181-
props.loading ? (
182-
<>
183-
<Typography variant="body2">
184-
<Skeleton width="10%" />
185-
</Typography>
186-
<Typography variant="body1">
187-
<Skeleton width="40%" />
188-
</Typography>
189-
</>
190-
) : null
191-
}
192-
LabelProps={{
193-
color: 'textSecondary',
194-
variant: 'body2',
195-
...props.LabelProps,
196-
}}
197-
ValueProps={{
198-
color: 'textPrimary',
199-
...props.ValueProps,
200-
}}
201-
/>
202-
);

0 commit comments

Comments
 (0)