mysql param types, async search, bugfix

This commit is contained in:
Roman 2023-01-20 12:16:18 +01:00
parent 92c78356ed
commit 424a945fa6
8 changed files with 90 additions and 36 deletions

@ -100,14 +100,14 @@ class Parameter {
return $str; return $str;
} }
public static function parseType($value): int { public static function parseType(mixed $value, bool $strict = false): int {
if (is_array($value)) if (is_array($value))
return Parameter::TYPE_ARRAY; 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; 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; 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; return Parameter::TYPE_BOOLEAN;
else if (is_a($value, 'DateTime')) else if (is_a($value, 'DateTime'))
return Parameter::TYPE_DATE_TIME; return Parameter::TYPE_DATE_TIME;

@ -91,7 +91,7 @@ class MySQL extends SQL {
private function getPreparedParams($values): array { private function getPreparedParams($values): array {
$sqlParams = array(''); $sqlParams = array('');
foreach ($values as $value) { foreach ($values as $value) {
$paramType = Parameter::parseType($value); $paramType = Parameter::parseType($value, true); // TODO: is strict type checking really correct here?
switch ($paramType) { switch ($paramType) {
case Parameter::TYPE_BOOLEAN: case Parameter::TYPE_BOOLEAN:
$value = $value ? 1 : 0; $value = $value ? 1 : 0;

@ -342,7 +342,9 @@ class DatabaseEntityHandler implements Persistable {
$value = $row[$columnName]; $value = $row[$columnName];
if ($column instanceof DateTimeColumn) { if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value); if ($value !== null) {
$value = new \DateTime($value);
}
} else if ($column instanceof JsonColumn) { } else if ($column instanceof JsonColumn) {
$value = json_decode($value, true); $value = json_decode($value, true);
} else if (isset($this->relations[$propertyName])) { } else if (isset($this->relations[$propertyName])) {

@ -138,7 +138,7 @@ class DatabaseEntityQuery extends Select {
} }
} }
public function execute(): DatabaseEntity|array|null { public function execute(): DatabaseEntity|array|null|false {
if ($this->logVerbose) { if ($this->logVerbose) {
$params = []; $params = [];
@ -148,7 +148,7 @@ class DatabaseEntityQuery extends Select {
$res = parent::execute(); $res = parent::execute();
if ($res === null || $res === false) { if ($res === null || $res === false) {
return null; return $res;
} }
if ($this->resultType === SQL::FETCH_ALL) { if ($this->resultType === SQL::FETCH_ALL) {

@ -281,7 +281,7 @@ export default class API {
/** ApiKeyAPI **/ /** ApiKeyAPI **/
async getApiKeys(showActiveOnly = false, page = 1, count = 25, orderBy = "validUntil", sortOrder = "desc") { 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() { async createApiKey() {

@ -6,7 +6,7 @@ import "./data-table.css";
import {LocaleContext} from "../locale"; import {LocaleContext} from "../locale";
import clsx from "clsx"; import clsx from "clsx";
import {Box, IconButton} from "@mui/material"; import {Box, IconButton} from "@mui/material";
import {formatDateTime} from "../util"; import {formatDate, formatDateTime} from "../util";
import CachedIcon from "@material-ui/icons/Cached"; import CachedIcon from "@material-ui/icons/Cached";
@ -23,31 +23,35 @@ export function DataTable(props) {
const [doFetchData, setFetchData] = useState(false); const [doFetchData, setFetchData] = useState(false);
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase)); const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase));
const [sortColumn, setSortColumn] = useState(defaultSortColumn || null); 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 onRowClick = onClick || (() => {});
const onFetchData = useCallback((force = false) => { const onFetchData = useCallback((force = false) => {
if (doFetchData || force) { if (fetchData) {
setFetchData(false); if (doFetchData || force) {
const orderBy = columns[sortColumn]?.field || null; setFetchData(false);
const sortOrder = sortAscending ? "asc" : "desc"; const orderBy = columns[sortColumn]?.field || null;
fetchData(pagination.getPage(), pagination.getPageSize(), orderBy, sortOrder); 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? // pagination changed?
useEffect(() => { useEffect(() => {
let forceFetch = false; if (pagination) {
if (pagination.getPageSize() < pagination.getTotal()) { let forceFetch = false;
// page size is smaller than the total count if (pagination.getPageSize() < pagination.getTotal()) {
forceFetch = true; // page size is smaller than the total count
} else if (data?.length && pagination.getPageSize() >= data.length && data.length < pagination.getTotal()) { forceFetch = true;
// page size is greater than the current visible count but there were hidden rows before } else if (data?.length && pagination.getPageSize() >= data.length && data.length < pagination.getTotal()) {
forceFetch = true; // page size is greater than the current visible count but there were hidden rows before
} forceFetch = true;
}
onFetchData(forceFetch); onFetchData(forceFetch);
}, [pagination.data.pageSize, pagination.data.current]); }
}, [pagination?.data?.pageSize, pagination?.data?.current]);
// sorting changed // sorting changed
useEffect(() => { useEffect(() => {
@ -112,12 +116,17 @@ export function DataTable(props) {
} }
return <Box position={"relative"}> return <Box position={"relative"}>
<h3> {title ?
<IconButton onClick={() => onFetchData(true)}> <h3>
<CachedIcon/> {fetchData ?
</IconButton> <IconButton onClick={() => onFetchData(true)}>
{title} <CachedIcon/>
</h3> </IconButton>
: <></>
}
{title}
</h3> : <></>
}
<Table className={clsx("data-table", className)} size="small" {...other}> <Table className={clsx("data-table", className)} size="small" {...other}>
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -128,7 +137,7 @@ export function DataTable(props) {
{ rows } { rows }
</TableBody> </TableBody>
</Table> </Table>
{pagination.renderPagination(L, numRows)} {pagination && pagination.renderPagination(L, numRows)}
</Box> </Box>
} }
@ -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 { export class BoolColumn extends DataColumn {
constructor(label, field = null, params = {}) { constructor(label, field = null, params = {}) {
super(label, field, params); super(label, field, params);

@ -1,4 +1,4 @@
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import { import {
Box, Box,
Button, Button,
@ -20,11 +20,21 @@ export default function Dialog(props) {
const [inputData, setInputData] = useState({}); 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 = []; let buttons = [];
for (const [index, name] of options.entries()) { for (const [index, name] of options.entries()) {
buttons.push( buttons.push(
<Button variant={"outlined"} size={"small"} key={"button-" + name} <Button variant={"outlined"} size={"small"} key={"button-" + name}
onClick={() => { onClose(); onOption(index, inputData); }}> onClick={() => { onClose(); onOption(index, inputData); setInputData({}); }}>
{name} {name}
</Button> </Button>
) )
@ -40,6 +50,7 @@ export default function Dialog(props) {
case 'text': case 'text':
inputElements.push(<TextField inputElements.push(<TextField
{...inputProps} {...inputProps}
sx={{marginTop: 1}}
size={"small"} fullWidth={true} size={"small"} fullWidth={true}
key={"input-" + input.name} key={"input-" + input.name}
value={inputData[input.name] || ""} value={inputData[input.name] || ""}

@ -0,0 +1,21 @@
import {useEffect, useState} from "react";
export default function useAsyncSearch(callback, minLength = 1) {
const [searchString, setSearchString] = useState("");
const [results, setResults] = useState(null);
useEffect(() => {
if (!searchString || searchString.length < minLength) {
setResults([]);
return;
}
callback(searchString).then(results => {
setResults(results || null);
});
}, [searchString]);
return [searchString, setSearchString, results];
}