1
1
import { useQuery } from '@apollo/client' ;
2
2
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' ;
7
5
import { Helmet } from 'react-helmet-async' ;
8
6
import { useParams } from 'react-router-dom' ;
9
7
import { PartialDeep } from 'type-fest' ;
10
- import { RoleLabels } from '~/api/schema.graphql' ;
11
- import { canEditAny , labelsFrom } from '~/common' ;
8
+ import { canEditAny } from '~/common' ;
12
9
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' ;
13
17
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' ;
23
18
import { EditUser } from '../Edit' ;
24
19
import { UsersQueryVariables } from '../List/users.graphql' ;
25
20
import { ImpersonationToggle } from './ImpersonationToggle' ;
21
+ import { UserDetailProfile } from './Tabs/Profile/UserDetailProfile' ;
22
+ import { UserDetailProjects } from './Tabs/Projects/UserDetailProjects' ;
26
23
import { UserDocument } from './UserDetail.graphql' ;
27
24
25
+ const useUserDetailsFilters = makeQueryHandler ( {
26
+ tab : withDefault ( EnumParam ( [ 'profile' , 'projects' ] ) , 'profile' ) ,
27
+ } ) ;
28
+
28
29
export const UserDetail = ( ) => {
29
30
const { userId = '' } = useParams ( ) ;
30
31
const { data, error } = useQuery ( UserDocument , {
31
32
variables : { userId } ,
32
33
} ) ;
33
34
useComments ( userId ) ;
34
-
35
- const [ editUserState , editUser ] = useDialog ( ) ;
35
+ const [ filters , setFilters ] = useUserDetailsFilters ( ) ;
36
36
37
37
const user = data ?. user ;
38
-
38
+ const [ editUserState , editUser ] = useDialog ( ) ;
39
39
const canEditAnyFields = canEditAny ( user ) ;
40
40
41
41
return (
@@ -45,26 +45,31 @@ export const UserDetail = () => {
45
45
overflowY : 'auto' ,
46
46
p : 4 ,
47
47
gap : 3 ,
48
- maxWidth : ( theme ) => theme . breakpoints . values . md ,
48
+ flex : 1 ,
49
+ maxWidth : ( theme ) => theme . breakpoints . values . xl ,
49
50
} }
50
51
>
51
52
< 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 && (
55
61
< >
56
62
< Box
57
63
sx = { {
58
- flex : 1 ,
59
64
display : 'flex' ,
60
65
gap : 1 ,
61
66
} }
62
67
>
63
68
< Typography
64
69
variant = "h2"
65
70
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' ,
68
73
} }
69
74
>
70
75
{ ! user ? (
@@ -96,107 +101,27 @@ export const UserDetail = () => {
96
101
< ToggleCommentsButton loading = { ! user } />
97
102
< ImpersonationToggle user = { user } />
98
103
</ 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 >
150
122
</ >
151
123
) }
124
+ { user ? < EditUser user = { user } { ...editUserState } /> : null }
152
125
</ Stack >
153
126
) ;
154
127
} ;
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