diff --git a/Core/API/RoutesAPI.class.php b/Core/API/RoutesAPI.class.php index 94282fa..0a0bc2d 100644 --- a/Core/API/RoutesAPI.class.php +++ b/Core/API/RoutesAPI.class.php @@ -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; } diff --git a/Core/Driver/Logger/Logger.class.php b/Core/Driver/Logger/Logger.class.php index 9022ac4..fe34024 100644 --- a/Core/Driver/Logger/Logger.class.php +++ b/Core/Driver/Logger/Logger.class.php @@ -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(); diff --git a/Core/Objects/DatabaseEntity/Group.class.php b/Core/Objects/DatabaseEntity/Group.class.php index 25e7f22..c517342 100644 --- a/Core/Objects/DatabaseEntity/Group.class.php +++ b/Core/Objects/DatabaseEntity/Group.class.php @@ -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; + } + } } \ No newline at end of file diff --git a/react/admin-panel/src/views/access-control-list.js b/react/admin-panel/src/views/access-control-list.js index 9135b90..78924d3 100644 --- a/react/admin-panel/src/views/access-control-list.js +++ b/react/admin-panel/src/views/access-control-list.js @@ -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; } diff --git a/react/admin-panel/src/views/group/group-edit.js b/react/admin-panel/src/views/group/group-edit.js index 1170da3..04d59c5 100644 --- a/react/admin-panel/src/views/group/group-edit.js +++ b/react/admin-panel/src/views/group/group-edit.js @@ -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) { + { !isNewGroup && + + } {!isNewGroup && api.hasPermission("groups/getMembers") ? -
-
+
-
+
: <> } diff --git a/react/shared/api.js b/react/shared/api.js index 52e320a..8cde13f 100644 --- a/react/shared/api.js +++ b/react/shared/api.js @@ -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 }); diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index dc808f8..a7204d2 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -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} ); + break; + case 'custom': + let element = inputProps.element; + delete inputProps.element; + inputElements.push(React.createElement(element, inputProps)); + break; + default: + break; } } diff --git a/react/shared/elements/search-field.js b/react/shared/elements/search-field.js new file mode 100644 index 0000000..3c81533 --- /dev/null +++ b/react/shared/elements/search-field.js @@ -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 displayText(r)} + options={Object.values(results ?? {})} + onChange={(e, n) => onSelect(n)} + renderInput={(params) => ( + setSearchString(e.target.value)} + label={"Search input"} + InputProps={{ + ...params.InputProps, + type: 'search', + }} + /> + )} + />; +} \ No newline at end of file