1
1
import { useQuery } from '@apollo/client' ;
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' ;
2
+ import { TabContext , TabList , TabPanel } from '@mui/lab' ;
3
+ import { Box , Skeleton , Stack , Typography } from '@mui/material' ;
7
4
import { Helmet } from 'react-helmet-async' ;
8
5
import { useParams } from 'react-router-dom' ;
9
6
import { PartialDeep } from 'type-fest' ;
10
- import { RoleLabels } from '~/api/schema.graphql' ;
11
- import { canEditAny , labelsFrom } from '~/common' ;
12
7
import { ToggleCommentsButton } from '~/components/Comments/ToggleCommentButton' ;
8
+ import { Error } from '~/components/Error' ;
9
+ import { Redacted } from '~/components/Redacted' ;
10
+ import { Tab , TabsContainer } from '~/components/Tabs' ;
11
+ import { TogglePinButton } from '~/components/TogglePinButton' ;
12
+ import { EnumParam , makeQueryHandler , withDefault } from '~/hooks' ;
13
13
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
- import { EditUser } from '../Edit' ;
24
14
import { UsersQueryVariables } from '../List/users.graphql' ;
25
15
import { ImpersonationToggle } from './ImpersonationToggle' ;
16
+ import { UserDetailProfile } from './Tabs/Profile/UserDetailProfile' ;
17
+ import { UserDetailProjects } from './Tabs/Projects/UserDetailProjects' ;
26
18
import { UserDocument } from './UserDetail.graphql' ;
27
19
20
+ const useUserDetailsFilters = makeQueryHandler ( {
21
+ tab : withDefault ( EnumParam ( [ 'profile' , 'projects' ] ) , 'profile' ) ,
22
+ } ) ;
23
+
28
24
export const UserDetail = ( ) => {
29
25
const { userId = '' } = useParams ( ) ;
30
26
const { data, error } = useQuery ( UserDocument , {
31
27
variables : { userId } ,
32
28
} ) ;
33
29
useComments ( userId ) ;
34
-
35
- const [ editUserState , editUser ] = useDialog ( ) ;
36
-
30
+ const [ filters , setFilters ] = useUserDetailsFilters ( ) ;
37
31
const user = data ?. user ;
38
32
39
- const canEditAnyFields = canEditAny ( user ) ;
40
-
41
33
return (
42
34
< Stack
43
35
component = "main"
44
36
sx = { {
45
37
overflowY : 'auto' ,
46
38
p : 4 ,
47
39
gap : 3 ,
48
- maxWidth : ( theme ) => theme . breakpoints . values . md ,
40
+ flex : 1 ,
41
+ maxWidth : ( theme ) => theme . breakpoints . values . xl ,
49
42
} }
50
43
>
51
44
< Helmet title = { user ?. fullName ?? undefined } />
52
- { error ? (
53
- < Typography variant = "h4" > Error loading person</ Typography >
54
- ) : (
45
+
46
+ < Error error = { error } >
47
+ { {
48
+ NotFound : 'Could not find user' ,
49
+ Default : 'Error loading user' ,
50
+ } }
51
+ </ Error >
52
+ { ! error && (
55
53
< >
56
54
< Box
57
55
sx = { {
58
- flex : 1 ,
59
56
display : 'flex' ,
60
57
gap : 1 ,
61
58
} }
62
59
>
63
60
< Typography
64
61
variant = "h2"
65
62
sx = { {
66
- mr : 2 , // a little extra between text and buttons
67
- lineHeight : 'inherit' , // centers text with buttons better
63
+ mr : 2 ,
64
+ lineHeight : 'inherit' ,
68
65
} }
69
66
>
70
67
{ ! user ? (
@@ -78,13 +75,6 @@ export const UserDetail = () => {
78
75
)
79
76
) }
80
77
</ Typography >
81
- { canEditAnyFields ? (
82
- < Tooltip title = "Edit Person" >
83
- < IconButton aria-label = "edit person" onClick = { editUser } >
84
- < Edit />
85
- </ IconButton >
86
- </ Tooltip >
87
- ) : null }
88
78
< TogglePinButton
89
79
object = { user }
90
80
label = "Person"
@@ -96,107 +86,26 @@ export const UserDetail = () => {
96
86
< ToggleCommentsButton loading = { ! user } />
97
87
< ImpersonationToggle user = { user } />
98
88
</ 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
- ) }
89
+ < TabsContainer >
90
+ < TabContext value = { filters . tab } >
91
+ < TabList
92
+ onChange = { ( _e , tab ) => setFilters ( { ...filters , tab } ) }
93
+ aria-label = "user navigation tabs"
94
+ variant = "scrollable"
95
+ >
96
+ < Tab label = "Profile" value = "profile" />
97
+ < Tab label = "Projects" value = "projects" />
98
+ </ TabList >
99
+ < TabPanel value = "profile" >
100
+ { user && < UserDetailProfile user = { user } /> }
101
+ </ TabPanel >
102
+ < TabPanel value = "projects" >
103
+ < UserDetailProjects />
104
+ </ TabPanel >
105
+ </ TabContext >
106
+ </ TabsContainer >
150
107
</ >
151
108
) }
152
109
</ Stack >
153
110
) ;
154
111
} ;
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