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

@ -75,8 +75,8 @@ namespace Core\API\Routes {
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\Route;
use Core\Objects\Router\ApiRoute;
use Core\Objects\Router\EmptyRoute;
use Core\Objects\Router\Router;
use Core\Objects\Router\StaticRoute;
class Fetch extends RoutesAPI {
@ -363,7 +363,7 @@ namespace Core\API\Routes {
$path = $this->getParam("path");
$pattern = $this->getParam("pattern");
$exact = $this->getParam("exact");
$route = new StaticRoute($pattern, $exact, "");
$route = new EmptyRoute($pattern, $exact, "");
$this->result["match"] = $route->match($path);
return $this->success;
}

@ -54,7 +54,7 @@ class Logger {
}, $debugTrace));
}
public function log(string $message, string $severity, bool $appendStackTrace = true) {
public function log(string $message, string $severity, bool $appendStackTrace = true): void {
if ($appendStackTrace) {
$message .= "\n" . $this->getStackTrace();

@ -44,4 +44,14 @@ class Group extends DatabaseEntity {
new Group(Group::SUPPORT, Group::GROUPS[Group::SUPPORT], "#007bff"),
];
}
public function delete(SQL $sql): bool {
if (parent::delete($sql)) {
$handler = User::getHandler($sql);
$table = $handler->getNMRelation("groups")->getTableName();
return $sql->delete($table)->whereEq("group_id", $this->id)->execute();
} else {
return false;
}
}
}

@ -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;
}

@ -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 => {
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>
: <></>
}

@ -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 });

@ -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;
}
}

@ -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',
}}
/>
)}
/>;
}