From 424a945fa6a16ddb1713c2907a516b16867c4f29 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 20 Jan 2023 12:16:18 +0100 Subject: [PATCH] mysql param types, async search, bugfix --- Core/API/Parameter/Parameter.class.php | 8 +-- Core/Driver/SQL/MySQL.class.php | 2 +- .../Controller/DatabaseEntityHandler.php | 4 +- .../Controller/DatabaseEntityQuery.class.php | 4 +- react/shared/api.js | 2 +- react/shared/elements/data-table.js | 70 ++++++++++++------- react/shared/elements/dialog.jsx | 15 +++- react/shared/hooks/async-search.js | 21 ++++++ 8 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 react/shared/hooks/async-search.js diff --git a/Core/API/Parameter/Parameter.class.php b/Core/API/Parameter/Parameter.class.php index a85e93a..3b1afe7 100644 --- a/Core/API/Parameter/Parameter.class.php +++ b/Core/API/Parameter/Parameter.class.php @@ -100,14 +100,14 @@ class Parameter { return $str; } - public static function parseType($value): int { + public static function parseType(mixed $value, bool $strict = false): int { if (is_array($value)) return Parameter::TYPE_ARRAY; - else if (is_numeric($value) && intval($value) == $value) + else if (is_int($value) || (!$strict && is_numeric($value) && intval($value) == $value)) return Parameter::TYPE_INT; - else if (is_float($value) || (is_numeric($value) && floatval($value) == $value)) + else if (is_float($value) || (!$strict && is_numeric($value) && floatval($value) == $value)) return Parameter::TYPE_FLOAT; - else if (is_bool($value) || $value == "true" || $value == "false") + else if (is_bool($value) || (!$strict && ($value == "true" || $value == "false"))) return Parameter::TYPE_BOOLEAN; else if (is_a($value, 'DateTime')) return Parameter::TYPE_DATE_TIME; diff --git a/Core/Driver/SQL/MySQL.class.php b/Core/Driver/SQL/MySQL.class.php index 0ffa581..ba9fd71 100644 --- a/Core/Driver/SQL/MySQL.class.php +++ b/Core/Driver/SQL/MySQL.class.php @@ -91,7 +91,7 @@ class MySQL extends SQL { private function getPreparedParams($values): array { $sqlParams = array(''); foreach ($values as $value) { - $paramType = Parameter::parseType($value); + $paramType = Parameter::parseType($value, true); // TODO: is strict type checking really correct here? switch ($paramType) { case Parameter::TYPE_BOOLEAN: $value = $value ? 1 : 0; diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php index 8b4532f..9f31b92 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php @@ -342,7 +342,9 @@ class DatabaseEntityHandler implements Persistable { $value = $row[$columnName]; if ($column instanceof DateTimeColumn) { - $value = new \DateTime($value); + if ($value !== null) { + $value = new \DateTime($value); + } } else if ($column instanceof JsonColumn) { $value = json_decode($value, true); } else if (isset($this->relations[$propertyName])) { diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php index 051d8ed..f8b50ec 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php @@ -138,7 +138,7 @@ class DatabaseEntityQuery extends Select { } } - public function execute(): DatabaseEntity|array|null { + public function execute(): DatabaseEntity|array|null|false { if ($this->logVerbose) { $params = []; @@ -148,7 +148,7 @@ class DatabaseEntityQuery extends Select { $res = parent::execute(); if ($res === null || $res === false) { - return null; + return $res; } if ($this->resultType === SQL::FETCH_ALL) { diff --git a/react/shared/api.js b/react/shared/api.js index 0088234..2e75552 100644 --- a/react/shared/api.js +++ b/react/shared/api.js @@ -281,7 +281,7 @@ export default class API { /** ApiKeyAPI **/ async getApiKeys(showActiveOnly = false, page = 1, count = 25, orderBy = "validUntil", sortOrder = "desc") { - return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly, pageNum: page, count: count, orderBy: orderBy, sortOrder: sortOrder }); + return this.apiCall("apiKey/fetch", { showActiveOnly: showActiveOnly, page: page, count: count, orderBy: orderBy, sortOrder: sortOrder }); } async createApiKey() { diff --git a/react/shared/elements/data-table.js b/react/shared/elements/data-table.js index ca52b5f..59673ef 100644 --- a/react/shared/elements/data-table.js +++ b/react/shared/elements/data-table.js @@ -6,7 +6,7 @@ import "./data-table.css"; import {LocaleContext} from "../locale"; import clsx from "clsx"; import {Box, IconButton} from "@mui/material"; -import {formatDateTime} from "../util"; +import {formatDate, formatDateTime} from "../util"; import CachedIcon from "@material-ui/icons/Cached"; @@ -23,31 +23,35 @@ export function DataTable(props) { const [doFetchData, setFetchData] = useState(false); const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase)); const [sortColumn, setSortColumn] = useState(defaultSortColumn || null); - const sortable = props.hasOwnProperty("sortable") ? !!props.sortable : true; + const sortable = !!fetchData && (props.hasOwnProperty("sortable") ? !!props.sortable : true); const onRowClick = onClick || (() => {}); const onFetchData = useCallback((force = false) => { - if (doFetchData || force) { - setFetchData(false); - const orderBy = columns[sortColumn]?.field || null; - const sortOrder = sortAscending ? "asc" : "desc"; - fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder); + if (fetchData) { + if (doFetchData || force) { + setFetchData(false); + const orderBy = columns[sortColumn]?.field || null; + const sortOrder = sortAscending ? "asc" : "desc"; + fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder); + } } - }, [doFetchData, columns, sortColumn, sortAscending, pagination]); + }, [fetchData, doFetchData, columns, sortColumn, sortAscending, pagination]); // pagination changed? useEffect(() => { - let forceFetch = false; - if (pagination.getPageSize() < pagination.getTotal()) { - // page size is smaller than the total count - forceFetch = true; - } else if (data?.length && pagination.getPageSize() >= data.length && data.length < pagination.getTotal()) { - // page size is greater than the current visible count but there were hidden rows before - forceFetch = true; - } + if (pagination) { + let forceFetch = false; + if (pagination.getPageSize() < pagination.getTotal()) { + // page size is smaller than the total count + forceFetch = true; + } else if (data?.length && pagination.getPageSize() >= data.length && data.length < pagination.getTotal()) { + // page size is greater than the current visible count but there were hidden rows before + forceFetch = true; + } - onFetchData(forceFetch); - }, [pagination.data.pageSize, pagination.data.current]); + onFetchData(forceFetch); + } + }, [pagination?.data?.pageSize, pagination?.data?.current]); // sorting changed useEffect(() => { @@ -112,12 +116,17 @@ export function DataTable(props) { } return -

- onFetchData(true)}> - - - {title} -

+ {title ? +

+ {fetchData ? + onFetchData(true)}> + + + : <> + } + {title} +

: <> + } @@ -128,7 +137,7 @@ export function DataTable(props) { { rows }
- {pagination.renderPagination(L, numRows)} + {pagination && pagination.renderPagination(L, numRows)}
} @@ -210,6 +219,17 @@ export class DateTimeColumn extends DataColumn { } } +export class DateColumn extends DataColumn { + constructor(label, field = null, params = {}) { + super(label, field, params); + } + + renderData(L, entry, index) { + let date = super.renderData(L, entry); + return formatDate(L, date); + } +} + export class BoolColumn extends DataColumn { constructor(label, field = null, params = {}) { super(label, field, params); diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx index c126617..4144210 100644 --- a/react/shared/elements/dialog.jsx +++ b/react/shared/elements/dialog.jsx @@ -1,4 +1,4 @@ -import React, {useState} from "react"; +import React, {useEffect, useState} from "react"; import { Box, Button, @@ -20,11 +20,21 @@ export default function Dialog(props) { const [inputData, setInputData] = useState({}); + useEffect(() => { + if (props.inputs) { + let initialData = {}; + for (const input of props.inputs) { + initialData[input.name] = input.value || ""; + } + setInputData(initialData); + } + }, [props.inputs]); + let buttons = []; for (const [index, name] of options.entries()) { buttons.push( ) @@ -40,6 +50,7 @@ export default function Dialog(props) { case 'text': inputElements.push( { + if (!searchString || searchString.length < minLength) { + setResults([]); + return; + } + + callback(searchString).then(results => { + setResults(results || null); + }); + }, [searchString]); + + return [searchString, setSearchString, results]; +} \ No newline at end of file