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
1 change: 1 addition & 0 deletions src/api/schema/typePolicies/typePolicies.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const typePolicies: TypePolicies = {
User: {
fields: {
projects: {}, // no page merging (infinite scroll)
partners: {}, // no page merging (infinite scroll)
},
},
Query: {
Expand Down
22 changes: 22 additions & 0 deletions src/components/Grid/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Stack } from '@mui/material';
import { ChildrenProp, extendSx, StyleProps } from '~/common';

export const Footer = (props: ChildrenProp & StyleProps) => {
return (
<Stack
{...props}
sx={[
{
flexDirection: 'row',
alignItems: 'center',
background: 'var(--DataGrid-containerBackground)',
p: 1,
borderTop: 'thin solid var(--DataGrid-rowBorderColor)',
},
...extendSx(props.sx),
]}
>
{props.children}
</Stack>
);
};
1 change: 1 addition & 0 deletions src/components/PartnersDataGrid/PartnerColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const PartnerInitialState = {
...getInitialVisibility(PartnerColumns),
isMember: false,
pinned: false,
actions: false,
},
},
} satisfies DataGridProps['initialState'];
Expand Down
2 changes: 2 additions & 0 deletions src/components/PartnersDataGrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './PartnerColumns';
export * from './partnerDataGridRow.graphql';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mutation AssignOrganizationToUser($input: AssignOrganizationToUserInput!) {
assignOrganizationToUser(input: $input) {
partner {
...userPartnerDataGridRow
}
}
}

fragment AssignOrganizationToUserForm on User {
...Id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMutation } from '@apollo/client';
import { Except } from 'type-fest';
import { addItemToList } from '~/api';
import { DialogForm, DialogFormProps } from '~/components/Dialog/DialogForm';
import { CheckboxField, SubmitError } from '~/components/form';
import { OrganizationField } from '~/components/form/Lookup';
import { OrganizationLookupItemFragment } from '~/components/form/Lookup/Organization/OrganizationLookup.graphql';
import {
AssignOrganizationToUserDocument,
AssignOrganizationToUserFormFragment,
} from './AssignOrgToUser.graphql';

interface AssignOrgToUserFormValues {
assignment: {
orgId: OrganizationLookupItemFragment | null;
primary?: boolean;
};
}

type AssignOrgToUserFormProps = Except<
DialogFormProps<AssignOrgToUserFormValues>,
'onSubmit' | 'initialValues'
> & {
user: AssignOrganizationToUserFormFragment;
};

export const AssignOrgToUserForm = ({
user,
...props
}: AssignOrgToUserFormProps) => {
const [assignOrgToUser] = useMutation(AssignOrganizationToUserDocument, {
update: addItemToList({
listId: [user, 'partners'],
outputToItem: (data) => data.assignOrganizationToUser.partner,
}),
});

return (
<DialogForm<AssignOrgToUserFormValues>
title="Assign Organization to User"
{...props}
onSubmit={async ({ assignment }) => {
const input = {
assignment: {
userId: user.id,
orgId: assignment.orgId!.id,
primary: assignment.primary ?? false,
},
};

await assignOrgToUser({ variables: { input } });
}}
fieldsPrefix="assignment"
>
<SubmitError />
<OrganizationField name="orgId" required variant="outlined" />
<CheckboxField name="primary" label="Primary" />
</DialogForm>
);
};
11 changes: 11 additions & 0 deletions src/scenes/Users/Detail/Tabs/Partners/RemoveOrgFromUser.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mutation RemoveOrganizationFromUser($input: RemoveOrganizationFromUserInput!) {
removeOrganizationFromUser(input: $input) {
partner {
...userPartnerDataGridRow
}
}
}

fragment RemoveOrganizationFromUserForm on User {
...Id
}
148 changes: 148 additions & 0 deletions src/scenes/Users/Detail/Tabs/Partners/UserDetailPartners.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useMutation } from '@apollo/client/react/hooks/useMutation';
import { Add, Delete } from '@mui/icons-material';
import { Button, Tooltip } from '@mui/material';
import {
DataGridPro as DataGrid,
DataGridProProps as DataGridProps,
GridActionsCellItem,
} from '@mui/x-data-grid-pro';
import { merge } from 'lodash';
import { useCallback, useMemo } from 'react';
import { removeItemFromList } from '~/api';
import { useDialog } from '~/components/Dialog';
import {
DefaultDataGridStyles,
flexLayout,
noHeaderFilterButtons,
useDataGridSource,
} from '~/components/Grid';
import { Footer } from '~/components/Grid/Footer';
import {
PartnerDataGridRowFragment as Partner,
PartnerColumns,
PartnerInitialState,
PartnerToolbar,
} from '~/components/PartnersDataGrid';
import { TabPanelContent } from '~/components/Tabs/TabPanelContent';
import { AssignOrganizationToUserFormFragment } from './AssignOrgToUserForm/AssignOrgToUser.graphql';
import { AssignOrgToUserForm } from './AssignOrgToUserForm/AssignOrgToUserForm';
import { RemoveOrganizationFromUserDocument } from './RemoveOrgFromUser.graphql';
import { UserPartnerListDocument } from './UserPartnerList.graphql';

interface UserDetailPartnersProps {
user: AssignOrganizationToUserFormFragment;
}

