This commit is contained in:
2024-04-02 15:33:00 +02:00
parent f13ab7f9e0
commit a7dc4c0d2f
8 changed files with 132 additions and 13 deletions

View File

@@ -147,7 +147,7 @@ export default function AccessControlList(props) {
const permission = acl[index];
if (query) {
if (!permission.method.toLowerCase().includes(query.toLowerCase()) ||
if (!permission.method.toLowerCase().includes(query.toLowerCase()) &&
!permission.description.toLowerCase().includes(query.toLowerCase())) {
continue;
}

View File

@@ -1,6 +1,7 @@
import {useCallback, useContext, useEffect, useState} from "react";
import {Link, useNavigate, useParams} from "react-router-dom";
import {LocaleContext} from "shared/locale";
import SearchField from "shared/elements/search-field";
import {Button, CircularProgress} from "@material-ui/core";
import * as React from "react";
import ColorPicker from "material-ui-color-picker";
@@ -10,6 +11,7 @@ import usePagination from "shared/hooks/pagination";
import {Delete, KeyboardArrowLeft, Save} from "@material-ui/icons";
import Dialog from "shared/elements/dialog";
import {Box, FormControl, FormGroup, FormLabel, styled, TextField} from "@mui/material";
import {Add} from "@mui/icons-material";
const defaultGroupData = {
name: "",
@@ -37,6 +39,7 @@ export default function EditGroupView(props) {
const [fetchGroup, setFetchGroup] = useState(!isNewGroup);
const [group, setGroup] = useState(isNewGroup ? defaultGroupData : null);
const [members, setMembers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
// ui
const [dialogData, setDialogData] = useState({open: false});
@@ -91,11 +94,11 @@ export default function EditGroupView(props) {
setSaving(true);
if (isNewGroup) {
api.createGroup(group.name, group.color).then(data => {
if (!data.success) {
setSaving(false);
if (!data.success) {
props.showDialog(data.msg, "Error creating group");
setSaving(false);
} else {
navigate(`/admin/groups/${data.id}`)
navigate(`/admin/group/${data.id}`)
}
});
} else {
@@ -108,6 +111,41 @@ export default function EditGroupView(props) {
}
}, [api, groupId, isNewGroup, group]);
const onSearchUser = useCallback((async (query) => {
let data = await api.searchUser(query);
if (!data.success) {
props.showDialog(data.msg, "Error searching users");
return [];
}
return data.users;
}), [api]);
const onAddMember = useCallback(() => {
if (selectedUser) {
api.addGroupMember(groupId, selectedUser.id).then(data => {
if (!data.success) {
props.showDialog(data.msg, "Error adding member");
} else {
let newMembers = [...members];
newMembers.push(selectedUser);
setMembers(newMembers);
}
setSelectedUser(null);
});
}
}, [api, groupId, selectedUser])
const onDeleteGroup = useCallback(() => {
api.deleteGroup(groupId).then(data => {
if (!data.success) {
props.showDialog(data.msg, "Error deleting group");
} else {
navigate("/admin/groups");
}
});
}, [api, groupId]);
useEffect(() => {
onFetchGroup();
}, []);
@@ -170,19 +208,31 @@ export default function EditGroupView(props) {
<Button startIcon={<KeyboardArrowLeft />}
variant={"outlined"}
onClick={() => navigate("/admin/groups")}>
{L("general.cancel")}
{L("general.go_back")}
</Button>
<Button startIcon={<Save />} color={"primary"}
variant={"outlined"} disabled={isSaving}
variant={"outlined"}
disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))}
onClick={onSave}>
{isSaving ? L("general.saving") + "…" : L("general.save")}
</Button>
{ !isNewGroup &&
<Button startIcon={<Delete/>} disabled={!api.hasPermission("groups/delete")}
variant={"outlined"} color={"secondary"}
onClick={() => setDialogData({
open: true,
title: L("Delete Group"),
message: L("Do you really want to delete this group? This action cannot be undone."),
onOption: option => option === 0 && onDeleteGroup()
})}>
{L("general.delete")}
</Button>
}
</ButtonBar>
</div>
</div>
{!isNewGroup && api.hasPermission("groups/getMembers") ?
<div className={"m-3"}>
<div className={"col-6"}>
<div className={"m-3 col-6"}>
<DataTable
data={members}
pagination={pagination}
@@ -216,7 +266,26 @@ export default function EditGroupView(props) {
]),
]}
/>
</div>
<Button startIcon={<Add />} color={"primary"}
variant={"outlined"} disabled={!api.hasPermission("groups/addMember")}
onClick={() => setDialogData({
open: true,
title: L("Add member"),
message: "Search a user to add to the group",
inputs: [
{
type: "custom", name: "search", element: SearchField,
size: "small", key: "search",
onSearch: v => onSearchUser(v),
onSelect: u => setSelectedUser(u),
displayText: u => u.fullName || u.name
}
],
onOption: (option) => option === 0 ? onAddMember() : setSelectedUser(null)
})
}>
{L("general.add")}
</Button>
</div>
: <></>
}

View File

@@ -161,6 +161,10 @@ export default class API {
return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
}
async searchUser(query) {
return this.apiCall("user/search", { query : query });
}
async updateProfile(username=null, fullName=null, password=null, confirmPassword = null, oldPassword = null) {
let res = await this.apiCall("user/updateProfile", { username: username, fullName: fullName,
password: password, confirmPassword: confirmPassword, oldPassword: oldPassword });

View File

@@ -24,7 +24,7 @@ export default function Dialog(props) {
if (props.inputs) {
let initialData = {};
for (const input of props.inputs) {
if (input.type !== "label") {
if (input.type !== "label" && input.hasOwnProperty("name")) {
initialData[input.name] = input.value || "";
}
}
@@ -76,6 +76,14 @@ export default function Dialog(props) {
{listItems}
</List>
</Box>);
break;
case 'custom':
let element = inputProps.element;
delete inputProps.element;
inputElements.push(React.createElement(element, inputProps));
break;
default:
break;
}
}

View File

@@ -0,0 +1,28 @@
import {Autocomplete, TextField} from "@mui/material";
import useAsyncSearch from "../hooks/async-search";
export default function SearchField(props) {
const { onSearch, displayText, onSelect, ...other } = props;
const [searchString, setSearchString, results] = useAsyncSearch(props.onSearch, 3);
return <Autocomplete {...other}
getOptionLabel={r => displayText(r)}
options={Object.values(results ?? {})}
onChange={(e, n) => onSelect(n)}
renderInput={(params) => (
<TextField
{...params}
value={searchString}
onChange={e => setSearchString(e.target.value)}
label={"Search input"}
InputProps={{
...params.InputProps,
type: 'search',
}}
/>
)}
/>;
}