Bugfix
This commit is contained in:
parent
f13ab7f9e0
commit
a7dc4c0d2f
@ -75,8 +75,8 @@ namespace Core\API\Routes {
|
|||||||
use Core\Objects\DatabaseEntity\Group;
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
use Core\Objects\DatabaseEntity\Route;
|
use Core\Objects\DatabaseEntity\Route;
|
||||||
use Core\Objects\Router\ApiRoute;
|
use Core\Objects\Router\ApiRoute;
|
||||||
|
use Core\Objects\Router\EmptyRoute;
|
||||||
use Core\Objects\Router\Router;
|
use Core\Objects\Router\Router;
|
||||||
use Core\Objects\Router\StaticRoute;
|
|
||||||
|
|
||||||
class Fetch extends RoutesAPI {
|
class Fetch extends RoutesAPI {
|
||||||
|
|
||||||
@ -363,7 +363,7 @@ namespace Core\API\Routes {
|
|||||||
$path = $this->getParam("path");
|
$path = $this->getParam("path");
|
||||||
$pattern = $this->getParam("pattern");
|
$pattern = $this->getParam("pattern");
|
||||||
$exact = $this->getParam("exact");
|
$exact = $this->getParam("exact");
|
||||||
$route = new StaticRoute($pattern, $exact, "");
|
$route = new EmptyRoute($pattern, $exact, "");
|
||||||
$this->result["match"] = $route->match($path);
|
$this->result["match"] = $route->match($path);
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class Logger {
|
|||||||
}, $debugTrace));
|
}, $debugTrace));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function log(string $message, string $severity, bool $appendStackTrace = true) {
|
public function log(string $message, string $severity, bool $appendStackTrace = true): void {
|
||||||
|
|
||||||
if ($appendStackTrace) {
|
if ($appendStackTrace) {
|
||||||
$message .= "\n" . $this->getStackTrace();
|
$message .= "\n" . $this->getStackTrace();
|
||||||
|
@ -44,4 +44,14 @@ class Group extends DatabaseEntity {
|
|||||||
new Group(Group::SUPPORT, Group::GROUPS[Group::SUPPORT], "#007bff"),
|
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];
|
const permission = acl[index];
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
if (!permission.method.toLowerCase().includes(query.toLowerCase()) ||
|
if (!permission.method.toLowerCase().includes(query.toLowerCase()) &&
|
||||||
!permission.description.toLowerCase().includes(query.toLowerCase())) {
|
!permission.description.toLowerCase().includes(query.toLowerCase())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {Link, useNavigate, useParams} from "react-router-dom";
|
import {Link, useNavigate, useParams} from "react-router-dom";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
|
import SearchField from "shared/elements/search-field";
|
||||||
import {Button, CircularProgress} from "@material-ui/core";
|
import {Button, CircularProgress} from "@material-ui/core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ColorPicker from "material-ui-color-picker";
|
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 {Delete, KeyboardArrowLeft, Save} from "@material-ui/icons";
|
||||||
import Dialog from "shared/elements/dialog";
|
import Dialog from "shared/elements/dialog";
|
||||||
import {Box, FormControl, FormGroup, FormLabel, styled, TextField} from "@mui/material";
|
import {Box, FormControl, FormGroup, FormLabel, styled, TextField} from "@mui/material";
|
||||||
|
import {Add} from "@mui/icons-material";
|
||||||
|
|
||||||
const defaultGroupData = {
|
const defaultGroupData = {
|
||||||
name: "",
|
name: "",
|
||||||
@ -37,6 +39,7 @@ export default function EditGroupView(props) {
|
|||||||
const [fetchGroup, setFetchGroup] = useState(!isNewGroup);
|
const [fetchGroup, setFetchGroup] = useState(!isNewGroup);
|
||||||
const [group, setGroup] = useState(isNewGroup ? defaultGroupData : null);
|
const [group, setGroup] = useState(isNewGroup ? defaultGroupData : null);
|
||||||
const [members, setMembers] = useState([]);
|
const [members, setMembers] = useState([]);
|
||||||
|
const [selectedUser, setSelectedUser] = useState(null);
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
const [dialogData, setDialogData] = useState({open: false});
|
const [dialogData, setDialogData] = useState({open: false});
|
||||||
@ -91,11 +94,11 @@ export default function EditGroupView(props) {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
if (isNewGroup) {
|
if (isNewGroup) {
|
||||||
api.createGroup(group.name, group.color).then(data => {
|
api.createGroup(group.name, group.color).then(data => {
|
||||||
|
setSaving(false);
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
props.showDialog(data.msg, "Error creating group");
|
props.showDialog(data.msg, "Error creating group");
|
||||||
setSaving(false);
|
|
||||||
} else {
|
} else {
|
||||||
navigate(`/admin/groups/${data.id}`)
|
navigate(`/admin/group/${data.id}`)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -108,6 +111,41 @@ export default function EditGroupView(props) {
|
|||||||
}
|
}
|
||||||
}, [api, groupId, isNewGroup, group]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
onFetchGroup();
|
onFetchGroup();
|
||||||
}, []);
|
}, []);
|
||||||
@ -170,19 +208,31 @@ export default function EditGroupView(props) {
|
|||||||
<Button startIcon={<KeyboardArrowLeft />}
|
<Button startIcon={<KeyboardArrowLeft />}
|
||||||
variant={"outlined"}
|
variant={"outlined"}
|
||||||
onClick={() => navigate("/admin/groups")}>
|
onClick={() => navigate("/admin/groups")}>
|
||||||
{L("general.cancel")}
|
{L("general.go_back")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button startIcon={<Save />} color={"primary"}
|
<Button startIcon={<Save />} color={"primary"}
|
||||||
variant={"outlined"} disabled={isSaving}
|
variant={"outlined"}
|
||||||
|
disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))}
|
||||||
onClick={onSave}>
|
onClick={onSave}>
|
||||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||||
</Button>
|
</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>
|
</ButtonBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
||||||
<div className={"m-3"}>
|
<div className={"m-3 col-6"}>
|
||||||
<div className={"col-6"}>
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={members}
|
data={members}
|
||||||
pagination={pagination}
|
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>
|
</div>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,10 @@ export default class API {
|
|||||||
return this.apiCall("user/create", { username: username, email: email, password: password, confirmPassword: confirmPassword });
|
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) {
|
async updateProfile(username=null, fullName=null, password=null, confirmPassword = null, oldPassword = null) {
|
||||||
let res = await this.apiCall("user/updateProfile", { username: username, fullName: fullName,
|
let res = await this.apiCall("user/updateProfile", { username: username, fullName: fullName,
|
||||||
password: password, confirmPassword: confirmPassword, oldPassword: oldPassword });
|
password: password, confirmPassword: confirmPassword, oldPassword: oldPassword });
|
||||||
|
@ -24,7 +24,7 @@ export default function Dialog(props) {
|
|||||||
if (props.inputs) {
|
if (props.inputs) {
|
||||||
let initialData = {};
|
let initialData = {};
|
||||||
for (const input of props.inputs) {
|
for (const input of props.inputs) {
|
||||||
if (input.type !== "label") {
|
if (input.type !== "label" && input.hasOwnProperty("name")) {
|
||||||
initialData[input.name] = input.value || "";
|
initialData[input.name] = input.value || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,6 +76,14 @@ export default function Dialog(props) {
|
|||||||
{listItems}
|
{listItems}
|
||||||
</List>
|
</List>
|
||||||
</Box>);
|
</Box>);
|
||||||
|
break;
|
||||||
|
case 'custom':
|
||||||
|
let element = inputProps.element;
|
||||||
|
delete inputProps.element;
|
||||||
|
inputElements.push(React.createElement(element, inputProps));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
react/shared/elements/search-field.js
Normal file
28
react/shared/elements/search-field.js
Normal 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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user