Skip to content

Commit

Permalink
feat(unit-user-usage): add initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverDudgeon committed Apr 4, 2024
1 parent 2a38ad1 commit a901ec8
Show file tree
Hide file tree
Showing 22 changed files with 801 additions and 124 deletions.
4 changes: 1 addition & 3 deletions components/Chips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ export const Chips = styled("div", { shouldForwardProp: (prop) => prop !== "spac
display: "flex",
alignItems: "center",
flexWrap: "wrap",
"& > *": {
margin: theme.spacing(spacing),
},
gap: theme.spacing(spacing),
}),
);
47 changes: 25 additions & 22 deletions components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface DataTableProps<Data extends Record<string, any>> {
/**
* Child element of the toolbar in the table header
*/
ToolbarChild?: ReactNode;
toolbarContent?: ReactNode;
/**
* Toolbar with actions which sits beneath the table header toolbar with search.
*/
Expand Down Expand Up @@ -150,7 +150,7 @@ export const DataTable = <Data extends Record<string, any>>(props: DataTableProp
columns,
data,
ToolbarActionChild,
ToolbarChild,
toolbarContent: ToolbarChild,
initialSelection,
onSelection,
subRowsEnabled,
Expand Down Expand Up @@ -296,29 +296,32 @@ export const DataTable = <Data extends Record<string, any>>(props: DataTableProp
{headerGroup.headers.map((header) => (
<TableCell
className={header.column.getCanSort() ? "cursor-pointer select-none" : ""}
colSpan={header.colSpan}
key={header.id}
>
<Box>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() ? (
<TableSortLabel
active={!!header.column.getIsSorted()}
// react-table has a unsorted state which is not treated here
direction={header.column.getIsSorted() || undefined}
onClick={header.column.getToggleSortingHandler()}
/>
) : null}
{header.column.getCanFilter() ? (
<div>
{/* TODO: debounce this field */}
<TextField
placeholder="Search"
value={header.column.getFilterValue()}
onChange={(event) => header.column.setFilterValue(event.target.value)}
{header.isPlaceholder ? null : (
<Box sx={{ textWrap: "nowrap" }}>
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() ? (
<TableSortLabel
active={!!header.column.getIsSorted()}
// react-table has a unsorted state which is not treated here
direction={header.column.getIsSorted() || undefined}
onClick={header.column.getToggleSortingHandler()}
/>
</div>
) : null}
</Box>
) : null}
{header.column.getCanFilter() ? (
<div>
{/* TODO: debounce this field */}
<TextField
placeholder="Search"
value={header.column.getFilterValue()}
onChange={(event) => header.column.setFilterValue(event.target.value)}
/>
</div>
) : null}
</Box>
)}
</TableCell>
))}
</TableRow>
Expand Down
7 changes: 6 additions & 1 deletion components/projects/CreateProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
useGetProductTypes,
} from "@squonk/account-server-client/product";
import type { DmError } from "@squonk/data-manager-client";
import { getGetUserInventoryQueryKey } from "@squonk/data-manager-client/inventory";
import { getGetProjectsQueryKey, useCreateProject } from "@squonk/data-manager-client/project";

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

queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetProductsQueryKey() });
typeof unitId === "string" &&
if (typeof unitId === "string") {
queryClient.invalidateQueries({ queryKey: getGetProductsForUnitQueryKey(unitId) });
queryClient.invalidateQueries({
queryKey: getGetUserInventoryQueryKey({ unit_id: unitId }),
});
}

