pagination + sql expressions + frontend improvements
This commit is contained in:
parent
878cd62bbe
commit
92c78356ed
@ -18,7 +18,7 @@ trait Pagination {
|
|||||||
return [
|
return [
|
||||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 25),
|
'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']),
|
'sortOrder' => new StringType('sortOrder', -1, true, $defaultSortOrder, ['asc', 'desc']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ trait Pagination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pageCount = intval(ceil($this->entityCount / $this->pageSize));
|
$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"] = [
|
$this->result["pagination"] = [
|
||||||
"current" => $this->page,
|
"current" => $this->page,
|
||||||
@ -91,18 +91,22 @@ trait Pagination {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($orderBy) {
|
if ($orderBy) {
|
||||||
$handler = $baseQuery->getHandler();
|
$sortColumn = array_search($orderBy, $this->paginationOrderColumns);
|
||||||
$baseTable = $handler->getTableName();
|
if (is_string($sortColumn)) {
|
||||||
$sortColumn = DatabaseEntityHandler::buildColumnName($orderBy);
|
|
||||||
$fullyQualifiedColumn = "$baseTable.$sortColumn";
|
|
||||||
$selectedColumns = $baseQuery->getSelectValues();
|
|
||||||
|
|
||||||
if (in_array($sortColumn, $selectedColumns)) {
|
|
||||||
$entityQuery->orderBy($sortColumn);
|
$entityQuery->orderBy($sortColumn);
|
||||||
} else if (in_array($fullyQualifiedColumn, $selectedColumns)) {
|
|
||||||
$entityQuery->orderBy($fullyQualifiedColumn);
|
|
||||||
} else {
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
Core/Driver/SQL/Expression/AbstractFunction.class.php
Normal file
28
Core/Driver/SQL/Expression/AbstractFunction.class.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
|
use Core\Driver\SQL\SQL;
|
||||||
|
|
||||||
|
abstract class AbstractFunction extends Expression {
|
||||||
|
|
||||||
|
private string $functionName;
|
||||||
|
private mixed $value;
|
||||||
|
|
||||||
|
public function __construct(string $functionName, mixed $value) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,21 +2,10 @@
|
|||||||
|
|
||||||
namespace Core\Driver\SQL\Expression;
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
use Core\Driver\SQL\SQL;
|
class Distinct extends AbstractFunction {
|
||||||
|
|
||||||
class Distinct extends Expression {
|
|
||||||
|
|
||||||
private mixed $value;
|
|
||||||
|
|
||||||
public function __construct(mixed $value) {
|
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) . ")";
|
|
||||||
}
|
|
||||||
}
|
}
|
11
Core/Driver/SQL/Expression/Lower.class.php
Normal file
11
Core/Driver/SQL/Expression/Lower.class.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
|
class Lower extends AbstractFunction {
|
||||||
|
|
||||||
|
public function __construct(mixed $value) {
|
||||||
|
parent::__construct("LOWER", $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,15 +2,10 @@
|
|||||||
|
|
||||||
namespace Core\Driver\SQL\Expression;
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
use Core\Driver\SQL\SQL;
|
class Sum extends AbstractFunction {
|
||||||
|
|
||||||
class Sum extends Alias {
|
public function __construct(mixed $value) {
|
||||||
|
parent::__construct("SUM", $value);
|
||||||
public function __construct(mixed $value, string $alias) {
|
|
||||||
parent::__construct($value, $alias);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function addValue(SQL $sql, array &$params): string {
|
|
||||||
return "SUM(" . $sql->addValue($this->getValue(), $params) . ")";
|
|
||||||
}
|
|
||||||
}
|
}
|
11
Core/Driver/SQL/Expression/Upper.class.php
Normal file
11
Core/Driver/SQL/Expression/Upper.class.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Core\Driver\SQL\Expression;
|
||||||
|
|
||||||
|
class Upper extends AbstractFunction {
|
||||||
|
|
||||||
|
public function __construct(mixed $value) {
|
||||||
|
parent::__construct("UPPER", $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -35,6 +35,7 @@ return [
|
|||||||
"rename" => "Umbenennen",
|
"rename" => "Umbenennen",
|
||||||
"remove" => "Entfernen",
|
"remove" => "Entfernen",
|
||||||
"change" => "Bearbeiten",
|
"change" => "Bearbeiten",
|
||||||
|
"close" => "Schließen",
|
||||||
"reset" => "Zurücksetzen",
|
"reset" => "Zurücksetzen",
|
||||||
"move" => "Verschieben",
|
"move" => "Verschieben",
|
||||||
"delete" => "Löschen",
|
"delete" => "Löschen",
|
||||||
|
@ -27,6 +27,7 @@ return [
|
|||||||
"request" => "Request",
|
"request" => "Request",
|
||||||
"cancel" => "Cancel",
|
"cancel" => "Cancel",
|
||||||
"confirm" => "Confirm",
|
"confirm" => "Confirm",
|
||||||
|
"close" => "Close",
|
||||||
"ok" => "OK",
|
"ok" => "OK",
|
||||||
"remove" => "Remove",
|
"remove" => "Remove",
|
||||||
"change" => "Change",
|
"change" => "Change",
|
||||||
|
@ -146,12 +146,12 @@ class User extends DatabaseEntity {
|
|||||||
return !empty($this->fullName) ? $this->fullName : $this->name;
|
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(
|
return new Alias(
|
||||||
$sql->select(new Coalesce(
|
$sql->select(new Coalesce(
|
||||||
new NullIf(new Column("User.full_name"), ""),
|
new NullIf(new Column("User.full_name"), ""),
|
||||||
new NullIf(new Column("User.name"), ""))
|
new NullIf(new Column("User.name"), ""))
|
||||||
)->from("User")->whereEq("User.id", new Column($joinColumn)),
|
)->from("User")->whereEq("User.id", new Column($joinColumn)),
|
||||||
"user");
|
$alias);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,9 @@
|
|||||||
import {USER_GROUP_ADMIN} from "./constants";
|
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 {
|
export default class API {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -14,7 +18,7 @@ export default class API {
|
|||||||
return this.loggedIn ? this.session.csrfToken : null;
|
return this.loggedIn ? this.session.csrfToken : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async apiCall(method, params) {
|
async apiCall(method, params, expectBinary=false) {
|
||||||
params = params || { };
|
params = params || { };
|
||||||
const csrfToken = this.csrfToken();
|
const csrfToken = this.csrfToken();
|
||||||
const config = {method: 'post'};
|
const config = {method: 'post'};
|
||||||
@ -32,6 +36,16 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let response = await fetch("/api/" + method, config);
|
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();
|
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;
|
||||||
@ -56,7 +70,6 @@ export default class API {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
hasGroup(groupIdOrName) {
|
hasGroup(groupIdOrName) {
|
||||||
if (this.loggedIn && this.user?.groups) {
|
if (this.loggedIn && this.user?.groups) {
|
||||||
if (isInt(groupIdOrName)) {
|
if (isInt(groupIdOrName)) {
|
||||||
|
@ -25,3 +25,12 @@
|
|||||||
.data-table-clickable {
|
.data-table-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-page-size > div {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
@ -85,8 +85,10 @@ export function DataTable(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const numColumns = columns.length;
|
const numColumns = columns.length;
|
||||||
|
let numRows = 0;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
if (data && data?.length) {
|
if (data && data?.length) {
|
||||||
|
numRows = data.length;
|
||||||
for (const [rowIndex, entry] of data.entries()) {
|
for (const [rowIndex, entry] of data.entries()) {
|
||||||
let row = [];
|
let row = [];
|
||||||
for (const [index, column] of columns.entries()) {
|
for (const [index, column] of columns.entries()) {
|
||||||
@ -96,14 +98,14 @@ export function DataTable(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows.push(<TableRow className={clsx({["data-table-clickable"]: typeof onClick === 'function'})}
|
rows.push(<TableRow className={clsx({["data-table-clickable"]: typeof onClick === 'function'})}
|
||||||
onClick={() => onRowClick(rowIndex, entry)}
|
onClick={(e) => ["tr","td"].includes(e.target.tagName.toLowerCase()) && onRowClick(rowIndex, entry)}
|
||||||
key={"row-" + rowIndex}>
|
key={"row-" + rowIndex}>
|
||||||
{ row }
|
{ row }
|
||||||
</TableRow>);
|
</TableRow>);
|
||||||
}
|
}
|
||||||
} else if (placeholder) {
|
} else if (placeholder) {
|
||||||
rows.push(<TableRow key={"row-placeholder"}>
|
rows.push(<TableRow key={"row-placeholder"}>
|
||||||
<TableCell colSpan={numColumns}>
|
<TableCell colSpan={numColumns} align={"center"}>
|
||||||
{ placeholder }
|
{ placeholder }
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>);
|
</TableRow>);
|
||||||
@ -126,7 +128,7 @@ export function DataTable(props) {
|
|||||||
{ rows }
|
{ rows }
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
{pagination.renderPagination(L, rows.length)}
|
{pagination.renderPagination(L, numRows)}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +142,7 @@ export class DataColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
return entry[this.field]
|
return typeof this.field === 'function' ? this.field(entry) : entry[this.field];
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHead() {
|
renderHead() {
|
||||||
@ -228,14 +230,33 @@ export class ControlsColumn extends DataColumn {
|
|||||||
renderData(L, entry, index) {
|
renderData(L, entry, index) {
|
||||||
let buttonElements = [];
|
let buttonElements = [];
|
||||||
for (const [index, button] of this.buttons.entries()) {
|
for (const [index, button] of this.buttons.entries()) {
|
||||||
let element = button.element;
|
let element = typeof button.element === 'function'
|
||||||
let props = {
|
? button.element(entry, index)
|
||||||
key: "button-" + index,
|
: button.element;
|
||||||
onClick: (() => button.onClick(entry)),
|
|
||||||
className: "data-table-clickable"
|
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))
|
buttonElements.push(React.createElement(element, props))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import React, {useContext} from "react";
|
import React, {useState} from "react";
|
||||||
import {Dialog as MuiDialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material";
|
import {
|
||||||
import {Button} from "@material-ui/core";
|
Box,
|
||||||
import {LocaleContext} from "../locale";
|
Button,
|
||||||
import "./dialog.css";
|
Dialog as MuiDialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Input, TextField
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
export default function Dialog(props) {
|
export default function Dialog(props) {
|
||||||
|
|
||||||
@ -10,37 +16,55 @@ export default function Dialog(props) {
|
|||||||
const onClose = props.onClose || function() { };
|
const onClose = props.onClose || function() { };
|
||||||
const onOption = props.onOption || function() { };
|
const onOption = props.onOption || function() { };
|
||||||
const options = props.options || ["Close"];
|
const options = props.options || ["Close"];
|
||||||
const type = props.type || "default";
|
const inputs = props.inputs || [];
|
||||||
const {translate: L} = useContext(LocaleContext);
|
|
||||||
|
|
||||||
|
const [inputData, setInputData] = useState({});
|
||||||
|
|
||||||
let buttons = [];
|
let buttons = [];
|
||||||
for (let name of options) {
|
for (const [index, name] of options.entries()) {
|
||||||
let type = "default";
|
|
||||||
if (name === "Yes") type = "warning";
|
|
||||||
else if(name === "No") type = "danger";
|
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Button variant={"outlined"} size={"small"} type="button" key={"button-" + name}
|
<Button variant={"outlined"} size={"small"} key={"button-" + name}
|
||||||
data-dismiss={"modal"} onClick={() => { onClose(); onOption(name); }}>
|
onClick={() => { onClose(); onOption(index, inputData); }}>
|
||||||
{name}
|
{name}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let inputElements = [];
|
||||||
|
for (const input of inputs) {
|
||||||
|
let inputProps = { ...input };
|
||||||
|
delete inputProps.name;
|
||||||
|
delete inputProps.type;
|
||||||
|
|
||||||
|
switch (input.type) {
|
||||||
|
case 'text':
|
||||||
|
inputElements.push(<TextField
|
||||||
|
{...inputProps}
|
||||||
|
size={"small"} fullWidth={true}
|
||||||
|
key={"input-" + input.name}
|
||||||
|
value={inputData[input.name] || ""}
|
||||||
|
onChange={e => setInputData({ ...inputData, [input.name]: e.target.value })}
|
||||||
|
/>)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <MuiDialog
|
return <MuiDialog
|
||||||
open={show}
|
open={show}
|
||||||
onClose={onClose}
|
onClose={onClose}>
|
||||||
aria-labelledby="alert-dialog-title"
|
<DialogTitle>
|
||||||
aria-describedby="alert-dialog-description">
|
{ props.title }
|
||||||
<DialogTitle>{ props.title }</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{ props.message }
|
{ props.message }
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
<Box mt={2}>
|
||||||
|
{ inputElements }
|
||||||
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{buttons}
|
{ buttons }
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MuiDialog>
|
</MuiDialog>
|
||||||
}
|
}
|
21
react/shared/hooks/current-path.js
Normal file
21
react/shared/hooks/current-path.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {useLocation, useParams} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
export default function useCurrentPath() {
|
||||||
|
const location = useLocation();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const { pathname } = location;
|
||||||
|
|
||||||
|
if (!Object.keys(params).length) {
|
||||||
|
return pathname; // we don't need to replace anything
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = pathname;
|
||||||
|
Object.entries(params).forEach(([paramName, paramValue]) => {
|
||||||
|
if (paramValue) {
|
||||||
|
path = path.replace(paramValue, `:${paramName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return path;
|
||||||
|
}
|
@ -56,17 +56,23 @@ class Pagination {
|
|||||||
renderPagination(L, numEntries, options = null) {
|
renderPagination(L, numEntries, options = null) {
|
||||||
options = options || [10, 25, 50, 100];
|
options = options || [10, 25, 50, 100];
|
||||||
|
|
||||||
return <Box>
|
return <Box display={"grid"} gridTemplateColumns={"75px auto"} className={"pagination-controls"}>
|
||||||
<Select
|
<Select
|
||||||
value={this.data.pageSize}
|
value={this.data.pageSize}
|
||||||
|
className={"pagination-page-size"}
|
||||||
label={L("general.entries_per_page")}
|
label={L("general.entries_per_page")}
|
||||||
onChange={(e) => this.setPageSize(parseInt(e.target.value))}
|
onChange={(e) => this.setPageSize(parseInt(e.target.value))}
|
||||||
size={"small"}
|
size={"small"}
|
||||||
>
|
>
|
||||||
{options.map(size => <MenuItem key={"size-" + size} value={size}>{size}</MenuItem>)}
|
{options.map(size => <MenuItem key={"size-" + size} value={size}>{size}</MenuItem>)}
|
||||||
</Select>
|
</Select>
|
||||||
<MuiPagination count={this.getPageCount()} onChange={(_, page) => this.setPage(page)} />
|
<MuiPagination
|
||||||
{sprintf(L("general.showing_x_of_y_entries"), numEntries, this.data.total)}
|
count={this.getPageCount()}
|
||||||
|
onChange={(_, page) => this.setPage(page)}
|
||||||
|
/>
|
||||||
|
<Box gridColumn={"1 / 3"} mt={1}>
|
||||||
|
{sprintf(L("general.showing_x_of_y_entries"), numEntries, this.data.total)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
import {format, parse} from "date-fns";
|
import {format, parse} from "date-fns";
|
||||||
import {API_DATE_FORMAT, API_DATETIME_FORMAT} from "./constants";
|
import {API_DATE_FORMAT, API_DATETIME_FORMAT} from "./constants";
|
||||||
|
|
||||||
|
function createDownload(name, data) {
|
||||||
|
const url = window.URL.createObjectURL(new Blob([data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', name);
|
||||||
|
link.setAttribute("target", "_blank");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function humanReadableSize(bytes, dp = 1) {
|
function humanReadableSize(bytes, dp = 1) {
|
||||||
const thresh = 1024;
|
const thresh = 1024;
|
||||||
|
|
||||||
@ -87,4 +98,4 @@ const isInt = (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl,
|
export { humanReadableSize, removeParameter, getParameter, encodeText, decodeText, getBaseUrl,
|
||||||
formatDate, formatDateTime, upperFirstChars, isInt };
|
formatDate, formatDateTime, upperFirstChars, isInt, createDownload };
|
Loading…
Reference in New Issue
Block a user