diff --git a/Core/API/Traits/Pagination.trait.php b/Core/API/Traits/Pagination.trait.php
index 63985f7..4aa7e48 100644
--- a/Core/API/Traits/Pagination.trait.php
+++ b/Core/API/Traits/Pagination.trait.php
@@ -18,7 +18,7 @@ trait Pagination {
return [
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 25),
- 'orderBy' => new StringType('orderBy', -1, true, $defaultOrderBy, $orderColumns),
+ 'orderBy' => new StringType('orderBy', -1, true, $defaultOrderBy, array_values($orderColumns)),
'sortOrder' => new StringType('sortOrder', -1, true, $defaultSortOrder, ['asc', 'desc']),
];
}
@@ -36,7 +36,7 @@ trait Pagination {
}
$pageCount = intval(ceil($this->entityCount / $this->pageSize));
- $this->page = min($this->page, $pageCount); // number of pages changed due to pageSize / filter
+ $this->page = max(1, min($this->page, $pageCount)); // number of pages changed due to pageSize / filter
$this->result["pagination"] = [
"current" => $this->page,
@@ -91,18 +91,22 @@ trait Pagination {
}
if ($orderBy) {
- $handler = $baseQuery->getHandler();
- $baseTable = $handler->getTableName();
- $sortColumn = DatabaseEntityHandler::buildColumnName($orderBy);
- $fullyQualifiedColumn = "$baseTable.$sortColumn";
- $selectedColumns = $baseQuery->getSelectValues();
-
- if (in_array($sortColumn, $selectedColumns)) {
+ $sortColumn = array_search($orderBy, $this->paginationOrderColumns);
+ if (is_string($sortColumn)) {
$entityQuery->orderBy($sortColumn);
- } else if (in_array($fullyQualifiedColumn, $selectedColumns)) {
- $entityQuery->orderBy($fullyQualifiedColumn);
} else {
- $entityQuery->orderBy($orderBy);
+ $handler = $baseQuery->getHandler();
+ $baseTable = $handler->getTableName();
+ $sortColumn = DatabaseEntityHandler::buildColumnName($orderBy);
+ $fullyQualifiedColumn = "$baseTable.$sortColumn";
+ $selectedColumns = $baseQuery->getSelectValues();
+ if (in_array($sortColumn, $selectedColumns)) {
+ $entityQuery->orderBy($sortColumn);
+ } else if (in_array($fullyQualifiedColumn, $selectedColumns)) {
+ $entityQuery->orderBy($fullyQualifiedColumn);
+ } else {
+ $entityQuery->orderBy($orderBy);
+ }
}
}
diff --git a/Core/Driver/SQL/Expression/AbstractFunction.class.php b/Core/Driver/SQL/Expression/AbstractFunction.class.php
new file mode 100644
index 0000000..d1ef8fe
--- /dev/null
+++ b/Core/Driver/SQL/Expression/AbstractFunction.class.php
@@ -0,0 +1,28 @@
+functionName = $functionName;
+ $this->value = $value;
+ }
+
+ public function getExpression(SQL $sql, array &$params): string {
+ return $this->functionName . "(" . $sql->addValue($this->getValue(), $params) . ")";
+ }
+
+ public function getFunctionName(): string {
+ return $this->functionName;
+ }
+
+ public function getValue(): mixed {
+ return $this->value;
+ }
+}
\ No newline at end of file
diff --git a/Core/Driver/SQL/Expression/Distinct.class.php b/Core/Driver/SQL/Expression/Distinct.class.php
index 31fa827..fc59dd3 100644
--- a/Core/Driver/SQL/Expression/Distinct.class.php
+++ b/Core/Driver/SQL/Expression/Distinct.class.php
@@ -2,21 +2,10 @@
namespace Core\Driver\SQL\Expression;
-use Core\Driver\SQL\SQL;
-
-class Distinct extends Expression {
-
- private mixed $value;
+class Distinct extends AbstractFunction {
public function __construct(mixed $value) {
- $this->value = $value;
+ parent::__construct("DISTINCT", $value);
}
- public function getValue(): mixed {
- return $this->value;
- }
-
- function getExpression(SQL $sql, array &$params): string {
- return "DISTINCT(" . $sql->addValue($this->getValue(), $params) . ")";
- }
}
\ No newline at end of file
diff --git a/Core/Driver/SQL/Expression/Lower.class.php b/Core/Driver/SQL/Expression/Lower.class.php
new file mode 100644
index 0000000..0a0ab21
--- /dev/null
+++ b/Core/Driver/SQL/Expression/Lower.class.php
@@ -0,0 +1,11 @@
+addValue($this->getValue(), $params) . ")";
- }
}
\ No newline at end of file
diff --git a/Core/Driver/SQL/Expression/Upper.class.php b/Core/Driver/SQL/Expression/Upper.class.php
new file mode 100644
index 0000000..ca76341
--- /dev/null
+++ b/Core/Driver/SQL/Expression/Upper.class.php
@@ -0,0 +1,11 @@
+ "Umbenennen",
"remove" => "Entfernen",
"change" => "Bearbeiten",
+ "close" => "Schließen",
"reset" => "Zurücksetzen",
"move" => "Verschieben",
"delete" => "Löschen",
diff --git a/Core/Localization/en_US/general.php b/Core/Localization/en_US/general.php
index ce24620..e52d7e8 100644
--- a/Core/Localization/en_US/general.php
+++ b/Core/Localization/en_US/general.php
@@ -27,6 +27,7 @@ return [
"request" => "Request",
"cancel" => "Cancel",
"confirm" => "Confirm",
+ "close" => "Close",
"ok" => "OK",
"remove" => "Remove",
"change" => "Change",
diff --git a/Core/Objects/DatabaseEntity/User.class.php b/Core/Objects/DatabaseEntity/User.class.php
index 1a162bf..37899a3 100644
--- a/Core/Objects/DatabaseEntity/User.class.php
+++ b/Core/Objects/DatabaseEntity/User.class.php
@@ -146,12 +146,12 @@ class User extends DatabaseEntity {
return !empty($this->fullName) ? $this->fullName : $this->name;
}
- public static function buildSQLDisplayName(SQL $sql, string $joinColumn): Alias {
+ public static function buildSQLDisplayName(SQL $sql, string $joinColumn, string $alias = "user"): Alias {
return new Alias(
$sql->select(new Coalesce(
new NullIf(new Column("User.full_name"), ""),
new NullIf(new Column("User.name"), ""))
)->from("User")->whereEq("User.id", new Column($joinColumn)),
- "user");
+ $alias);
}
}
\ No newline at end of file
diff --git a/react/shared/api.js b/react/shared/api.js
index 8bac1ab..0088234 100644
--- a/react/shared/api.js
+++ b/react/shared/api.js
@@ -1,5 +1,9 @@
import {USER_GROUP_ADMIN} from "./constants";
-import {isInt} from "./util";
+import {createDownload, isInt} from "./util";
+
+Date.prototype.toJSON = function() {
+ return Math.round(this.getTime() / 1000);
+};
export default class API {
constructor() {
@@ -14,7 +18,7 @@ export default class API {
return this.loggedIn ? this.session.csrfToken : null;
}
- async apiCall(method, params) {
+ async apiCall(method, params, expectBinary=false) {
params = params || { };
const csrfToken = this.csrfToken();
const config = {method: 'post'};
@@ -32,6 +36,16 @@ export default class API {
}
let response = await fetch("/api/" + method, config);
+ if (response.headers.has("content-disposition")) {
+ let contentDisposition = response.headers.get("content-disposition");
+ if (contentDisposition.toLowerCase().startsWith("attachment;")) {
+ let fileName = /filename="?([^"]*)"?/;
+ let blob = await response.blob();
+ createDownload(fileName.exec(contentDisposition)[1], blob);
+ return { success: true, msg: "" };
+ }
+ }
+
let res = await response.json();
if (!res.success && res.msg === "You are not logged in.") {
this.loggedIn = false;
@@ -56,7 +70,6 @@ export default class API {
return false;
}
-
hasGroup(groupIdOrName) {
if (this.loggedIn && this.user?.groups) {
if (isInt(groupIdOrName)) {
diff --git a/react/shared/elements/data-table.css b/react/shared/elements/data-table.css
index f1fcf46..44fb47e 100644
--- a/react/shared/elements/data-table.css
+++ b/react/shared/elements/data-table.css
@@ -24,4 +24,13 @@
.data-table-clickable {
cursor: pointer;
+}
+
+.pagination-controls {
+ margin-top: 6px;
+}
+
+.pagination-page-size > div {
+ padding-top: 5px;
+ padding-bottom: 5px;
}
\ No newline at end of file
diff --git a/react/shared/elements/data-table.js b/react/shared/elements/data-table.js
index 336b21f..ca52b5f 100644
--- a/react/shared/elements/data-table.js
+++ b/react/shared/elements/data-table.js
@@ -85,8 +85,10 @@ export function DataTable(props) {
}
const numColumns = columns.length;
+ let numRows = 0;
let rows = [];
if (data && data?.length) {
+ numRows = data.length;
for (const [rowIndex, entry] of data.entries()) {
let row = [];
for (const [index, column] of columns.entries()) {
@@ -96,14 +98,14 @@ export function DataTable(props) {
}
rows.push( onRowClick(rowIndex, entry)}
+ onClick={(e) => ["tr","td"].includes(e.target.tagName.toLowerCase()) && onRowClick(rowIndex, entry)}
key={"row-" + rowIndex}>
{ row }
);
}
} else if (placeholder) {
rows.push(
-
+
{ placeholder }
);
@@ -126,7 +128,7 @@ export function DataTable(props) {
{ rows }
- {pagination.renderPagination(L, rows.length)}
+ {pagination.renderPagination(L, numRows)}
}
@@ -140,7 +142,7 @@ export class DataColumn {
}
renderData(L, entry, index) {
- return entry[this.field]
+ return typeof this.field === 'function' ? this.field(entry) : entry[this.field];
}
renderHead() {
@@ -228,14 +230,33 @@ export class ControlsColumn extends DataColumn {
renderData(L, entry, index) {
let buttonElements = [];
for (const [index, button] of this.buttons.entries()) {
- let element = button.element;
- let props = {
- key: "button-" + index,
- onClick: (() => button.onClick(entry)),
- className: "data-table-clickable"
+ let element = typeof button.element === 'function'
+ ? button.element(entry, index)
+ : button.element;
+
+ let buttonProps = {};
+ if (typeof button.props === 'function') {
+ buttonProps = button.props(entry, index);
+ } else {
+ buttonProps = button.props;
}
- if (typeof button.showIf !== 'function' || button.showIf(entry)) {
+ let props = {
+ ...buttonProps,
+ key: "button-" + index,
+ onClick: (e) => { e.stopPropagation(); button.onClick(entry, index); },
+ className: "data-table-clickable",
+ }
+
+ if (button.hasOwnProperty("disabled")) {
+ props.disabled = typeof button.disabled === 'function'
+ ? button.disabled(entry, index)
+ : button.disabled;
+ }
+
+ if ((!button.hasOwnProperty("hidden")) ||
+ (typeof button.hidden === 'function' && !button.hidden(entry, index)) ||
+ (!button.hidden)) {
buttonElements.push(React.createElement(element, props))
}
}
diff --git a/react/shared/elements/dialog.jsx b/react/shared/elements/dialog.jsx
index 6fe48fa..c126617 100644
--- a/react/shared/elements/dialog.jsx
+++ b/react/shared/elements/dialog.jsx
@@ -1,8 +1,14 @@
-import React, {useContext} from "react";
-import {Dialog as MuiDialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material";
-import {Button} from "@material-ui/core";
-import {LocaleContext} from "../locale";
-import "./dialog.css";
+import React, {useState} from "react";
+import {
+ Box,
+ Button,
+ Dialog as MuiDialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ Input, TextField
+} from "@mui/material";
export default function Dialog(props) {
@@ -10,37 +16,55 @@ export default function Dialog(props) {
const onClose = props.onClose || function() { };
const onOption = props.onOption || function() { };
const options = props.options || ["Close"];
- const type = props.type || "default";
- const {translate: L} = useContext(LocaleContext);
+ const inputs = props.inputs || [];
+ const [inputData, setInputData] = useState({});
let buttons = [];
- for (let name of options) {
- let type = "default";
- if (name === "Yes") type = "warning";
- else if(name === "No") type = "danger";
-
+ for (const [index, name] of options.entries()) {
buttons.push(
-