setCurrentProjectId(project_id);
} catch (error) {
Expand Down
62 changes: 23 additions & 39 deletions components/projects/EditProjectButton/EditProjectButton.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
import type { ReactNode } from "react";
import { useState } from "react";

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

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

import { ModalWrapper } from "../../modals/ModalWrapper";
import { PrivateProjectToggle } from "./PrivateProjectToggle";
import { ProjectAdministrators } from "./ProjectAdministrators";
import { ProjectEditors } from "./ProjectEditors";
import { ProjectObservers } from "./ProjectObservers";
interface ChildProps {
openDialog: () => void;
open: boolean;
}

export interface EditProjectButtonProps {
/**
* Project to be edited.
* ID of project to be edited.
*/
projectId: string;
/**
* child render prop
*/
project: ProjectDetail;
children: (props: ChildProps) => ReactNode;
}

/**
* Button controlling a modal with options to edit the project editors
*/
export const EditProjectButton = ({ project }: EditProjectButtonProps) => {
export const EditProjectButton = ({ projectId, children }: EditProjectButtonProps) => {
const [open, setOpen] = useState(false);

const { data: project } = useGetProject(projectId);

if (!project) {
return null;
}

return (
<>
<Tooltip title={"Edit Project"}>
<span>
<IconButton size="small" sx={{ p: "1px" }} onClick={() => setOpen(!open)}>
<EditIcon />
</IconButton>
</span>
</Tooltip>

<ModalWrapper
DialogProps={{ maxWidth: "sm", fullWidth: true }}
id="edit-project"
open={open}
submitText="Save"
title="Edit Project"
onClose={() => setOpen(false)}
>
<Typography gutterBottom variant="h5">
{project.name}
</Typography>

<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />

<Box display="flex" flexDirection="column" gap={2}>
<ProjectAdministrators project={project} />
<ProjectEditors project={project} />
<ProjectObservers project={project} />
</Box>
</ModalWrapper>
{children({ openDialog: () => setOpen(true), open })}

<EditProjectModal open={open} projectId={project.project_id} onClose={() => setOpen(false)} />
</>
);
};
64 changes: 64 additions & 0 deletions components/projects/EditProjectButton/EditProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useGetProject } from "@squonk/data-manager-client/project";

import { Box, Typography } from "@mui/material";

import { ModalWrapper } from "../../modals/ModalWrapper";
import { PrivateProjectToggle } from "./PrivateProjectToggle";
import { ProjectAdministrators } from "./ProjectAdministrators";
import { ProjectEditors } from "./ProjectEditors";
import { ProjectObservers } from "./ProjectObservers";

export interface EditProjectModalProps {
projectId: string;
open: boolean;
onClose: () => void;
onMemberChange?: () => Promise<void>;
}

export const EditProjectModal = ({
open,
projectId,
onClose,
onMemberChange,
}: EditProjectModalProps) => {
const { data: project } = useGetProject(projectId);

if (project === undefined) {
return null;
}

return (
<ModalWrapper
DialogProps={{ maxWidth: "sm", fullWidth: true }}
id="edit-project"
open={open}
submitText="Save"
title="Edit Project"
onClose={onClose}
>
<Typography gutterBottom variant="h5">
{project.name}
</Typography>

<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />

<Box display="flex" flexDirection="column" gap={2}>
<ProjectAdministrators
administrators={project.administrators}
projectId={project.project_id}
onChange={onMemberChange}
/>
<ProjectEditors
editors={project.editors}
projectId={project.project_id}
onChange={onMemberChange}
/>
<ProjectObservers
observers={project.observers}
projectId={project.project_id}
onChange={onMemberChange}
/>
</Box>
</ModalWrapper>
);
};
34 changes: 24 additions & 10 deletions components/projects/EditProjectButton/ProjectAdministrators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,25 @@ export interface ProjectAdministratorsProps {
/**
* Project to be edited.
*/
project: ProjectDetail;
projectId: ProjectDetail["project_id"];
/**
* administrators
*/
administrators: ProjectDetail["administrators"];
/**
* onChange function to be called after the project administrators have been updated
*/
onChange?: () => Promise<void>;
}

/**
* MuiAutocomplete to manage the current administrators of the selected project
*/
export const ProjectAdministrators = ({ project }: ProjectAdministratorsProps) => {
export const ProjectAdministrators = ({
projectId,
administrators,
onChange,
}: ProjectAdministratorsProps) => {
const { isLoading: isProjectsLoading } = useGetProjects();

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

return (
<ProjectMemberSelection
addMember={(userId) => addAdministrator({ projectId: project.project_id, userId })}
addMember={(userId) => addAdministrator({ projectId, userId })}
isLoading={isAdding || isRemoving || isProjectsLoading}
memberList={project.administrators}
removeMember={(userId) => removeAdministrator({ projectId: project.project_id, userId })}
memberList={administrators}
removeMember={(userId) => removeAdministrator({ projectId, userId })}
title="Administrators"
onSettled={() =>
Promise.all([
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
onSettled={() => {
const promises = [
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(projectId) }),
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
])
}
];
onChange && promises.push(onChange());
return Promise.all(promises);
}}
/>
);
};
30 changes: 20 additions & 10 deletions components/projects/EditProjectButton/ProjectEditors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ export interface ProjectEditorsProps {
/**
* Project to be edited.
*/
project: ProjectDetail;
projectId: ProjectDetail["project_id"];
/**
* editors
*/
editors: ProjectDetail["editors"];
/**
* onChange function to be called after the project editors have been updated
*/
onChange?: () => Promise<void>;
}

/**
* MuiAutocomplete to manage the current editors of the selected project
*/
export const ProjectEditors = ({ project }: ProjectEditorsProps) => {
export const ProjectEditors = ({ projectId, editors, onChange }: ProjectEditorsProps) => {
const { isLoading: isProjectsLoading } = useGetProjects();

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

return (
<ProjectMemberSelection
addMember={(userId) => addEditor({ projectId: project.project_id, userId })}
addMember={(userId) => addEditor({ projectId, userId })}
isLoading={isAdding || isRemoving || isProjectsLoading}
memberList={project.editors}
removeMember={(userId) => removeEditor({ projectId: project.project_id, userId })}
memberList={editors}
removeMember={(userId) => removeEditor({ projectId, userId })}
title="Editors"
onSettled={() =>
Promise.all([
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
onSettled={() => {
const promises = [
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(projectId) }),
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
])
}
];
onChange && promises.push(onChange());
return Promise.all(promises);
}}
/>
);
};
Loading

0 comments on commit a901ec8

Please sign in to comment.