Skip to content

Commit a901ec8

Browse files
committed
feat(unit-user-usage): add initial implementation
1 parent 2a38ad1 commit a901ec8

File tree

22 files changed

+801
-124
lines changed

22 files changed

+801
-124
lines changed

components/Chips.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ export const Chips = styled("div", { shouldForwardProp: (prop) => prop !== "spac
1515
display: "flex",
1616
alignItems: "center",
1717
flexWrap: "wrap",
18-
"& > *": {
19-
margin: theme.spacing(spacing),
20-
},
18+
gap: theme.spacing(spacing),
2119
}),
2220
);

components/DataTable/DataTable.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface DataTableProps<Data extends Record<string, any>> {
7070
/**
7171
* Child element of the toolbar in the table header
7272
*/
73-
ToolbarChild?: ReactNode;
73+
toolbarContent?: ReactNode;
7474
/**
7575
* Toolbar with actions which sits beneath the table header toolbar with search.
7676
*/
@@ -150,7 +150,7 @@ export const DataTable = <Data extends Record<string, any>>(props: DataTableProp
150150
columns,
151151
data,
152152
ToolbarActionChild,
153-
ToolbarChild,
153+
toolbarContent: ToolbarChild,
154154
initialSelection,
155155
onSelection,
156156
subRowsEnabled,
@@ -296,29 +296,32 @@ export const DataTable = <Data extends Record<string, any>>(props: DataTableProp
296296
{headerGroup.headers.map((header) => (
297297
<TableCell
298298
className={header.column.getCanSort() ? "cursor-pointer select-none" : ""}
299+
colSpan={header.colSpan}
299300
key={header.id}
300301
>
301-
<Box>
302-
{flexRender(header.column.columnDef.header, header.getContext())}
303-
{header.column.getCanSort() ? (
304-
<TableSortLabel
305-
active={!!header.column.getIsSorted()}
306-
// react-table has a unsorted state which is not treated here
307-
direction={header.column.getIsSorted() || undefined}
308-
onClick={header.column.getToggleSortingHandler()}
309-
/>
310-
) : null}
311-
{header.column.getCanFilter() ? (
312-
<div>
313-
{/* TODO: debounce this field */}
314-
<TextField
315-
placeholder="Search"
316-
value={header.column.getFilterValue()}
317-
onChange={(event) => header.column.setFilterValue(event.target.value)}
302+
{header.isPlaceholder ? null : (
303+
<Box sx={{ textWrap: "nowrap" }}>
304+
{flexRender(header.column.columnDef.header, header.getContext())}
305+
{header.column.getCanSort() ? (
306+
<TableSortLabel
307+
active={!!header.column.getIsSorted()}
308+
// react-table has a unsorted state which is not treated here
309+
direction={header.column.getIsSorted() || undefined}
310+
onClick={header.column.getToggleSortingHandler()}
318311
/>
319-
</div>
320-
) : null}
321-
</Box>
312+
) : null}
313+
{header.column.getCanFilter() ? (
314+
<div>
315+
{/* TODO: debounce this field */}
316+
<TextField
317+
placeholder="Search"
318+
value={header.column.getFilterValue()}
319+
onChange={(event) => header.column.setFilterValue(event.target.value)}
320+
/>
321+
</div>
322+
) : null}
323+
</Box>
324+
)}
322325
</TableCell>
323326
))}
324327
</TableRow>

components/projects/CreateProjectForm.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useGetProductTypes,
1212
} from "@squonk/account-server-client/product";
1313
import type { DmError } from "@squonk/data-manager-client";
14+
import { getGetUserInventoryQueryKey } from "@squonk/data-manager-client/inventory";
1415
import { getGetProjectsQueryKey, useCreateProject } from "@squonk/data-manager-client/project";
1516

1617
import {
@@ -104,8 +105,12 @@ export const CreateProjectForm = ({ modal, unitId, product }: CreateProjectFormP
104105

105106
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() });
106107
queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() });
107-
typeof unitId === "string" &&
108+
if (typeof unitId === "string") {
108109
queryClient.invalidateQueries({ queryKey: getGetProductsForUnitQueryKey(unitId) });
110+
queryClient.invalidateQueries({
111+
queryKey: getGetUserInventoryQueryKey({ unit_id: unitId }),
112+
});
113+
}
109114