export const UserDetailPartners = ({ user }: UserDetailPartnersProps) => {
const [dataGridProps] = useDataGridSource({
query: UserPartnerListDocument,
variables: { userId: user.id },
listAt: 'user.partners',
initialInput: {
sort: 'organization.name',
},
});

const [addPartnerState, addPartner] = useDialog();

const PartnerFooter = useCallback(
() => (
<Footer>
<Tooltip title="Add Partner" placement="top">
<Button color="primary" onClick={addPartner} size="small">
<Add />
</Button>
</Tooltip>
<Tooltip title="Remove Partner" placement="top">
<Button
color="error"
onClick={() => {
const isActionColVisible = dataGridProps.apiRef.current
.getVisibleColumns()
.find((col) => col.field === 'actions');
dataGridProps.apiRef.current.setColumnVisibility(
'actions',
!isActionColVisible
);
dataGridProps.apiRef.current.setPinnedColumns({
left: ['organization.name'],
right: isActionColVisible ? [] : ['actions'],
});
}}
size="small"
>
<Delete />
</Button>
</Tooltip>
</Footer>
),
[addPartner, dataGridProps.apiRef]
);

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

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

const [removeOrgFromUser] = useMutation(RemoveOrganizationFromUserDocument);

const getActions = ({ row }: { row: Partner }) => [
<GridActionsCellItem
key="remove"
icon={<Delete />}
label="Remove Partner"
onClick={() =>
void removeOrgFromUser({
variables: {
input: {
assignment: {
userId: user.id,
orgId: row.organization.value!.id,
},
},
},
update: removeItemFromList({
listId: [user, 'partners'],
item: row,
}),
})
}
color="error"
/>,
];

return (
<TabPanelContent>
<DataGrid<Partner>
{...dataGridProps}
{...DefaultDataGridStyles}
slots={slots}
slotProps={slotProps}
columns={[
...PartnerColumns,
{
field: 'actions',
type: 'actions',
headerName: 'Remove',
width: 100,
hideable: false,
getActions,
},
]}
initialState={PartnerInitialState}
headerFilters
sx={[flexLayout, noHeaderFilterButtons]}
/>
<AssignOrgToUserForm {...addPartnerState} user={user} />
</TabPanelContent>
);
};
17 changes: 17 additions & 0 deletions src/scenes/Users/Detail/Tabs/Partners/UserPartnerList.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
query UserPartnerList($userId: ID!, $input: PartnerListInput) {
user(id: $userId) {
id
partners(input: $input) {
canRead
hasMore
total
items {
...userPartnerDataGridRow
}
}
}
}

fragment userPartnerDataGridRow on Partner {
...partnerDataGridRow
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
fragment UserProfile on User {
primaryOrganization {
organization {
value {
name {
value
}
}
}
}
...DisplayUser
...UserForm
}
19 changes: 5 additions & 14 deletions src/scenes/Users/Detail/Tabs/Profile/UserDetailProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
DisplaySimpleProperty,
DisplaySimplePropertyProps,
} from '~/components/DisplaySimpleProperty';
import { PartnerListItemCard } from '~/components/PartnerListItemCard';
import { EditUser } from '../../../Edit';
import { UserProfileFragment } from './UserDetailProfile.graphql';

Expand Down Expand Up @@ -75,24 +74,16 @@ export const UserDetailProfile = ({ user }: UserDetailProfileProps) => {
value={user.phone.value}
loading={!user}
/>
<DisplayProperty
label="Primary Partner"
value={user.primaryOrganization?.organization.value?.name.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 ? (
Expand Down
3 changes: 1 addition & 2 deletions src/scenes/Users/Detail/UserDetail.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
query User($userId: ID!) {
user(id: $userId) {
...DisplayUser
...UserForm
...UserProfile
...TogglePin
}
}
12 changes: 11 additions & 1 deletion src/scenes/Users/Detail/UserDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { EnumParam, makeQueryHandler, withDefault } from '~/hooks';
import { useComments } from '../../../components/Comments/CommentsContext';
import { UsersQueryVariables } from '../List/users.graphql';
import { ImpersonationToggle } from './ImpersonationToggle';
import { AssignOrganizationToUserFormFragment } from './Tabs/Partners/AssignOrgToUserForm/AssignOrgToUser.graphql';
import { UserDetailPartners } from './Tabs/Partners/UserDetailPartners';
import { UserDetailProfile } from './Tabs/Profile/UserDetailProfile';
import { UserDetailProjects } from './Tabs/Projects/UserDetailProjects';
import { UserDocument } from './UserDetail.graphql';

const useUserDetailsFilters = makeQueryHandler({
tab: withDefault(EnumParam(['profile', 'projects']), 'profile'),
tab: withDefault(EnumParam(['profile', 'projects', 'partners']), 'profile'),
});

export const UserDetail = () => {
Expand Down Expand Up @@ -95,13 +97,21 @@ export const UserDetail = () => {
>
<Tab label="Profile" value="profile" />
<Tab label="Projects" value="projects" />
<Tab label="Partners" value="partners" />
</TabList>
<TabPanel value="profile">
{user && <UserDetailProfile user={user} />}
</TabPanel>
<TabPanel value="projects">
<UserDetailProjects />
</TabPanel>
<TabPanel value="partners">
{user && (
<UserDetailPartners
user={{ ...(user as AssignOrganizationToUserFormFragment) }}
/>
)}
</TabPanel>
</TabContext>
</TabsContainer>
</>
Expand Down
Loading