Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/schema/typePolicies/typePolicies.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export const typePolicies: TypePolicies = {
engagements: {},
},
},
User: {
fields: {
projects: {}, // no page merging (infinite scroll)
},
},
Query: {
fields: {
projects: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fragment UserProfile on User {
...DisplayUser
...UserForm
}
157 changes: 157 additions & 0 deletions src/scenes/Users/Detail/Tabs/Profile/UserDetailProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Edit } from '@mui/icons-material';
import {
Box,
IconButton,
Paper,
Skeleton,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import { useInterval } from 'ahooks';
import { DateTime } from 'luxon';
import { useState } from 'react';
import { RoleLabels } from '~/api/schema.graphql';
import { canEditAny, labelsFrom } from '~/common';
import { useDialog } from '~/components/Dialog';
import {
DisplaySimpleProperty,
DisplaySimplePropertyProps,
} from '~/components/DisplaySimpleProperty';
import { PartnerListItemCard } from '~/components/PartnerListItemCard';
import { EditUser } from '../../../Edit';
import { UserProfileFragment } from './UserDetailProfile.graphql';

interface UserDetailProfileProps {
user: UserProfileFragment;
}

export const UserDetailProfile = ({ user }: UserDetailProfileProps) => {
const [editUserState, editUser] = useDialog();

const canEditAnyFields = canEditAny(user);

return (
<Box
component={Paper}
sx={(theme) => ({
display: 'flex',
justifyContent: 'space-between',
width: theme.breakpoints.values.md,
})}
>
<Stack
sx={{
p: 2,
gap: 2,
}}
>
<DisplayProperty
label="Email"
value={user.email.value}
loading={!user}
/>
<DisplayProperty
label="Title"
value={user.title.value}
loading={!user}
/>
<DisplayProperty
label="Roles"
value={labelsFrom(RoleLabels)(user.roles.value)}
loading={!user}
/>
<DisplayProperty
label="Local Time"
value={
user.timezone.value?.name ? (
<LocalTime timezone={user.timezone.value.name} />
) : null
}
loading={!user}
/>
<DisplayProperty
label="Phone"
value={user.phone.value}
loading={!user}
/>
<DisplayProperty
label="About"
value={user.about.value}
loading={!user}
/>

{!!user.partners.items.length && (
<>
<Typography variant="h3">Partners</Typography>
<Box sx={{ mt: 1 }}>
{user.partners.items.map((item) => (
<Box key={item.id} sx={{ mb: 2 }}>
<PartnerListItemCard partner={item} />
</Box>
))}
</Box>
</>
)}
</Stack>
<Box sx={{ p: 1 }}>
{canEditAnyFields ? (
<Tooltip title="Edit Person">
<IconButton aria-label="edit person" onClick={editUser}>
<Edit />
</IconButton>
</Tooltip>
) : null}
</Box>
<EditUser user={user} {...editUserState} />
</Box>
);
};

const LocalTime = ({ timezone }: { timezone?: string }) => {
const now = useNow();
const formatted = now.toLocaleString({
timeZone: timezone,
...DateTime.TIME_SIMPLE,
timeZoneName: 'short',
});
return <>{formatted}</>;
};

const useNow = (updateInterval = 1_000) => {
const [now, setNow] = useState(() => DateTime.local());
useInterval(() => {
setNow(DateTime.local());
}, updateInterval);
return now;
};

const DisplayProperty = (props: DisplaySimplePropertyProps) =>
!props.value && !props.loading ? null : (
<DisplaySimpleProperty
variant="body1"
{...{ component: 'div' }}
{...props}
loading={
props.loading ? (
<>
<Typography variant="body2">
<Skeleton width="10%" />
</Typography>
<Typography variant="body1">
<Skeleton width="40%" />
</Typography>
</>
) : null
}
LabelProps={{
color: 'textSecondary',
variant: 'body2',
...props.LabelProps,
}}
ValueProps={{
color: 'textPrimary',
...props.ValueProps,
}}
/>
);
10 changes: 10 additions & 0 deletions src/scenes/Users/Detail/Tabs/Projects/UserDetailProjects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { TabPanelContent } from '~/components/Tabs';
import { UserProjectsPanel } from './UserProjectPanel/UserProjectsPanel';

export const UserDetailProjects = () => {
return (
<TabPanelContent>
<UserProjectsPanel />
</TabPanelContent>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
query UserProjects($userId: ID!, $input: ProjectListInput) {
user(id: $userId) {
id
projects(input: $input) {
canRead
hasMore
total
items {
...userProjectDataGridRow
}
}
}
}

fragment userProjectDataGridRow on Project {
membership(user: $userId) {
id
roles {
value
}
}
...projectDataGridRow
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
DataGridPro as DataGrid,
DataGridProProps as DataGridProps,
GridColDef,
} from '@mui/x-data-grid-pro';
import { merge } from 'lodash';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { RoleLabels, RoleList } from '~/api/schema.graphql';
import { unmatchedIndexThrow } from '~/common';
import {
DefaultDataGridStyles,
flexLayout,
multiEnumColumn,
noFooter,
noHeaderFilterButtons,
useDataGridSource,
} from '~/components/Grid';
import {
ProjectColumns,
ProjectInitialState,
ProjectToolbar,
} from '~/components/ProjectDataGrid';
import {
UserProjectDataGridRowFragment as UserProject,
UserProjectsDocument,
} from './UserProjectList.graphql';

export const UserProjectsPanel = () => {
const { userId = '' } = useParams();

const [dataGridProps] = useDataGridSource({
query: UserProjectsDocument,
variables: { userId },
listAt: 'user.projects',
initialInput: {
sort: 'name',
},
});

const slots = useMemo(
() =>
merge({}, DefaultDataGridStyles.slots, dataGridProps.slots, {
toolbar: ProjectToolbar,
} satisfies DataGridProps['slots']),
[dataGridProps.slots]
);

const slotProps = useMemo(
() => merge({}, DefaultDataGridStyles.slotProps, dataGridProps.slotProps),
[dataGridProps.slotProps]
);

return (
<DataGrid<UserProject>
{...DefaultDataGridStyles}
{...dataGridProps}
slots={slots}
slotProps={slotProps}
columns={UserProjectColumns}
initialState={ProjectInitialState}
headerFilters
hideFooter
sx={[flexLayout, noHeaderFilterButtons, noFooter]}
/>
);
};

const UserProjectRoleColumn: GridColDef<UserProject> = {
field: 'user.membership',
headerName: 'Role',
width: 300,
...multiEnumColumn(RoleList, RoleLabels),
valueGetter: (_, { membership }) => membership.roles.value,
};

const indexAfterName =
unmatchedIndexThrow(ProjectColumns.findIndex((c) => c.field === 'name')) + 1;

const UserProjectColumns = (ProjectColumns as Array<GridColDef<UserProject>>)
// Add roles' column after name
.toSpliced(indexAfterName, 0, UserProjectRoleColumn);
Loading