110115
setCurrentProjectId(project_id);
111116
} catch (error) {
Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,43 @@
1+
import type { ReactNode } from "react";
12
import { useState } from "react";
23

3-
import type { ProjectDetail } from "@squonk/data-manager-client";
4+
import { useGetProject } from "@squonk/data-manager-client/project";
45

5-
import { Edit as EditIcon } from "@mui/icons-material";
6-
import { Box, IconButton, Tooltip, Typography } from "@mui/material";
6+
import { EditProjectModal } from "./EditProjectModal";
77

8-
import { ModalWrapper } from "../../modals/ModalWrapper";
9-
import { PrivateProjectToggle } from "./PrivateProjectToggle";
10-
import { ProjectAdministrators } from "./ProjectAdministrators";
11-
import { ProjectEditors } from "./ProjectEditors";
12-
import { ProjectObservers } from "./ProjectObservers";
8+
interface ChildProps {
9+
openDialog: () => void;
10+
open: boolean;
11+
}
1312

1413
export interface EditProjectButtonProps {
1514
/**
16-
* Project to be edited.
15+
* ID of project to be edited.
16+
*/
17+
projectId: string;
18+
/**
19+
* child render prop
1720
*/
18-
project: ProjectDetail;
21+
children: (props: ChildProps) => ReactNode;
1922
}
2023

2124
/**
2225
* Button controlling a modal with options to edit the project editors
2326
*/
24-
export const EditProjectButton = ({ project }: EditProjectButtonProps) => {
27+
export const EditProjectButton = ({ projectId, children }: EditProjectButtonProps) => {
2528
const [open, setOpen] = useState(false);
2629

30+
const { data: project } = useGetProject(projectId);
31+
32+
if (!project) {
33+
return null;
34+
}
35+
2736
return (
2837
<>
29-
<Tooltip title={"Edit Project"}>
30-
<span>
31-
<IconButton size="small" sx={{ p: "1px" }} onClick={() => setOpen(!open)}>
32-
<EditIcon />
33-
</IconButton>
34-
</span>
35-
</Tooltip>
36-
37-
<ModalWrapper
38-
DialogProps={{ maxWidth: "sm", fullWidth: true }}
39-
id="edit-project"
40-
open={open}
41-
submitText="Save"
42-
title="Edit Project"
43-
onClose={() => setOpen(false)}
44-
>
45-
<Typography gutterBottom variant="h5">
46-
{project.name}
47-
</Typography>
48-
49-
<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />
50-
51-
<Box display="flex" flexDirection="column" gap={2}>
52-
<ProjectAdministrators project={project} />
53-
<ProjectEditors project={project} />
54-
<ProjectObservers project={project} />
55-
</Box>
56-
</ModalWrapper>
38+
{children({ openDialog: () => setOpen(true), open })}
39+
40+
<EditProjectModal open={open} projectId={project.project_id} onClose={() => setOpen(false)} />
5741
</>
5842
);
5943
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useGetProject } from "@squonk/data-manager-client/project";
2+
3+
import { Box, Typography } from "@mui/material";
4+
5+
import { ModalWrapper } from "../../modals/ModalWrapper";
6+
import { PrivateProjectToggle } from "./PrivateProjectToggle";
7+
import { ProjectAdministrators } from "./ProjectAdministrators";
8+
import { ProjectEditors } from "./ProjectEditors";
9+
import { ProjectObservers } from "./ProjectObservers";
10+
11+
export interface EditProjectModalProps {
12+
projectId: string;
13+
open: boolean;
14+
onClose: () => void;
15+
onMemberChange?: () => Promise<void>;
16+
}
17+
18+
export const EditProjectModal = ({
19+
open,
20+
projectId,
21+
onClose,
22+
onMemberChange,
23+
}: EditProjectModalProps) => {
24+
const { data: project } = useGetProject(projectId);
25+
26+
if (project === undefined) {
27+
return null;
28+
}
29+
30+
return (
31+
<ModalWrapper
32+
DialogProps={{ maxWidth: "sm", fullWidth: true }}
33+
id="edit-project"
34+
open={open}
35+
submitText="Save"
36+
title="Edit Project"
37+
onClose={onClose}
38+
>
39+
<Typography gutterBottom variant="h5">
40+
{project.name}
41+
</Typography>
42+
43+
<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />
44+
45+
<Box display="flex" flexDirection="column" gap={2}>
46+
<ProjectAdministrators
47+
administrators={project.administrators}
48+
projectId={project.project_id}
49+
onChange={onMemberChange}
50+
/>
51+
<ProjectEditors
52+
editors={project.editors}
53+
projectId={project.project_id}
54+
onChange={onMemberChange}
55+
/>
56+
<ProjectObservers
57+
observers={project.observers}
58+
projectId={project.project_id}
59+
onChange={onMemberChange}
60+
/>
61+
</Box>
62+
</ModalWrapper>
63+
);
64+
};

components/projects/EditProjectButton/ProjectAdministrators.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,25 @@ export interface ProjectAdministratorsProps {
1515
/**
1616
* Project to be edited.
1717
*/
18-
project: ProjectDetail;
18+
projectId: ProjectDetail["project_id"];
19+
/**
20+
* administrators
21+
*/
22+
administrators: ProjectDetail["administrators"];
23+
/**
24+
* onChange function to be called after the project administrators have been updated
25+
*/
26+
onChange?: () => Promise<void>;
1927
}
2028

2129
/**
2230
* MuiAutocomplete to manage the current administrators of the selected project
2331
*/
24-
export const ProjectAdministrators = ({ project }: ProjectAdministratorsProps) => {
32+
export const ProjectAdministrators = ({
33+
projectId,
34+
administrators,
35+
onChange,
36+
}: ProjectAdministratorsProps) => {
2537
const { isLoading: isProjectsLoading } = useGetProjects();
2638

2739
const { mutateAsync: addAdministrator, isPending: isAdding } = useAddAdministratorToProject();
@@ -31,17 +43,19 @@ export const ProjectAdministrators = ({ project }: ProjectAdministratorsProps) =
3143

3244
return (
3345
<ProjectMemberSelection
34-
addMember={(userId) => addAdministrator({ projectId: project.project_id, userId })}
46+
addMember={(userId) => addAdministrator({ projectId, userId })}
3547
isLoading={isAdding || isRemoving || isProjectsLoading}
36-
memberList={project.administrators}
37-
removeMember={(userId) => removeAdministrator({ projectId: project.project_id, userId })}
48+
memberList={administrators}
49+
removeMember={(userId) => removeAdministrator({ projectId, userId })}
3850
title="Administrators"
39-
onSettled={() =>
40-
Promise.all([
41-
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
51+
onSettled={() => {
52+
const promises = [
53+
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(projectId) }),
4254
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
43-
])
44-
}
55+
];
56+
onChange && promises.push(onChange());
57+
return Promise.all(promises);
58+
}}
4559
/>
4660
);
4761
};

components/projects/EditProjectButton/ProjectEditors.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@ export interface ProjectEditorsProps {
1515
/**
1616
* Project to be edited.
1717
*/
18-
project: ProjectDetail;
18+
projectId: ProjectDetail["project_id"];
19+
/**
20+
* editors
21+
*/
22+
editors: ProjectDetail["editors"];
23+
/**
24+
* onChange function to be called after the project editors have been updated
25+
*/
26+
onChange?: () => Promise<void>;
1927
}
2028

2129
/**
2230
* MuiAutocomplete to manage the current editors of the selected project
2331
*/
24-
export const ProjectEditors = ({ project }: ProjectEditorsProps) => {
32+
export const ProjectEditors = ({ projectId, editors, onChange }: ProjectEditorsProps) => {
2533
const { isLoading: isProjectsLoading } = useGetProjects();
2634

2735
const { mutateAsync: addEditor, isPending: isAdding } = useAddEditorToProject();
@@ -30,17 +38,19 @@ export const ProjectEditors = ({ project }: ProjectEditorsProps) => {
3038

3139
return (
3240
<ProjectMemberSelection
33-
addMember={(userId) => addEditor({ projectId: project.project_id, userId })}
41+
addMember={(userId) => addEditor({ projectId, userId })}
3442
isLoading={isAdding || isRemoving || isProjectsLoading}
35-
memberList={project.editors}
36-
removeMember={(userId) => removeEditor({ projectId: project.project_id, userId })}
43+
memberList={editors}
44+
removeMember={(userId) => removeEditor({ projectId, userId })}
3745
title="Editors"
38-
onSettled={() =>
39-
Promise.all([
40-
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
46+
onSettled={() => {
47+
const promises = [
48+
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(projectId) }),
4149
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
42-
])
43-
}
50+
];
51+
onChange && promises.push(onChange());
52+
return Promise.all(promises);
53+
}}
4454
/>
4555
);
4656
};

0 commit comments

Comments
 (0)