mysql param types, async search, bugfix
This commit is contained in:
parent
92c78356ed
commit
424a945fa6
@ -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] || ""}
|
||||||
|
21
react/shared/hooks/async-search.js
Normal file
21
react/shared/hooks/async-search.js
Normal file
@ -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];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user