Skip to content

Commit 4638c9c

Browse files
committed
Create Field Zone tabs, Add Projects grid components and gql
1 parent 13e4234 commit 4638c9c

File tree

6 files changed

+236
-97
lines changed

6 files changed

+236
-97
lines changed

src/api/schema/typePolicies/typePolicies.base.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,14 @@ export const typePolicies: TypePolicies = {
9090
projects: {}, // no page merging (infinite scroll)
9191
},
9292
},
93+
FieldZone: {
94+
fields: {
95+
projects: {}, // no page merging (infinite scroll)
96+
},
97+
},
9398
Query: {
9499
fields: {
95100
projects: {},
96-
engagements: {},
97101
progressReports: {},
98102
partners: {},
99103
users: {},
Lines changed: 47 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,97 @@
11
import { useQuery } from '@apollo/client';
2-
import { Edit } from '@mui/icons-material';
3-
import { Box, Skeleton, Typography } from '@mui/material';
2+
import { TabContext, TabList, TabPanel } from '@mui/lab';
3+
import { Box, Skeleton, Stack, Typography } from '@mui/material';
44
import { Helmet } from 'react-helmet-async';
55
import { useParams } from 'react-router-dom';
6-
import { canEditAny } from '~/common';
7-
import { useDialog } from '~/components/Dialog';
8-
import {
9-
DisplaySimpleProperty,
10-
DisplaySimplePropertyProps,
11-
} from '~/components/DisplaySimpleProperty';
12-
import { Link } from '~/components/Routing';
6+
import { Tab, TabsContainer } from '~/components/Tabs';
7+
import { EnumParam, makeQueryHandler, withDefault } from '~/hooks';
138
import { Error } from '../../../components/Error';
14-
import { Fab } from '../../../components/Fab';
15-
import { EditFieldZone } from '../../../components/FieldZone';
169
import { Redacted } from '../../../components/Redacted';
1710
import { FieldZoneDetailDocument } from './FieldZoneDetail.graphql';
11+
import { FieldZoneProfile } from './Tabs/Profile/FieldZoneProfile';
12+
import { FieldZoneProjects } from './Tabs/Projects/FieldZoneProjects';
13+
14+
const useFieldZoneDetailsFilters = makeQueryHandler({
15+
tab: withDefault(EnumParam(['profile', 'projects']), 'profile'),
16+
});
1817

1918
export const FieldZoneDetail = () => {
2019
const { fieldZoneId = '' } = useParams();
2120

22-
const [editZoneState, editZone] = useDialog();
23-
2421
const { data, error } = useQuery(FieldZoneDetailDocument, {
2522
variables: { fieldZoneId },
2623
});
2724

25+
const [filters, setFilters] = useFieldZoneDetailsFilters();
26+
2827
const fieldZone = data?.fieldZone;
2928

3029
return (
31-
<Box
30+
<Stack
3231
component="main"
3332
sx={{
34-
flex: 1,
33+
overflowY: 'auto',
3534
p: 4,
35+
gap: 3,
36+
flex: 1,
37+
maxWidth: (theme) => theme.breakpoints.values.xl,
3638
}}
3739
>
38-
<Helmet title={fieldZone?.name.value || undefined} />
3940
<Error error={error}>
4041
{{
41-
NotFound: 'Could not find field zone',
42-
Default: 'Error loading field zone',
42+
NotFound: 'Could not find fieldZone',
43+
Default: 'Error loading fieldZone',
4344
}}
4445
</Error>
46+
<Helmet title={fieldZone?.name.value ?? undefined} />
47+
4548
{!error && (
46-
<Box
47-
sx={{
48-
maxWidth: (theme) => theme.breakpoints.values.md,
49-
display: 'flex',
50-
flexDirection: 'column',
51-
gap: 3,
52-
}}
53-
>
49+
<>
5450
<Box
55-
component="header"
5651
sx={{
57-
flex: 1,
5852
display: 'flex',
53+
gap: 1,
5954
}}
6055
>
6156
<Typography
6257
variant="h2"
6358
sx={{
64-
mr: 4,
65-
width: !fieldZone?.name.value ? '40%' : undefined,
59+
mr: 2,
60+
lineHeight: 'inherit',
6661
}}
6762
>
6863
{!fieldZone ? (
69-
<Skeleton width="100%" />
64+
<Skeleton width="20ch" />
7065
) : (
7166
fieldZone.name.value ?? (
7267
<Redacted
7368
info="You don't have permission to view this field zone's name"
74-
width="40%"
69+
width="20ch"
7570
/>
7671
)
7772
)}
7873
</Typography>
79-
{canEditAny(fieldZone, true) && (
80-
<Fab
81-
color="primary"
82-
aria-label="edit zone"
83-
onClick={editZone}
84-
loading={!fieldZone}
85-
>
86-
<Edit />
87-
</Fab>
88-
)}
89-
</Box>
90-
<Box
91-
sx={{
92-
display: 'flex',
93-
}}
94-
>
95-
<Typography variant="h4">
96-
{fieldZone ? 'Field Zone' : <Skeleton width={200} />}
97-
</Typography>
9874
</Box>
99-
100-
<DisplayProperty
101-
label="Director"
102-
value={
103-
fieldZone?.director.value && (
104-
<Link to={`/users/${fieldZone.director.value.id}`}>
105-
{fieldZone.director.value.fullName}
106-
</Link>
107-
)
108-
}
109-
loading={!fieldZone}
110-
/>
111-
</Box>
75+
<TabsContainer>
76+
<TabContext value={filters.tab}>
77+
<TabList
78+
onChange={(_e, tab) => setFilters({ ...filters, tab })}
79+
aria-label="field zone navigation tabs"
80+
variant="scrollable"
81+
>
82+
<Tab label="Profile" value="profile" />
83+
<Tab label="Projects" value="projects" />
84+
</TabList>
85+
<TabPanel value="profile">
86+
{fieldZone && <FieldZoneProfile fieldZone={fieldZone} />}
87+
</TabPanel>
88+
<TabPanel value="projects">
89+
{fieldZone && <FieldZoneProjects />}
90+
</TabPanel>
91+
</TabContext>
92+
</TabsContainer>
93+
</>
11294
)}
113-
{fieldZone && <EditFieldZone fieldZone={fieldZone} {...editZoneState} />}
114-
</Box>
95+
</Stack>
11596
);
11697
};
117-
118-
const DisplayProperty = (props: DisplaySimplePropertyProps) =>
119-
!props.value && !props.loading ? null : (
120-
<DisplaySimpleProperty
121-
variant="body1"
122-
{...{ component: 'div' }}
123-
{...props}
124-
loading={
125-
props.loading ? (
126-
<>
127-
<Typography variant="body2">
128-
<Skeleton width="10%" />
129-
</Typography>
130-
<Typography variant="body1">
131-
<Skeleton width="40%" />
132-
</Typography>
133-
</>
134-
) : null
135-
}
136-
LabelProps={{
137-
color: 'textSecondary',
138-
variant: 'body2',
139-
...props.LabelProps,
140-
}}
141-
ValueProps={{
142-
color: 'textPrimary',
143-
...props.ValueProps,
144-
}}
145-
/>
146-
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fragment FieldZoneProfile on FieldZone {
2+
...DisplayFieldZone
3+
...FieldZoneForm
4+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Edit } from '@mui/icons-material';
2+
import {
3+
Box,
4+
IconButton,
5+
Paper,
6+
Skeleton,
7+
Stack,
8+
Tooltip,
9+
Typography,
10+
} from '@mui/material';
11+
import { canEditAny } from '~/common';
12+
import { useDialog } from '~/components/Dialog';
13+
import {
14+
DisplaySimpleProperty,
15+
DisplaySimplePropertyProps,
16+
} from '~/components/DisplaySimpleProperty';
17+
import { EditFieldZone } from '~/components/FieldZone';
18+
import { Link } from '~/components/Routing';
19+
import { FieldZoneProfileFragment } from './FieldZoneProfile.graphql';
20+
21+
interface FieldZoneProfileProps {
22+
fieldZone: FieldZoneProfileFragment;
23+
}
24+
25+
export const FieldZoneProfile = ({ fieldZone }: FieldZoneProfileProps) => {
26+
const [editZoneState, editZone] = useDialog();
27+
28+
const canEditAnyFields = canEditAny(fieldZone);
29+
30+
return (
31+
<Box
32+
component={Paper}
33+
sx={(theme) => ({
34+
display: 'flex',
35+
justifyContent: 'space-between',
36+
width: theme.breakpoints.values.md,
37+
minHeight: 200,
38+
})}
39+
>
40+
<Stack
41+
sx={{
42+
p: 2,
43+
gap: 2,
44+
}}
45+
>
46+
<DisplayProperty
47+
label="Director"
48+
value={
49+
<Link to={`/users/${fieldZone.director.value?.id}`}>
50+
{fieldZone.director.value?.fullName}
51+
</Link>
52+
}
53+
loading={!fieldZone}
54+
/>
55+
</Stack>
56+
<Box sx={{ p: 1 }}>
57+
{canEditAnyFields ? (
58+
<Tooltip title="Edit Field Zone">
59+
<IconButton aria-label="edit field Zone" onClick={editZone}>
60+
<Edit />
61+
</IconButton>
62+
</Tooltip>
63+
) : null}
64+
</Box>
65+
<EditFieldZone fieldZone={fieldZone} {...editZoneState} />
66+
</Box>
67+
);
68+
};
69+
70+
const DisplayProperty = (props: DisplaySimplePropertyProps) =>
71+
!props.value && !props.loading ? null : (
72+
<DisplaySimpleProperty
73+
variant="body1"
74+
{...{ component: 'div' }}
75+
{...props}
76+
loading={
77+
props.loading ? (
78+
<>
79+
<Typography variant="body2">
80+
<Skeleton width="10%" />
81+
</Typography>
82+
<Typography variant="body1">
83+
<Skeleton width="40%" />
84+
</Typography>
85+
</>
86+
) : null
87+
}
88+
LabelProps={{
89+
color: 'textSecondary',
90+
variant: 'body2',
91+
...props.LabelProps,
92+
}}
93+
ValueProps={{
94+
color: 'textPrimary',
95+
...props.ValueProps,
96+
}}
97+
/>
98+
);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
query FieldZoneProjects($fieldZoneId: ID!, $input: ProjectListInput) {
2+
fieldZone(id: $fieldZoneId) {
3+
id
4+
projects(input: $input) {
5+
canRead
6+
hasMore
7+
total
8+
items {
9+
...fieldZoneProjectDataGridRow
10+
}
11+
}
12+
}
13+
}
14+
15+
fragment fieldZoneProjectDataGridRow on Project {
16+
...projectDataGridRow
17+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
DataGridPro as DataGrid,
3+
DataGridProProps as DataGridProps,
4+
} from '@mui/x-data-grid-pro';
5+
import { merge } from 'lodash';
6+
import { useMemo } from 'react';
7+
import { useParams } from 'react-router-dom';
8+
import {
9+
DefaultDataGridStyles,
10+
flexLayout,
11+
noFooter,
12+
noHeaderFilterButtons,
13+
useDataGridSource,
14+
} from '~/components/Grid';
15+
import {
16+
ProjectColumns,
17+
ProjectInitialState,
18+
ProjectToolbar,
19+
} from '~/components/ProjectDataGrid';
20+
import { TabPanelContent } from '~/components/Tabs';
21+
import {
22+
FieldZoneProjectDataGridRowFragment as FieldZoneProject,
23+
FieldZoneProjectsDocument,
24+
} from './FieldZoneProjects.graphql';
25+
26+
export const FieldZoneProjects = () => {
27+
const { fieldZoneId = '' } = useParams();
28+
29+
const [props] = useDataGridSource({
30+
query: FieldZoneProjectsDocument,
31+
variables: { fieldZoneId },
32+
listAt: 'fieldZone.projects',
33+
initialInput: {
34+
sort: 'name',
35+
},
36+
});
37+
38+
const slots = useMemo(
39+
() =>
40+
merge({}, DefaultDataGridStyles.slots, props.slots, {
41+
toolbar: ProjectToolbar,
42+
} satisfies DataGridProps['slots']),
43+
[props.slots]
44+
);
45+
const slotProps = useMemo(
46+
() => merge({}, DefaultDataGridStyles.slotProps, props.slotProps),
47+
[props.slotProps]
48+
);
49+
50+
return (
51+
<TabPanelContent>
52+
<DataGrid<FieldZoneProject>
53+
{...DefaultDataGridStyles}
54+
{...props}
55+
slots={slots}
56+
slotProps={slotProps}
57+
columns={ProjectColumns}
58+
initialState={ProjectInitialState}
59+
headerFilters
60+
hideFooter
61+
sx={[flexLayout, noHeaderFilterButtons, noFooter]}
62+
/>
63+
</TabPanelContent>
64+
);
65+
};

0 commit comments

Comments
 (0)