shared frontend, UserAPI invalidate sessions, bugfixes, stuff
This commit is contained in:
parent
8a09fc1f2d
commit
5acd13b945
@ -76,13 +76,9 @@ namespace Core\API\Permission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// user would have required groups, check for 2fa-state
|
// user would have required groups, check for 2fa-state
|
||||||
if ($currentUser) {
|
if ($currentUser && !$this->check2FA()) {
|
||||||
$tfaToken = $currentUser->getTwoFactorToken();
|
http_response_code(401);
|
||||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
return false;
|
||||||
$this->lastError = '2FA-Authorization is required';
|
|
||||||
http_response_code(401);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ namespace Core\API;
|
|||||||
use Core\Driver\Logger\Logger;
|
use Core\Driver\Logger\Logger;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
use Core\Objects\Context;
|
use Core\Objects\Context;
|
||||||
|
use Core\Objects\DatabaseEntity\TwoFactorToken;
|
||||||
|
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
|
||||||
use PhpMqtt\Client\MqttClient;
|
use PhpMqtt\Client\MqttClient;
|
||||||
|
|
||||||
abstract class Request {
|
abstract class Request {
|
||||||
@ -126,6 +128,33 @@ abstract class Request {
|
|||||||
protected abstract function _execute(): bool;
|
protected abstract function _execute(): bool;
|
||||||
public static function getDefaultACL(Insert $insert): void { }
|
public static function getDefaultACL(Insert $insert): void { }
|
||||||
|
|
||||||
|
protected function check2FA(?TwoFactorToken $tfaToken = null): bool {
|
||||||
|
|
||||||
|
// do not require 2FA for verifying endpoints
|
||||||
|
if ($this instanceof \Core\API\Tfa\VerifyTotp || $this instanceof \Core\API\Tfa\VerifyKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tfaToken === null) {
|
||||||
|
$tfaToken = $this->context->getUser()?->getTwoFactorToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
||||||
|
|
||||||
|
if ($tfaToken instanceof KeyBasedTwoFactorToken && !$tfaToken->hasChallenge()) {
|
||||||
|
$tfaToken->generateChallenge();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->lastError = '2FA-Authorization is required';
|
||||||
|
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
|
||||||
|
"type", "challenge", "authenticated", "confirmed", "credentialID"
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public final function execute($values = array()): bool {
|
public final function execute($values = array()): bool {
|
||||||
|
|
||||||
$this->params = array_merge([], $this->defaultParams);
|
$this->params = array_merge([], $this->defaultParams);
|
||||||
@ -196,15 +225,9 @@ abstract class Request {
|
|||||||
$this->lastError = 'You are not logged in.';
|
$this->lastError = 'You are not logged in.';
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
return false;
|
return false;
|
||||||
} else if ($session) {
|
} else if ($session && !$this->check2FA()) {
|
||||||
$tfaToken = $session->getUser()->getTwoFactorToken();
|
http_response_code(401);
|
||||||
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
return false;
|
||||||
if (!($this instanceof \Core\API\Tfa\VerifyTotp) && !($this instanceof \Core\API\Tfa\VerifyKey)) {
|
|
||||||
$this->lastError = '2FA-Authorization is required';
|
|
||||||
http_response_code(401);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ use Core\Driver\SQL\Expression\Count;
|
|||||||
use Core\Driver\SQL\Expression\Distinct;
|
use Core\Driver\SQL\Expression\Distinct;
|
||||||
use Core\Driver\SQL\Query\Insert;
|
use Core\Driver\SQL\Query\Insert;
|
||||||
use Core\Objects\DatabaseEntity\Group;
|
use Core\Objects\DatabaseEntity\Group;
|
||||||
|
use Core\Objects\DatabaseEntity\Route;
|
||||||
|
use Core\Objects\DatabaseEntity\User;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Core\Driver\SQL\Condition\Compare;
|
use Core\Driver\SQL\Condition\Compare;
|
||||||
use Core\Driver\SQL\Condition\CondBool;
|
use Core\Driver\SQL\Condition\CondBool;
|
||||||
@ -20,26 +22,6 @@ class Stats extends Request {
|
|||||||
parent::__construct($context, $externalCall, array());
|
parent::__construct($context, $externalCall, array());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getUserCount(): int {
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$res = $sql->select(new Count())->from("User")->execute();
|
|
||||||
$this->success = $this->success && ($res !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
|
||||||
|
|
||||||
return ($this->success ? intval($res[0]["count"]) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPageCount(): int {
|
|
||||||
$sql = $this->context->getSQL();
|
|
||||||
$res = $sql->select(new Count())->from("Route")
|
|
||||||
->where(new CondBool("active"))
|
|
||||||
->execute();
|
|
||||||
$this->success = $this->success && ($res !== FALSE);
|
|
||||||
$this->lastError = $sql->getLastError();
|
|
||||||
|
|
||||||
return ($this->success ? intval($res[0]["count"]) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function checkSettings(): bool {
|
private function checkSettings(): bool {
|
||||||
$req = new \Core\API\Settings\Get($this->context);
|
$req = new \Core\API\Settings\Get($this->context);
|
||||||
$this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$"));
|
$this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$"));
|
||||||
@ -72,8 +54,9 @@ class Stats extends Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function _execute(): bool {
|
public function _execute(): bool {
|
||||||
$userCount = $this->getUserCount();
|
$sql = $this->context->getSQL();
|
||||||
$pageCount = $this->getPageCount();
|
$userCount = User::count($sql);
|
||||||
|
$pageCount = Route::count($sql, new CondBool("active"));
|
||||||
$req = new \Core\API\Visitors\Stats($this->context);
|
$req = new \Core\API\Visitors\Stats($this->context);
|
||||||
$this->success = $req->execute(array("type"=>"monthly"));
|
$this->success = $req->execute(array("type"=>"monthly"));
|
||||||
$this->lastError = $req->getLastError();
|
$this->lastError = $req->getLastError();
|
||||||
|
@ -189,6 +189,11 @@ namespace Core\API\TFA {
|
|||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
$this->success = $twoFactorToken->confirm($sql) !== false;
|
$this->success = $twoFactorToken->confirm($sql) !== false;
|
||||||
$this->lastError = $sql->getLastError();
|
$this->lastError = $sql->getLastError();
|
||||||
|
|
||||||
|
if ($this->success) {
|
||||||
|
$this->context->invalidateSessions(true);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,6 +320,7 @@ namespace Core\API\TFA {
|
|||||||
|
|
||||||
if ($this->success) {
|
if ($this->success) {
|
||||||
$this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize();
|
$this->result["twoFactorToken"] = $twoFactorToken->jsonSerialize();
|
||||||
|
$this->context->invalidateSessions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +222,8 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
public function __construct(Context $context, $externalCall = false) {
|
public function __construct(Context $context, $externalCall = false) {
|
||||||
parent::__construct($context, $externalCall,
|
parent::__construct($context, $externalCall,
|
||||||
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'])
|
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'],
|
||||||
|
'id', 'asc')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,8 +254,8 @@ namespace Core\API\User {
|
|||||||
|
|
||||||
$groupNames = new Alias(
|
$groupNames = new Alias(
|
||||||
$sql->select(new JsonArrayAgg("name"))->from("Group")
|
$sql->select(new JsonArrayAgg("name"))->from("Group")
|
||||||
->leftJoin("NM_Group_User", "NM_Group_User.group_id", "Group.id")
|
->leftJoin("NM_User_groups", "NM_User_groups.group_id", "Group.id")
|
||||||
->whereEq("NM_Group_User.user_id", new Column("User.id")),
|
->whereEq("NM_User_groups.user_id", new Column("User.id")),
|
||||||
"groups"
|
"groups"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -588,15 +589,7 @@ namespace Core\API\User {
|
|||||||
$this->result["user"] = $user->jsonSerialize();
|
$this->result["user"] = $user->jsonSerialize();
|
||||||
$this->result["session"] = $session->jsonSerialize();
|
$this->result["session"] = $session->jsonSerialize();
|
||||||
$this->result["logoutIn"] = $session->getExpiresSeconds();
|
$this->result["logoutIn"] = $session->getExpiresSeconds();
|
||||||
if ($tfaToken && $tfaToken->isConfirmed()) {
|
$this->check2FA($tfaToken);
|
||||||
if ($tfaToken instanceof KeyBasedTwoFactorToken) {
|
|
||||||
$tfaToken->generateChallenge();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->result["twoFactorToken"] = $tfaToken->jsonSerialize([
|
|
||||||
"type", "challenge", "authenticated", "confirmed", "credentialID"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$this->success = true;
|
$this->success = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1116,6 +1109,7 @@ namespace Core\API\User {
|
|||||||
if ($user->save($sql)) {
|
if ($user->save($sql)) {
|
||||||
$this->logger->info("Issued password reset for user id=" . $user->getId());
|
$this->logger->info("Issued password reset for user id=" . $user->getId());
|
||||||
$userToken->invalidate($sql);
|
$userToken->invalidate($sql);
|
||||||
|
$this->context->invalidateSessions(false);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return $this->createError("Error updating user details: " . $sql->getLastError());
|
return $this->createError("Error updating user details: " . $sql->getLastError());
|
||||||
@ -1152,6 +1146,7 @@ namespace Core\API\User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sql = $this->context->getSQL();
|
$sql = $this->context->getSQL();
|
||||||
|
$updateFields = [];
|
||||||
|
|
||||||
$currentUser = $this->context->getUser();
|
$currentUser = $this->context->getUser();
|
||||||
if ($newUsername !== null) {
|
if ($newUsername !== null) {
|
||||||
@ -1159,11 +1154,13 @@ namespace Core\API\User {
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
$currentUser->name = $newUsername;
|
$currentUser->name = $newUsername;
|
||||||
|
$updateFields[] = "name";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($newFullName !== null) {
|
if ($newFullName !== null) {
|
||||||
$currentUser->fullName = $newFullName;
|
$currentUser->fullName = $newFullName;
|
||||||
|
$updateFields[] = "fullName";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
if ($newPassword !== null || $newPasswordConfirm !== null) {
|
||||||
@ -1175,11 +1172,17 @@ namespace Core\API\User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$currentUser->password = $this->hashPassword($newPassword);
|
$currentUser->password = $this->hashPassword($newPassword);
|
||||||
|
$updateFields[] = "password";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->success = $currentUser->save($sql) !== false;
|
if (!empty($updateFields)) {
|
||||||
$this->lastError = $sql->getLastError();
|
$this->success = $currentUser->save($sql, $updateFields) !== false;
|
||||||
|
$this->lastError = $sql->getLastError();
|
||||||
|
if ($this->success && in_array("password", $updateFields)) {
|
||||||
|
$this->context->invalidateSessions(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,6 @@ class CreateDatabase extends DatabaseScript {
|
|||||||
$method = $reflectionClass->getName() . "::getDefaultACL";
|
$method = $reflectionClass->getName() . "::getDefaultACL";
|
||||||
$method($query);
|
$method($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($query->hasRows()) {
|
if ($query->hasRows()) {
|
||||||
$queries[] = $query;
|
$queries[] = $query;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,10 @@ class Settings {
|
|||||||
public static function loadDefaults(): Settings {
|
public static function loadDefaults(): Settings {
|
||||||
$hostname = $_SERVER["SERVER_NAME"] ?? null;
|
$hostname = $_SERVER["SERVER_NAME"] ?? null;
|
||||||
if (empty($hostname)) {
|
if (empty($hostname)) {
|
||||||
$hostname = "localhost";
|
$hostname = $_SERVER["HTTP_HOST"];
|
||||||
|
if (empty($hostname)) {
|
||||||
|
$hostname = "localhost";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$protocol = getProtocol();
|
$protocol = getProtocol();
|
||||||
|
34
Core/Driver/SQL/Expression/JsonObjectAgg.class.php
Normal file
34
Core/Driver/SQL/Expression/JsonObjectAgg.class.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\Column\Column;
|
||||||
|
use Core\Driver\SQL\MySQL;
|
||||||
|
use Core\Driver\SQL\PostgreSQL;
|
||||||
|
use Core\Driver\SQL\SQL;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class JsonObjectAgg extends Expression {
|
||||||
|
|
||||||
|
private mixed $key;
|
||||||
|
private mixed $value;
|
||||||
|
|
||||||
|
public function __construct(mixed $key, mixed $value) {
|
||||||
|
$this->key = $key;
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpression(SQL $sql, array &$params): string {
|
||||||
|
$value = is_string($this->value) ? new Column($this->value) : $this->value;
|
||||||
|
$value = $sql->addValue($value, $params);
|
||||||
|
$key = is_string($this->key) ? new Column($this->key) : $this->key;
|
||||||
|
$key = $sql->addValue($key, $params);
|
||||||
|
if ($sql instanceof MySQL) {
|
||||||
|
return "JSON_OBJECTAGG($key, $value)";
|
||||||
|
} else if ($sql instanceof PostgreSQL) {
|
||||||
|
return "JSON_OBJECT_AGG($value)";
|
||||||
|
} else {
|
||||||
|
throw new Exception("JsonObjectAgg not implemented for driver type: " . get_class($sql));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -211,4 +211,16 @@ class Context {
|
|||||||
public function getLanguage(): Language {
|
public function getLanguage(): Language {
|
||||||
return $this->language;
|
return $this->language;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function invalidateSessions(bool $keepCurrent = true): bool {
|
||||||
|
$query = $this->sql->update("Session")
|
||||||
|
->set("active", false)
|
||||||
|
->whereEq("user_id", $this->user->getId());
|
||||||
|
|
||||||
|
if (!$keepCurrent && $this->session !== null) {
|
||||||
|
$query->whereNeq("id", $this->session->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->execute();
|
||||||
|
}
|
||||||
}
|
}
|
@ -36,6 +36,10 @@ class KeyBasedTwoFactorToken extends TwoFactorToken {
|
|||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasChallenge(): bool {
|
||||||
|
return isset($this->challenge);
|
||||||
|
}
|
||||||
|
|
||||||
public function getChallenge(): string {
|
public function getChallenge(): string {
|
||||||
return $this->challenge;
|
return $this->challenge;
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import {Link, Navigate, useNavigate} from "react-router-dom";
|
import {Link, Navigate, useNavigate} from "react-router-dom";
|
||||||
import {useCallback, useContext, useEffect} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||||
import {Button, IconButton} from "@material-ui/core";
|
import {Button, IconButton} from "@material-ui/core";
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import {Chip} from "@mui/material";
|
import {Chip} from "@mui/material";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import usePagination from "shared/hooks/pagination";
|
||||||
|
|
||||||
|
|
||||||
export default function UserListView(props) {
|
export default function UserListView(props) {
|
||||||
|
|
||||||
|
const api = props.api;
|
||||||
|
const showDialog = props.showDialog;
|
||||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const pagination = usePagination();
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||||
@ -21,15 +26,17 @@ export default function UserListView(props) {
|
|||||||
});
|
});
|
||||||
}, [currentLocale]);
|
}, [currentLocale]);
|
||||||
|
|
||||||
const onFetchUsers = useCallback(async (page, count, orderBy, sortOrder) => {
|
const onFetchUsers = useCallback((page, count, orderBy, sortOrder) => {
|
||||||
let res = await props.api.fetchUsers(page, count, orderBy, sortOrder);
|
api.fetchUsers(page, count, orderBy, sortOrder).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
return Promise.resolve([res.users, res.pagination]);
|
setUsers(res.users);
|
||||||
} else {
|
pagination.update(res.pagination);
|
||||||
props.showDialog(res.msg, "Error fetching users");
|
} else {
|
||||||
return null;
|
showDialog(res.msg, "Error fetching users");
|
||||||
}
|
return null;
|
||||||
}, []);
|
}
|
||||||
|
});
|
||||||
|
}, [api, showDialog]);
|
||||||
|
|
||||||
const groupColumn = (() => {
|
const groupColumn = (() => {
|
||||||
let column = new DataColumn(L("account.groups"), "groups");
|
let column = new DataColumn(L("account.groups"), "groups");
|
||||||
@ -80,9 +87,13 @@ export default function UserListView(props) {
|
|||||||
{L("general.create_new")}
|
{L("general.create_new")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<DataTable className={"table table-striped"}
|
<DataTable
|
||||||
fetchData={onFetchUsers}
|
data={users}
|
||||||
placeholder={"No users to display"} columns={columnDefinitions} />
|
pagination={pagination}
|
||||||
|
className={"table table-striped"}
|
||||||
|
fetchData={onFetchUsers}
|
||||||
|
placeholder={"No users to display"}
|
||||||
|
columns={columnDefinitions} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,6 +49,7 @@ export default class API {
|
|||||||
let res = await response.json();
|
let res = await response.json();
|
||||||
if (!res.success && res.msg === "You are not logged in.") {
|
if (!res.success && res.msg === "You are not logged in.") {
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
|
this.user = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
@ -5,7 +5,7 @@ import React, {useCallback, useContext, useEffect, useState} from "react";
|
|||||||
import "./data-table.css";
|
import "./data-table.css";
|
||||||
import {LocaleContext} from "../locale";
|
import {LocaleContext} from "../locale";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {Box, IconButton, TextField} from "@mui/material";
|
import {Box, IconButton, Select, TextField} from "@mui/material";
|
||||||
import {formatDate, formatDateTime} from "../util";
|
import {formatDate, formatDateTime} from "../util";
|
||||||
import CachedIcon from "@material-ui/icons/Cached";
|
import CachedIcon from "@material-ui/icons/Cached";
|
||||||
|
|
||||||
@ -195,7 +195,7 @@ export class NumericColumn extends DataColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let number = super.renderData(L, entry).toString();
|
let number = super.renderData(L, entry, index).toString();
|
||||||
|
|
||||||
if (this.decimalDigits !== null) {
|
if (this.decimalDigits !== null) {
|
||||||
number = number.toFixed(this.decimalDigits);
|
number = number.toFixed(this.decimalDigits);
|
||||||
@ -223,8 +223,8 @@ export class DateTimeColumn extends DataColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let date = super.renderData(L, entry);
|
let date = super.renderData(L, entry, index);
|
||||||
return formatDateTime(L, date, this.precise);
|
return date ? formatDateTime(L, date, this.precise) : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,8 +234,8 @@ export class DateColumn extends DataColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let date = super.renderData(L, entry);
|
let date = super.renderData(L, entry, index);
|
||||||
return formatDate(L, date);
|
return date ? formatDate(L, date) : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +245,7 @@ export class BoolColumn extends DataColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let data = super.renderData(L, entry);
|
let data = super.renderData(L, entry, index);
|
||||||
return L(data ? "general.yes" : "general.no");
|
return L(data ? "general.yes" : "general.no");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -260,9 +260,17 @@ export class InputColumn extends DataColumn {
|
|||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let value = super.renderData(L, entry, index);
|
let value = super.renderData(L, entry, index);
|
||||||
|
let inputProps = typeof this.props === 'function' ? this.props(entry, index) : this.props;
|
||||||
if (this.type === 'text') {
|
if (this.type === 'text') {
|
||||||
return <TextField {...this.props} size={"small"} fullWidth={true}
|
return <TextField {...inputProps} size={"small"} fullWidth={true}
|
||||||
value={value} onChange={(e) => this.onChange(entry, index, e.target.value)} />
|
value={value} onChange={(e) => this.onChange(entry, index, e.target.value)} />
|
||||||
|
} else if (this.type === "select") {
|
||||||
|
let options = Object.entries(this.params.options || {}).map(([value, label]) =>
|
||||||
|
<option key={"option-" + value} value={value}>{label}</option>);
|
||||||
|
return <Select native {...inputProps} size={"small"} fullWidth={true} value={value}
|
||||||
|
onChange={(e) => this.onChange(entry, index, e.target.value)}>
|
||||||
|
{options}
|
||||||
|
</Select>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>[Invalid type: {this.type}]</>
|
return <>[Invalid type: {this.type}]</>
|
||||||
@ -292,15 +300,19 @@ export class ControlsColumn extends DataColumn {
|
|||||||
let props = {
|
let props = {
|
||||||
...buttonProps,
|
...buttonProps,
|
||||||
key: "button-" + index,
|
key: "button-" + index,
|
||||||
onClick: (e) => { e.stopPropagation(); button.onClick(entry, index); },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: icon button!
|
||||||
if (button.hasOwnProperty("disabled")) {
|
if (button.hasOwnProperty("disabled")) {
|
||||||
props.disabled = typeof button.disabled === 'function'
|
props.disabled = typeof button.disabled === 'function'
|
||||||
? button.disabled(entry, index)
|
? button.disabled(entry, index)
|
||||||
: button.disabled;
|
: button.disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!props.disabled) {
|
||||||
|
props.onClick = (e) => { e.stopPropagation(); button.onClick(entry, index); }
|
||||||
|
}
|
||||||
|
|
||||||
if ((!button.hasOwnProperty("hidden")) ||
|
if ((!button.hasOwnProperty("hidden")) ||
|
||||||
(typeof button.hidden === 'function' && !button.hidden(entry, index)) ||
|
(typeof button.hidden === 'function' && !button.hidden(entry, index)) ||
|
||||||
(!button.hidden)) {
|
(!button.hidden)) {
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Input, List, ListItem, TextField
|
Input, List, ListItem, Select, TextField
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
export default function Dialog(props) {
|
export default function Dialog(props) {
|
||||||
|
@ -7,7 +7,6 @@ export default function useAsyncSearch(callback, minLength = 1) {
|
|||||||
const [results, setResults] = useState(null);
|
const [results, setResults] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("searchString:", searchString);
|
|
||||||
if (minLength > 0 && (!searchString || searchString.length < minLength)) {
|
if (minLength > 0 && (!searchString || searchString.length < minLength)) {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
return;
|
return;
|
||||||
|
Loading…
Reference in New Issue
Block a user