mirror of
https://github.com/hexastack/hexabot
synced 2025-04-28 02:02:05 +00:00
refactor(frontend): update permissions dialogs
This commit is contained in:
parent
14dbe5075a
commit
58c96417b5
251
frontend/src/components/roles/PermissionsBody.tsx
Normal file
251
frontend/src/components/roles/PermissionsBody.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Divider,
|
||||
Grid,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { FC, Fragment, useEffect, useState } from "react";
|
||||
|
||||
import { IconButton } from "@/app-components/buttons/IconButton";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { ComponentFormProps } from "@/types/common/dialogs.types";
|
||||
import { IPermission, IPermissionAttributes } from "@/types/permission.types";
|
||||
import { IRole } from "@/types/role.types";
|
||||
|
||||
const DEFAULT_PAYLOAD: IPermissionAttributes = {
|
||||
action: "",
|
||||
model: "",
|
||||
relation: "",
|
||||
role: "",
|
||||
};
|
||||
const AccordionModelHead = () => (
|
||||
<Grid container direction="row" minHeight="6rem" alignContent="center" mb={1}>
|
||||
<Grid item width="6rem" />
|
||||
<Grid item xs textAlign="left">
|
||||
<Typography fontWeight={700} fontSize="body2.fontSize">
|
||||
Action
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs textAlign="left">
|
||||
<Typography fontWeight={700} fontSize="body2.fontSize">
|
||||
Relation
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
export const PermissionsBody: FC<ComponentFormProps<IRole>> = ({
|
||||
data: role,
|
||||
Wrapper = Fragment,
|
||||
WrapperProps,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { data: models, refetch: modelRefetch } = useFind(
|
||||
{ entity: EntityType.MODEL, format: Format.FULL },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const getPermissionFromCache = useGetFromCache(EntityType.PERMISSION);
|
||||
const options = {
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
modelRefetch();
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
};
|
||||
const { mutate: createPermission } = useCreate(EntityType.PERMISSION, {
|
||||
...options,
|
||||
onError: (error: Error & { statusCode?: number }) => {
|
||||
rest.onError?.();
|
||||
if (error.statusCode === 409) {
|
||||
toast.error(t("message.permission_already_exists"));
|
||||
} else {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
}
|
||||
},
|
||||
});
|
||||
const { mutate: deletePermission } = useDelete(
|
||||
EntityType.PERMISSION,
|
||||
options,
|
||||
);
|
||||
const [expanded, setExpanded] = useState<string | false>(false);
|
||||
const [payload, setPayload] =
|
||||
useState<IPermissionAttributes>(DEFAULT_PAYLOAD);
|
||||
const reset = () => setPayload(DEFAULT_PAYLOAD);
|
||||
const handleChange =
|
||||
(panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
||||
setExpanded(isExpanded ? panel : false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded === false && models?.[0]?.id) setExpanded(models[0].id);
|
||||
}, [models]);
|
||||
|
||||
return (
|
||||
<Wrapper open={!!WrapperProps?.open} onSubmit={() => {}} {...WrapperProps}>
|
||||
<Typography fontWeight={700} sx={{ marginBottom: 2 }}>
|
||||
{role?.name}
|
||||
</Typography>
|
||||
{models?.map((model) => (
|
||||
<Accordion
|
||||
key={model.id}
|
||||
expanded={expanded === model.id}
|
||||
onChange={handleChange(model.id)}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
boxShadow: "none",
|
||||
"&:before": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<KeyboardArrowUpIcon />}
|
||||
sx={{
|
||||
backgroundColor: "background.default",
|
||||
borderRadius: 1,
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
>
|
||||
<Typography>{model.name}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, m: 0 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<AccordionModelHead />
|
||||
{model.permissions
|
||||
?.map((p) => getPermissionFromCache(p))
|
||||
?.filter(
|
||||
(permission) => permission && permission.role === role?.id,
|
||||
)
|
||||
.map((p) => p as IPermission)
|
||||
.map(({ id, action, relation }, index) => {
|
||||
return (
|
||||
<>
|
||||
{index > 0 && <Divider />}
|
||||
<Grid
|
||||
container
|
||||
key={id}
|
||||
sx={{
|
||||
borderRadius: 0.8,
|
||||
padding: 1,
|
||||
"&:hover": {
|
||||
backgroundColor: "background.default",
|
||||
},
|
||||
}}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item width="6rem">
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={() => deletePermission(id)}
|
||||
size="small"
|
||||
>
|
||||
<DeleteOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Typography>{action}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Typography>{relation}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<Grid container minHeight="2.5rem" padding={1}>
|
||||
<Grid item width="6rem" alignContent="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
if (role?.id)
|
||||
createPermission({
|
||||
...payload,
|
||||
role: role.id,
|
||||
model: model.id,
|
||||
});
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs alignContent="center">
|
||||
<Input
|
||||
select
|
||||
sx={{ width: "6.875rem" }}
|
||||
label="Action"
|
||||
value={payload.action}
|
||||
onChange={(e) => {
|
||||
if (e.target.value)
|
||||
setPayload((currentPayload) => ({
|
||||
...currentPayload,
|
||||
action: e.target.value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<MenuItem value="create">{t("label.create")}</MenuItem>
|
||||
<MenuItem value="read">{t("label.read")}</MenuItem>
|
||||
<MenuItem value="update">{t("label.update")}</MenuItem>
|
||||
<MenuItem value="delete">{t("label.delete")}</MenuItem>
|
||||
</Input>
|
||||
</Grid>
|
||||
<Grid item xs alignContent="center">
|
||||
<Input
|
||||
select
|
||||
sx={{ width: "6.875rem" }}
|
||||
label={t("label.relation")}
|
||||
value={payload.relation}
|
||||
onChange={(e) => {
|
||||
if (e.target.value)
|
||||
setPayload((currentPayload) => ({
|
||||
...currentPayload,
|
||||
relation: e.target.value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<MenuItem value="role">{t("label.role")}</MenuItem>
|
||||
</Input>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
23
frontend/src/components/roles/PermissionsBodyDialog.tsx
Normal file
23
frontend/src/components/roles/PermissionsBodyDialog.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { GenericFormDialog } from "@/app-components/dialogs";
|
||||
import { ComponentFormDialogProps } from "@/types/common/dialogs.types";
|
||||
import { IRole } from "@/types/role.types";
|
||||
|
||||
import { PermissionsBody } from "./PermissionsBody";
|
||||
|
||||
export const PermissionBodyDialog = <T extends IRole = IRole>(
|
||||
props: ComponentFormDialogProps<T>,
|
||||
) => (
|
||||
<GenericFormDialog<T>
|
||||
Form={PermissionsBody}
|
||||
editText="title.manage_permissions"
|
||||
{...props}
|
||||
/>
|
||||
);
|
@ -1,278 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Dialog,
|
||||
Grid,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Typography,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import { useState, FC, useEffect } from "react";
|
||||
|
||||
import { IconButton } from "@/app-components/buttons/IconButton";
|
||||
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
|
||||
import { Input } from "@/app-components/inputs/Input";
|
||||
import { useCreate } from "@/hooks/crud/useCreate";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useGetFromCache } from "@/hooks/crud/useGet";
|
||||
import { DialogControlProps } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType, Format } from "@/services/types";
|
||||
import { IPermission, IPermissionAttributes } from "@/types/permission.types";
|
||||
import { IRole } from "@/types/role.types";
|
||||
|
||||
export type PermissionsDialogProps = DialogControlProps<{
|
||||
role: IRole;
|
||||
}>;
|
||||
|
||||
const DEFAULT_PAYLOAD: IPermissionAttributes = {
|
||||
action: "",
|
||||
model: "",
|
||||
relation: "",
|
||||
role: "",
|
||||
};
|
||||
const AccordionModelHead = () => (
|
||||
<Grid container direction="row" minHeight="35px" alignContent="center" mb={1}>
|
||||
<Grid item width="96px" />
|
||||
<Grid item xs textAlign="left">
|
||||
<Typography fontWeight={700} fontSize="body2.fontSize">
|
||||
Action
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs textAlign="left">
|
||||
<Typography fontWeight={700} fontSize="body2.fontSize">
|
||||
Relation
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
export const PermissionsDialog: FC<PermissionsDialogProps> = ({
|
||||
open,
|
||||
data,
|
||||
closeDialog: closeFunction,
|
||||
}) => {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { data: models, refetch: modelRefetch } = useFind(
|
||||
{ entity: EntityType.MODEL, format: Format.FULL },
|
||||
{
|
||||
hasCount: false,
|
||||
},
|
||||
);
|
||||
const getPermisionFromCache = useGetFromCache(EntityType.PERMISSION);
|
||||
const { mutateAsync: createPermission } = useCreate(EntityType.PERMISSION, {
|
||||
onError: (error: Error & { statusCode?: number }) => {
|
||||
if (error.statusCode === 409) {
|
||||
toast.error(t("message.permission_already_exists"));
|
||||
} else {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
modelRefetch();
|
||||
toast.success(t("message.success_save"));
|
||||
},
|
||||
});
|
||||
const { mutateAsync: deletePermission } = useDelete(EntityType.PERMISSION, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
modelRefetch();
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
});
|
||||
const [expanded, setExpanded] = useState<string | false>(false);
|
||||
const [payload, setPayload] =
|
||||
useState<IPermissionAttributes>(DEFAULT_PAYLOAD);
|
||||
const reset = () => setPayload(DEFAULT_PAYLOAD);
|
||||
const handleChange =
|
||||
(panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
||||
setExpanded(isExpanded ? panel : false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded === false && models?.[0]?.id) setExpanded(models[0].id);
|
||||
}, [models]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
fullWidth
|
||||
onClose={closeFunction}
|
||||
sx={{ maxWidth: "850px", margin: "auto" }}
|
||||
maxWidth="md"
|
||||
>
|
||||
<DialogTitle onClose={closeFunction}>
|
||||
{t("title.manage_permissions")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography fontWeight={700} sx={{ marginBottom: 2 }}>
|
||||
{data?.role.name}
|
||||
</Typography>
|
||||
{models?.map((model) => {
|
||||
return (
|
||||
<Accordion
|
||||
key={model.id}
|
||||
expanded={expanded === model.id}
|
||||
onChange={handleChange(model.id)}
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
boxShadow: "none",
|
||||
"&:before": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<KeyboardArrowUpIcon />}
|
||||
sx={{
|
||||
backgroundColor: "background.default",
|
||||
borderRadius: 1,
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
>
|
||||
<Typography>{model.name}</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 0, m: 0 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<AccordionModelHead />
|
||||
{model.permissions
|
||||
?.map((p) => getPermisionFromCache(p))
|
||||
?.filter(
|
||||
(permission) =>
|
||||
permission && permission.role === data?.role.id,
|
||||
)
|
||||
.map((p) => p as IPermission)
|
||||
.map(({ id, action, relation }, index) => {
|
||||
return (
|
||||
<>
|
||||
{index > 0 && <Divider />}
|
||||
<Grid
|
||||
container
|
||||
key={id}
|
||||
sx={{
|
||||
borderRadius: 0.8,
|
||||
padding: 1,
|
||||
"&:hover": {
|
||||
backgroundColor: "background.default",
|
||||
},
|
||||
}}
|
||||
alignItems="center"
|
||||
>
|
||||
<Grid item width="96px">
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
deletePermission(id);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<DeleteOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Typography>{action}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
<Typography>{relation}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<Grid container minHeight="40px" padding={1}>
|
||||
<Grid item width="96px" alignContent="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
if (data?.role.id)
|
||||
createPermission({
|
||||
...payload,
|
||||
model: model.id,
|
||||
role: data.role.id,
|
||||
});
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs alignContent="center">
|
||||
<Input
|
||||
select
|
||||
sx={{ width: "110px" }}
|
||||
label="Action"
|
||||
value={payload.action}
|
||||
onChange={(e) => {
|
||||
if (e.target.value)
|
||||
setPayload((currentPayload) => ({
|
||||
...currentPayload,
|
||||
action: e.target.value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<MenuItem value="create">{t("label.create")}</MenuItem>
|
||||
<MenuItem value="read">{t("label.read")}</MenuItem>
|
||||
<MenuItem value="update">{t("label.update")}</MenuItem>
|
||||
<MenuItem value="delete">{t("label.delete")}</MenuItem>
|
||||
</Input>
|
||||
</Grid>
|
||||
<Grid item xs alignContent="center">
|
||||
<Input
|
||||
select
|
||||
sx={{ width: "110px" }}
|
||||
label={t("label.relation")}
|
||||
value={payload.relation}
|
||||
onChange={(e) => {
|
||||
if (e.target.value)
|
||||
setPayload((currentPayload) => ({
|
||||
...currentPayload,
|
||||
relation: e.target.value,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<MenuItem value="role">{t("label.role")}</MenuItem>
|
||||
</Input>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={closeFunction}>
|
||||
{t("button.close")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user