SQL expression rewrite, Pagination, some frontend stuff
This commit is contained in:
parent
4bfd6754cf
commit
99bfd7e505
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Core\API {
|
||||
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Objects\Context;
|
||||
|
||||
abstract class GroupsAPI extends Request {
|
||||
@ -12,7 +13,7 @@ namespace Core\API {
|
||||
|
||||
protected function groupExists($name): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())
|
||||
$res = $sql->select(new Count())
|
||||
->from("Group")
|
||||
->whereEq("name", $name)
|
||||
->execute();
|
||||
@ -29,76 +30,81 @@ namespace Core\API\Groups {
|
||||
use Core\API\GroupsAPI;
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\Traits\Pagination;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Objects\Context;
|
||||
use Core\Objects\DatabaseEntity\Controller\NMRelation;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
|
||||
class Fetch extends GroupsAPI {
|
||||
|
||||
use Pagination;
|
||||
|
||||
private int $groupCount;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
|
||||
));
|
||||
parent::__construct($context, $externalCall,
|
||||
self::getPaginationParameters(['id', 'name', 'member_count'])
|
||||
);
|
||||
|
||||
$this->groupCount = 0;
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
$page = $this->getParam("page");
|
||||
if($page < 1) {
|
||||
return $this->createError("Invalid page count");
|
||||
}
|
||||
|
||||
$count = $this->getParam("count");
|
||||
if($count < 1 || $count > 50) {
|
||||
return $this->createError("Invalid fetch count");
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$groupCount = Group::count($sql);
|
||||
if ($groupCount === false) {
|
||||
return $this->createError("Error fetching group count: " . $sql->getLastError());
|
||||
if (!$this->initPagination($sql, Group::class)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$groups = Group::findBy(Group::createBuilder($sql, false)
|
||||
->orderBy("id")
|
||||
->ascending()
|
||||
->limit($count)
|
||||
->offset(($page - 1) * $count));
|
||||
$memberCount = new Alias($sql->select(new Count())
|
||||
->from(NMRelation::buildTableName("User", "Group"))
|
||||
->whereEq("group_id", new Column("Group.id")), "memberCount");
|
||||
|
||||
if ($groups !== false) {
|
||||
$groupsQuery = $this->createPaginationQuery($sql, [$memberCount]);
|
||||
$groups = $groupsQuery->execute();
|
||||
if ($groups !== false && $groups !== null) {
|
||||
$this->result["groups"] = [];
|
||||
$this->result["pageCount"] = intval(ceil($this->groupCount / $count));
|
||||
$this->result["totalCount"] = $this->groupCount;
|
||||
|
||||
foreach ($groups as $groupId => $group) {
|
||||
$this->result["groups"][$groupId] = $group->jsonSerialize();
|
||||
$this->result["groups"][$groupId]["memberCount"] = 0;
|
||||
}
|
||||
|
||||
$nmTable = NMRelation::buildTableName("User", "Group");
|
||||
$res = $sql->select("group_id", $sql->count("user_id"))
|
||||
->from($nmTable)
|
||||
->groupBy("group_id")
|
||||
->execute();
|
||||
|
||||
if (is_array($res)) {
|
||||
foreach ($res as $row) {
|
||||
list ($groupId, $memberCount) = [$row["group_id"], $row["user_id_count"]];
|
||||
if (isset($this->result["groups"][$groupId])) {
|
||||
$this->result["groups"][$groupId]["memberCount"] = $memberCount;
|
||||
}
|
||||
}
|
||||
foreach ($groups as $group) {
|
||||
$groupData = $group->jsonSerialize();
|
||||
$groupData["memberCount"] = $group["memberCount"];
|
||||
$this->result["groups"][] = $groupData;
|
||||
}
|
||||
} else {
|
||||
return $this->createError("Error fetching groups: " . $sql->getLastError());
|
||||
}
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
}
|
||||
|
||||
class Get extends GroupsAPI {
|
||||
public function __construct(Context $context, bool $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, [
|
||||
"id" => new Parameter("id", Parameter::TYPE_INT)
|
||||
]);
|
||||
}
|
||||
|
||||
protected function _execute(): bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$groupId = $this->getParam("id");
|
||||
$group = Group::find($sql, $groupId);
|
||||
if ($group === false) {
|
||||
return $this->createError("Error fetching group: " . $sql->getLastError());
|
||||
} else if ($group === null) {
|
||||
return $this->createError("Group not found");
|
||||
} else {
|
||||
$this->result["group"] = $group->jsonSerialize();
|
||||
$this->result["group"]["members"] = $group->getMembers($sql);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Create extends GroupsAPI {
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
|
@ -35,13 +35,15 @@ class Parameter {
|
||||
public bool $optional;
|
||||
public int $type;
|
||||
public string $typeName;
|
||||
public ?array $choices;
|
||||
|
||||
public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL) {
|
||||
public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL, ?array $choices = NULL) {
|
||||
$this->name = $name;
|
||||
$this->optional = $optional;
|
||||
$this->defaultValue = $defaultValue;
|
||||
$this->value = $defaultValue;
|
||||
$this->type = $type;
|
||||
$this->choices = $choices;
|
||||
$this->typeName = $this->getTypeName();
|
||||
}
|
||||
|
||||
@ -63,22 +65,22 @@ class Parameter {
|
||||
}
|
||||
|
||||
public function getSwaggerFormat(): ?string {
|
||||
switch ($this->type) {
|
||||
case self::TYPE_DATE:
|
||||
return self::DATE_FORMAT;
|
||||
case self::TYPE_TIME:
|
||||
return self::TIME_FORMAT;
|
||||
case self::TYPE_DATE_TIME:
|
||||
return self::DATE_TIME_FORMAT;
|
||||
case self::TYPE_EMAIL:
|
||||
return "email";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return match ($this->type) {
|
||||
self::TYPE_DATE => self::DATE_FORMAT,
|
||||
self::TYPE_TIME => self::TIME_FORMAT,
|
||||
self::TYPE_DATE_TIME => self::DATE_TIME_FORMAT,
|
||||
self::TYPE_EMAIL => "email",
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function getTypeName(): string {
|
||||
return ($this->type >= 0 && $this->type < count(Parameter::names)) ? Parameter::names[$this->type] : "INVALID";
|
||||
$typeName = Parameter::names[$this->type] ?? "INVALID";
|
||||
if ($this->choices) {
|
||||
$typeName .= ", choices: " . json_encode($this->choices);
|
||||
}
|
||||
|
||||
return $typeName;
|
||||
}
|
||||
|
||||
public function toString(): string {
|
||||
@ -86,7 +88,7 @@ class Parameter {
|
||||
|
||||
$str = "$typeName $this->name";
|
||||
$defaultValue = (is_null($this->value) ? 'NULL' : $this->value);
|
||||
if($this->optional) {
|
||||
if ($this->optional) {
|
||||
$str = "[$str = $defaultValue]";
|
||||
}
|
||||
|
||||
@ -94,21 +96,21 @@ class Parameter {
|
||||
}
|
||||
|
||||
public static function parseType($value): int {
|
||||
if(is_array($value))
|
||||
if (is_array($value))
|
||||
return Parameter::TYPE_ARRAY;
|
||||
else if(is_numeric($value) && intval($value) == $value)
|
||||
else if (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) || (is_numeric($value) && floatval($value) == $value))
|
||||
return Parameter::TYPE_FLOAT;
|
||||
else if(is_bool($value) || $value == "true" || $value == "false")
|
||||
else if (is_bool($value) || $value == "true" || $value == "false")
|
||||
return Parameter::TYPE_BOOLEAN;
|
||||
else if(is_a($value, 'DateTime'))
|
||||
else if (is_a($value, 'DateTime'))
|
||||
return Parameter::TYPE_DATE_TIME;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value)
|
||||
else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value)
|
||||
return Parameter::TYPE_DATE;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value)
|
||||
else if ($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value)
|
||||
return Parameter::TYPE_TIME;
|
||||
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value)
|
||||
else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value)
|
||||
return Parameter::TYPE_DATE_TIME;
|
||||
else if (filter_var($value, FILTER_VALIDATE_EMAIL))
|
||||
return Parameter::TYPE_EMAIL;
|
||||
@ -117,88 +119,90 @@ class Parameter {
|
||||
}
|
||||
|
||||
public function parseParam($value): bool {
|
||||
switch($this->type) {
|
||||
|
||||
$valid = false;
|
||||
switch ($this->type) {
|
||||
case Parameter::TYPE_INT:
|
||||
if(is_numeric($value) && intval($value) == $value) {
|
||||
if (is_numeric($value) && intval($value) == $value) {
|
||||
$this->value = intval($value);
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
case Parameter::TYPE_FLOAT:
|
||||
if(is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) {
|
||||
if (is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) {
|
||||
$this->value = floatval($value);
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
case Parameter::TYPE_BOOLEAN:
|
||||
if(strcasecmp($value, 'true') === 0)
|
||||
if (strcasecmp($value, 'true') === 0) {
|
||||
$this->value = true;
|
||||
else if(strcasecmp($value, 'false') === 0)
|
||||
$valid = true;
|
||||
} else if (strcasecmp($value, 'false') === 0) {
|
||||
$this->value = false;
|
||||
else if(is_bool($value))
|
||||
$valid = true;
|
||||
} else if (is_bool($value)) {
|
||||
$this->value = (bool)$value;
|
||||
else
|
||||
return false;
|
||||
return true;
|
||||
|
||||
case Parameter::TYPE_DATE:
|
||||
if(is_a($value, "DateTime")) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
|
||||
$d = DateTime::createFromFormat(self::DATE_FORMAT, $value);
|
||||
if($d && $d->format(self::DATE_FORMAT) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
case Parameter::TYPE_TIME:
|
||||
if(is_a($value, "DateTime")) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
$d = DateTime::createFromFormat(self::TIME_FORMAT, $value);
|
||||
if($d && $d->format(self::TIME_FORMAT) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Parameter::TYPE_DATE:
|
||||
case Parameter::TYPE_DATE_TIME:
|
||||
if(is_a($value, 'DateTime')) {
|
||||
if ($value instanceof DateTime) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
$valid = true;
|
||||
} else {
|
||||
$d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value);
|
||||
if($d && $d->format(self::DATE_TIME_FORMAT) === $value) {
|
||||
$format = $this->getFormat();
|
||||
$d = DateTime::createFromFormat($format, $value);
|
||||
if ($d && $d->format($format) === $value) {
|
||||
$this->value = $d;
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
case Parameter::TYPE_EMAIL:
|
||||
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
case Parameter::TYPE_ARRAY:
|
||||
if(is_array($value)) {
|
||||
if (is_array($value)) {
|
||||
$this->value = $value;
|
||||
return true;
|
||||
$valid = true;
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->value = $value;
|
||||
return true;
|
||||
$valid = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($valid && $this->choices) {
|
||||
if (!in_array($this->value, $this->choices)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
|
||||
private function getFormat(): ?string {
|
||||
if ($this->type === self::TYPE_TIME) {
|
||||
return self::TIME_FORMAT;
|
||||
} else if ($this->type === self::TYPE_DATE) {
|
||||
return self::DATE_FORMAT;
|
||||
} else if ($this->type === self::TYPE_DATE_TIME) {
|
||||
return self::DATE_TIME_FORMAT;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,9 @@ namespace Core\API\Parameter;
|
||||
class StringType extends Parameter {
|
||||
|
||||
public int $maxLength;
|
||||
public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL) {
|
||||
public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL, ?array $choices = NULL) {
|
||||
$this->maxLength = $maxLength;
|
||||
parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue);
|
||||
parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue, $choices);
|
||||
}
|
||||
|
||||
public function parseParam($value): bool {
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\API;
|
||||
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\Expression\Distinct;
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
@ -16,24 +18,24 @@ class Stats extends Request {
|
||||
parent::__construct($context, $externalCall, array());
|
||||
}
|
||||
|
||||
private function getUserCount() {
|
||||
private function getUserCount(): int {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())->from("User")->execute();
|
||||
$res = $sql->select(new Count())->from("User")->execute();
|
||||
$this->success = $this->success && ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
return ($this->success ? $res[0]["count"] : 0);
|
||||
return ($this->success ? intval($res[0]["count"]) : 0);
|
||||
}
|
||||
|
||||
private function getPageCount() {
|
||||
private function getPageCount(): int {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select($sql->count())->from("Route")
|
||||
$res = $sql->select(new Count())->from("Route")
|
||||
->where(new CondBool("active"))
|
||||
->execute();
|
||||
$this->success = $this->success && ($res !== FALSE);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
return ($this->success ? $res[0]["count"] : 0);
|
||||
return ($this->success ? intval($res[0]["count"]) : 0);
|
||||
}
|
||||
|
||||
private function checkSettings(): bool {
|
||||
@ -55,7 +57,7 @@ class Stats extends Request {
|
||||
$date = new DateTime();
|
||||
$monthStart = $date->format("Ym00");
|
||||
$monthEnd = $date->modify("+1 month")->format("Ym00");
|
||||
$res = $sql->select($sql->count($sql->distinct("cookie")))
|
||||
$res = $sql->select(new Count(new Distinct("cookie")))
|
||||
->from("Visitor")
|
||||
->where(new Compare("day", $monthStart, ">="))
|
||||
->where(new Compare("day", $monthEnd, "<"))
|
||||
@ -92,11 +94,12 @@ class Stats extends Request {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->result["userCount"] = $userCount;
|
||||
$this->result["pageCount"] = $pageCount;
|
||||
$this->result["visitors"] = $visitorStatistics;
|
||||
$this->result["visitorsTotal"] = $visitorCount;
|
||||
$this->result["server"] = array(
|
||||
$this->result["data"] = [
|
||||
"userCount" => $userCount,
|
||||
"pageCount" => $pageCount,
|
||||
"visitors" => $visitorStatistics,
|
||||
"visitorsTotal" => $visitorCount,
|
||||
"server" => [
|
||||
"version" => WEBBASE_VERSION,
|
||||
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
|
||||
"memory_usage" => memory_get_usage(),
|
||||
@ -104,7 +107,9 @@ class Stats extends Request {
|
||||
"database" => $this->context->getSQL()->getStatus(),
|
||||
"mail" => $this->mailConfigured,
|
||||
"reCaptcha" => $this->recaptchaConfigured
|
||||
);
|
||||
],
|
||||
];
|
||||
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ namespace Core\API\Template {
|
||||
|
||||
try {
|
||||
$this->result["html"] = $twigEnvironment->render($templateFile, $parameters);
|
||||
} catch (LoaderError | RuntimeError | SyntaxError $e) {
|
||||
} catch (LoaderError | RuntimeError | SyntaxError | \RuntimeException $e) {
|
||||
return $this->createError("Error rendering twig template: " . $e->getMessage());
|
||||
}
|
||||
|
||||
|
109
Core/API/Traits/Pagination.trait.php
Normal file
109
Core/API/Traits/Pagination.trait.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Core\API\Traits;
|
||||
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityQuery;
|
||||
use Core\Objects\DatabaseEntity\User;
|
||||
|
||||
trait Pagination {
|
||||
|
||||
static function getPaginationParameters(array $orderColumns): array {
|
||||
return [
|
||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20),
|
||||
'orderBy' => new StringType('orderBy', -1, true, "id", $orderColumns),
|
||||
'sortOrder' => new StringType('sortOrder', -1, true, 'asc', ['asc', 'desc']),
|
||||
];
|
||||
}
|
||||
|
||||
function initPagination(SQL $sql, string $class, ?Condition $condition = null, int $maxPageSize = 100): bool {
|
||||
$this->paginationClass = $class;
|
||||
$this->paginationCondition = $condition;
|
||||
if (!$this->validateParameters($maxPageSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->entityCount = call_user_func("$this->paginationClass::count", $sql, $condition);
|
||||
if ($this->entityCount === false) {
|
||||
return $this->createError("Error fetching $this->paginationClass::count: " . $sql->getLastError());
|
||||
}
|
||||
|
||||
$pageCount = intval(ceil($this->entityCount / $this->pageSize));
|
||||
$this->page = min($this->page, $pageCount); // number of pages changed due to pageSize / filter
|
||||
|
||||
$this->result["pagination"] = [
|
||||
"current" => $this->page,
|
||||
"pageSize" => $this->pageSize,
|
||||
"pageCount" => $pageCount,
|
||||
"total" => $this->entityCount
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateParameters(int $maxCount = 100): bool {
|
||||
$this->page = $this->getParam("page");
|
||||
if ($this->page < 1) {
|
||||
return $this->createError("Invalid page count");
|
||||
}
|
||||
|
||||
$this->pageSize = $this->getParam("count");
|
||||
if ($this->pageSize < 1 || $this->pageSize > $maxCount) {
|
||||
return $this->createError("Invalid fetch count");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function createPaginationQuery(SQL $sql, array $additionalValues = []): DatabaseEntityQuery {
|
||||
$page = $this->getParam("page");
|
||||
$count = $this->getParam("count");
|
||||
$orderBy = $this->getParam("orderBy");
|
||||
$sortOrder = $this->getParam("sortOrder");
|
||||
|
||||
$baseQuery = call_user_func("$this->paginationClass::createBuilder", $sql, false);
|
||||
$entityQuery = $baseQuery
|
||||
->fetchEntities()
|
||||
->limit($count)
|
||||
->offset(($page - 1) * $count);
|
||||
|
||||
if ($this->paginationCondition) {
|
||||
$entityQuery->where($this->paginationCondition);
|
||||
}
|
||||
|
||||
if (!empty($additionalValues)) {
|
||||
foreach ($additionalValues as $additionalValue) {
|
||||
$entityQuery->addCustomValue($additionalValue);
|
||||
}
|
||||
}
|
||||
|
||||
if ($orderBy) {
|
||||
$handler = $baseQuery->getHandler();
|
||||
$baseTable = $handler->getTableName();
|
||||
$sortColumn = DatabaseEntityHandler::getColumnName($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);
|
||||
}
|
||||
}
|
||||
|
||||
if ($sortOrder === "asc") {
|
||||
$entityQuery->ascending();
|
||||
} else {
|
||||
$entityQuery->descending();
|
||||
}
|
||||
|
||||
return $entityQuery;
|
||||
}
|
||||
}
|
@ -132,11 +132,12 @@ namespace Core\API\User {
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\Template\Render;
|
||||
use Core\API\Traits\Pagination;
|
||||
use Core\API\UserAPI;
|
||||
use Core\API\VerifyCaptcha;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Objects\DatabaseEntity\Group;
|
||||
use Core\Objects\DatabaseEntity\UserToken;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
@ -209,96 +210,65 @@ namespace Core\API\User {
|
||||
|
||||
class Fetch extends UserAPI {
|
||||
|
||||
use Pagination;
|
||||
|
||||
public function __construct(Context $context, $externalCall = false) {
|
||||
parent::__construct($context, $externalCall, array(
|
||||
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
|
||||
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
|
||||
));
|
||||
}
|
||||
|
||||
private function selectIds($page, $count): array|bool {
|
||||
$sql = $this->context->getSQL();
|
||||
$res = $sql->select("User.id")
|
||||
->from("User")
|
||||
->limit($count)
|
||||
->offset(($page - 1) * $count)
|
||||
->orderBy("User.id")
|
||||
->ascending()
|
||||
->execute();
|
||||
|
||||
$this->success = ($res !== NULL);
|
||||
$this->lastError = $sql->getLastError();
|
||||
|
||||
if ($this->success && is_array($res)) {
|
||||
return array_map(function ($row) {
|
||||
return intval($row["id"]);
|
||||
}, $res);
|
||||
}
|
||||
|
||||
return false;
|
||||
parent::__construct($context, $externalCall,
|
||||
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'])
|
||||
);
|
||||
}
|
||||
|
||||
public function _execute(): bool {
|
||||
|
||||
$page = $this->getParam("page");
|
||||
if ($page < 1) {
|
||||
return $this->createError("Invalid page count");
|
||||
}
|
||||
|
||||
$count = $this->getParam("count");
|
||||
if ($count < 1 || $count > 50) {
|
||||
return $this->createError("Invalid fetch count");
|
||||
}
|
||||
|
||||
$condition = null;
|
||||
$currentUser = $this->context->getUser();
|
||||
$fullInfo = ($currentUser->hasGroup(Group::ADMIN) ||
|
||||
$currentUser->hasGroup(Group::SUPPORT));
|
||||
|
||||
$orderBy = $this->getParam("orderBy");
|
||||
$publicAttributes = ["id", "name", "fullName", "profilePicture", "email"]; // TODO: , "groupNames"];
|
||||
|
||||
$condition = null;
|
||||
if (!$fullInfo) {
|
||||
$condition = new CondOr(
|
||||
new Compare("User.id", $currentUser->getId()),
|
||||
new CondBool("User.confirmed")
|
||||
);
|
||||
|
||||
if ($orderBy && !in_array($orderBy, $publicAttributes)) {
|
||||
return $this->createError("Insufficient permissions for sorting by field '$orderBy'");
|
||||
}
|
||||
}
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$userCount = User::count($sql, $condition);
|
||||
if ($userCount === false) {
|
||||
return $this->createError("Error fetching user count: " . $sql->getLastError());
|
||||
if (!$this->initPagination($sql, User::class, $condition)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$userQuery = User::createBuilder($sql, false)
|
||||
->orderBy("id")
|
||||
->ascending()
|
||||
->limit($count)
|
||||
->offset(($page - 1) * $count)
|
||||
->fetchEntities();
|
||||
|
||||
if ($condition) {
|
||||
$userQuery->where($condition);
|
||||
}
|
||||
$groupNames = new Alias(
|
||||
$sql->select(new JsonArrayAgg("name"))->from("Group")
|
||||
->leftJoin("NM_Group_User", "NM_Group_User.group_id", "Group.id")
|
||||
->whereEq("NM_Group_User.user_id", new Column("User.id")),
|
||||
"groups"
|
||||
);
|
||||
|
||||
$userQuery = $this->createPaginationQuery($sql, [$groupNames]);
|
||||
$users = User::findBy($userQuery);
|
||||
if ($users !== false) {
|
||||
if ($users !== false && $users !== null) {
|
||||
$this->result["users"] = [];
|
||||
|
||||
foreach ($users as $userId => $user) {
|
||||
$serialized = $user->jsonSerialize();
|
||||
|
||||
if (!$fullInfo && $userId !== $currentUser->getId()) {
|
||||
$publicAttributes = ["id", "name", "fullName", "profilePicture", "email", "groups"];
|
||||
foreach (array_keys($serialized) as $attr) {
|
||||
if (!in_array($attr, $publicAttributes)) {
|
||||
unset($serialized[$attr]);
|
||||
unset ($serialized[$attr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->result["users"][$userId] = $serialized;
|
||||
$this->result["users"][] = $serialized;
|
||||
}
|
||||
|
||||
$this->result["pageCount"] = intval(ceil($userCount / $count));
|
||||
$this->result["totalCount"] = $userCount;
|
||||
} else {
|
||||
return $this->createError("Error fetching users: " . $sql->getLastError());
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ namespace Core\API\Visitors {
|
||||
use Core\API\Parameter\Parameter;
|
||||
use Core\API\Parameter\StringType;
|
||||
use Core\API\VisitorsAPI;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use DateTime;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
@ -82,7 +83,7 @@ namespace Core\API\Visitors {
|
||||
$type = $this->getParam("type");
|
||||
|
||||
$sql = $this->context->getSQL();
|
||||
$query = $sql->select($sql->count(), "day")
|
||||
$query = $sql->select(new Count(), "day")
|
||||
->from("Visitor")
|
||||
->whereGt("count", 1)
|
||||
->groupBy("day")
|
||||
|
@ -19,6 +19,7 @@ namespace Documents\Install {
|
||||
|
||||
use Core\Configuration\Configuration;
|
||||
use Core\Configuration\CreateDatabase;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\Query\Commit;
|
||||
use Core\Driver\SQL\Query\StartTransaction;
|
||||
use Core\Driver\SQL\SQL;
|
||||
@ -196,8 +197,7 @@ namespace Documents\Install {
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
}
|
||||
|
||||
$countKeyword = $sql->count();
|
||||
$res = $sql->select($countKeyword)->from("User")->execute();
|
||||
$res = $sql->select(new Count())->from("User")->execute();
|
||||
if ($res === FALSE) {
|
||||
return self::DATABASE_CONFIGURATION;
|
||||
} else {
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Core\Driver\SQL\Column;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Column extends Expression {
|
||||
|
||||
@ -20,4 +21,7 @@ class Column extends Expression {
|
||||
public function notNull(): bool { return !$this->nullable; }
|
||||
public function getDefaultValue() { return $this->defaultValue; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
return $sql->columnName($this->name);
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Compare extends Condition {
|
||||
|
||||
private string $operator;
|
||||
@ -18,4 +20,16 @@ class Compare extends Condition {
|
||||
public function getValue() { return $this->value; }
|
||||
public function getOperator(): string { return $this->operator; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
|
||||
if ($this->value === null) {
|
||||
if ($this->operator === "=") {
|
||||
return $sql->columnName($this->column) . " IS NULL";
|
||||
} else if ($this->operator === "!=") {
|
||||
return $sql->columnName($this->column) . " IS NOT NULL";
|
||||
}
|
||||
}
|
||||
|
||||
return $sql->columnName($this->column) . $this->operator . $sql->addValue($this->value, $params);
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondAnd extends Condition {
|
||||
|
||||
private array $conditions;
|
||||
@ -11,4 +13,12 @@ class CondAnd extends Condition {
|
||||
}
|
||||
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
$conditions = array();
|
||||
foreach($this->getConditions() as $cond) {
|
||||
$conditions[] = $sql->addValue($cond, $params);
|
||||
}
|
||||
return "(" . implode(" AND ", $conditions) . ")";
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondBool extends Condition {
|
||||
|
||||
private $value;
|
||||
@ -12,4 +14,11 @@ class CondBool extends Condition {
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
if (is_string($this->value)) {
|
||||
return $sql->columnName($this->value);
|
||||
} else {
|
||||
return $sql->addValue($this->value);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,9 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondIn extends Condition {
|
||||
|
||||
private $needle;
|
||||
@ -14,4 +17,25 @@ class CondIn extends Condition {
|
||||
|
||||
public function getNeedle() { return $this->needle; }
|
||||
public function getHaystack() { return $this->haystack; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
|
||||
$haystack = $this->getHaystack();
|
||||
if (is_array($haystack)) {
|
||||
$values = array();
|
||||
foreach ($haystack as $value) {
|
||||
$values[] = $sql->addValue($value, $params);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
$values = "($values)";
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->build($params);
|
||||
} else {
|
||||
$sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack));
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sql->addValue($this->needle, $params) . " IN $values";
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class CondKeyword extends Condition {
|
||||
|
||||
private $leftExpression;
|
||||
@ -17,4 +19,11 @@ abstract class CondKeyword extends Condition {
|
||||
public function getLeftExp() { return $this->leftExpression; }
|
||||
public function getRightExp() { return $this->rightExpression; }
|
||||
public function getKeyword(): string { return $this->keyword; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
$keyword = $this->getKeyword();
|
||||
$left = $sql->addValue($this->getLeftExp(), $params);
|
||||
$right = $sql->addValue($this->getRightExp(), $params);
|
||||
return "$left $keyword $right";
|
||||
}
|
||||
}
|
@ -7,4 +7,5 @@ class CondLike extends CondKeyword {
|
||||
public function __construct($leftExpression, $rightExpression) {
|
||||
parent::__construct("LIKE", $leftExpression, $rightExpression);
|
||||
}
|
||||
|
||||
}
|
@ -2,15 +2,21 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondNot extends Condition {
|
||||
|
||||
private $expression; // string or condition
|
||||
private mixed $expression; // string or condition
|
||||
|
||||
public function __construct($expression) {
|
||||
public function __construct(mixed $expression) {
|
||||
$this->expression = $expression;
|
||||
}
|
||||
|
||||
public function getExpression() {
|
||||
return $this->expression;
|
||||
public function getExpression(SQL $sql, array &$params): string {
|
||||
if (is_string($this->expression)) {
|
||||
return "NOT " . $sql->columnName($this->expression);
|
||||
} else {
|
||||
return "NOT " . $sql->addValue($this->expression, $params);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondNull extends Condition {
|
||||
|
||||
private string $column;
|
||||
@ -11,4 +13,8 @@ class CondNull extends Condition {
|
||||
}
|
||||
|
||||
public function getColumn(): string { return $this->column; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
return $sql->columnName($this->getColumn()) . " IS NULL";
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CondOr extends Condition {
|
||||
|
||||
private array $conditions;
|
||||
@ -11,4 +13,12 @@ class CondOr extends Condition {
|
||||
}
|
||||
|
||||
public function getConditions(): array { return $this->conditions; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
$conditions = array();
|
||||
foreach($this->getConditions() as $cond) {
|
||||
$conditions[] = $sql->addValue($cond, $params);
|
||||
}
|
||||
return "(" . implode(" OR ", $conditions) . ")";
|
||||
}
|
||||
}
|
@ -1,22 +1,23 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Core\Driver\SQL\Condition;
|
||||
|
||||
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Exists extends Condition {
|
||||
|
||||
class Exists extends Condition
|
||||
{
|
||||
private Select $subQuery;
|
||||
|
||||
public function __construct(Select $subQuery)
|
||||
{
|
||||
public function __construct(Select $subQuery) {
|
||||
$this->subQuery = $subQuery;
|
||||
}
|
||||
|
||||
public function getSubQuery(): Select
|
||||
{
|
||||
public function getSubQuery(): Select {
|
||||
return $this->subQuery;
|
||||
}
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
return "EXISTS(" .$this->getSubQuery()->build($params) . ")";
|
||||
}
|
||||
}
|
32
Core/Driver/SQL/Expression/Alias.class.php
Normal file
32
Core/Driver/SQL/Expression/Alias.class.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Alias extends Expression {
|
||||
|
||||
private mixed $value;
|
||||
private string $alias;
|
||||
|
||||
public function __construct(mixed $value, string $alias) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function getAlias(): string {
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function getValue(): mixed {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
protected function addValue(SQL $sql, array &$params): string {
|
||||
return $sql->addValue($this->value, $params);
|
||||
}
|
||||
|
||||
public function getExpression(SQL $sql, array &$params): string {
|
||||
return $this->addValue($sql, $params) . " AS " . $this->getAlias();
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CaseWhen extends Expression {
|
||||
|
||||
@ -20,4 +21,13 @@ class CaseWhen extends Expression {
|
||||
public function getTrueCase() { return $this->trueCase; }
|
||||
public function getFalseCase() { return $this->falseCase; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
$condition = $sql->buildCondition($this->getCondition(), $params);
|
||||
|
||||
// psql requires constant values here
|
||||
$trueCase = $sql->addValue($this->getTrueCase(), $params, true);
|
||||
$falseCase = $sql->addValue($this->getFalseCase(), $params, true);
|
||||
|
||||
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
|
||||
}
|
||||
}
|
24
Core/Driver/SQL/Expression/Count.class.php
Normal file
24
Core/Driver/SQL/Expression/Count.class.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Count extends Alias {
|
||||
public function __construct(mixed $value = "*", string $alias = "count") {
|
||||
parent::__construct($value, $alias);
|
||||
}
|
||||
|
||||
function addValue(SQL $sql, array &$params): string {
|
||||
$value = $this->getValue();
|
||||
if (is_string($value)) {
|
||||
if ($value === "*") {
|
||||
return "COUNT(*)";
|
||||
} else {
|
||||
return "COUNT(" . $sql->columnName($value) . ")";
|
||||
}
|
||||
} else {
|
||||
return "COUNT(" . $sql->addValue($value, $params) . ")";
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,20 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\MySQL;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Exception;
|
||||
|
||||
class CurrentTimeStamp extends Expression {
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
if ($sql instanceof MySQL) {
|
||||
return "NOW()";
|
||||
} else if ($sql instanceof PostgreSQL) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else {
|
||||
throw new Exception("CurrentTimeStamp Not implemented for driver type: " . get_class($sql));
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,12 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\MySQL;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\External\PHPMailer\Exception;
|
||||
|
||||
class DateAdd extends Expression {
|
||||
|
||||
private Expression $lhs;
|
||||
@ -18,4 +24,26 @@ class DateAdd extends Expression {
|
||||
public function getRHS(): Expression { return $this->rhs; }
|
||||
public function getUnit(): string { return $this->unit; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
if ($sql instanceof MySQL) {
|
||||
$lhs = $sql->addValue($this->getLHS(), $params);
|
||||
$rhs = $sql->addValue($this->getRHS(), $params);
|
||||
$unit = $this->getUnit();
|
||||
return "DATE_ADD($lhs, INTERVAL $rhs $unit)";
|
||||
} else if ($sql instanceof PostgreSQL) {
|
||||
$lhs = $sql->addValue($this->getLHS(), $params);
|
||||
$rhs = $sql->addValue($this->getRHS(), $params);
|
||||
$unit = $this->getUnit();
|
||||
|
||||
if ($this->getRHS() instanceof Column) {
|
||||
$rhs = "$rhs * INTERVAL '1 $unit'";
|
||||
} else {
|
||||
$rhs = "$rhs $unit";
|
||||
}
|
||||
|
||||
return "$lhs - $rhs";
|
||||
} else {
|
||||
throw new Exception("DateAdd Not implemented for driver type: " . get_class($sql));
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,12 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\MySQL;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\External\PHPMailer\Exception;
|
||||
|
||||
class DateSub extends Expression {
|
||||
|
||||
private Expression $lhs;
|
||||
@ -18,4 +24,26 @@ class DateSub extends Expression {
|
||||
public function getRHS(): Expression { return $this->rhs; }
|
||||
public function getUnit(): string { return $this->unit; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
if ($sql instanceof MySQL) {
|
||||
$lhs = $sql->addValue($this->getLHS(), $params);
|
||||
$rhs = $sql->addValue($this->getRHS(), $params);
|
||||
$unit = $this->getUnit();
|
||||
return "DATE_SUB($lhs, INTERVAL $rhs $unit)";
|
||||
} else if ($sql instanceof PostgreSQL) {
|
||||
$lhs = $sql->addValue($this->getLHS(), $params);
|
||||
$rhs = $sql->addValue($this->getRHS(), $params);
|
||||
$unit = $this->getUnit();
|
||||
|
||||
if ($this->getRHS() instanceof Column) {
|
||||
$rhs = "$rhs * INTERVAL '1 $unit'";
|
||||
} else {
|
||||
$rhs = "$rhs $unit";
|
||||
}
|
||||
|
||||
return "$lhs - $rhs";
|
||||
} else {
|
||||
throw new Exception("DateSub Not implemented for driver type: " . get_class($sql));
|
||||
}
|
||||
}
|
||||
}
|
22
Core/Driver/SQL/Expression/Distinct.class.php
Normal file
22
Core/Driver/SQL/Expression/Distinct.class.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class Distinct extends Expression {
|
||||
|
||||
private mixed $value;
|
||||
|
||||
public function __construct(mixed $value) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): mixed {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
return "DISTINCT(" . $sql->addValue($this->getValue(), $params) . ")";
|
||||
}
|
||||
}
|
@ -2,6 +2,10 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
abstract class Expression {
|
||||
|
||||
abstract function getExpression(SQL $sql, array &$params): string;
|
||||
|
||||
}
|
@ -2,17 +2,29 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\MySQL;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Exception;
|
||||
|
||||
class JsonArrayAgg extends Expression {
|
||||
|
||||
private $value;
|
||||
private string $alias;
|
||||
private mixed $value;
|
||||
|
||||
public function __construct($value, string $alias) {
|
||||
public function __construct(mixed $value) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
public function getAlias(): string { return $this->alias; }
|
||||
|
||||
public function getExpression(SQL $sql, array &$params): string {
|
||||
$value = is_string($this->value) ? new Column($this->value) : $this->value;
|
||||
$value = $sql->addValue($value, $params);
|
||||
if ($sql instanceof MySQL) {
|
||||
return "JSON_ARRAYAGG($value)";
|
||||
} else if ($sql instanceof PostgreSQL) {
|
||||
return "JSON_AGG($value)";
|
||||
} else {
|
||||
throw new Exception("JsonArrayAgg not implemented for driver type: " . get_class($sql));
|
||||
}
|
||||
}
|
||||
}
|
@ -2,17 +2,15 @@
|
||||
|
||||
namespace Core\Driver\SQL\Expression;
|
||||
|
||||
class Sum extends Expression {
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
private $value;
|
||||
private string $alias;
|
||||
class Sum extends Alias {
|
||||
|
||||
public function __construct($value, string $alias) {
|
||||
$this->value = $value;
|
||||
$this->alias = $alias;
|
||||
public function __construct(mixed $value, string $alias) {
|
||||
parent::__construct($value, $alias);
|
||||
}
|
||||
|
||||
public function getValue() { return $this->value; }
|
||||
public function getAlias(): string { return $this->alias; }
|
||||
|
||||
protected function addValue(SQL $sql, array &$params): string {
|
||||
return "SUM(" . $sql->addValue($this->getValue(), $params) . ")";
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ namespace Core\Driver\SQL;
|
||||
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
|
||||
// Unsafe sql
|
||||
class Keyword extends Expression {
|
||||
|
||||
private string $value;
|
||||
@ -14,4 +15,7 @@ class Keyword extends Expression {
|
||||
|
||||
public function getValue(): string { return $this->value; }
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn;
|
||||
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\DateAdd;
|
||||
use Core\Driver\SQL\Expression\DateSub;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\Query\CreateTrigger;
|
||||
use Core\Driver\SQL\Query\Query;
|
||||
@ -337,14 +334,8 @@ class MySQL extends SQL {
|
||||
}
|
||||
|
||||
public function addValue($val, &$params = NULL, bool $unsafe = false) {
|
||||
if ($val instanceof Keyword) {
|
||||
return $val->getValue();
|
||||
} else if ($val instanceof CurrentColumn) {
|
||||
return $val->getName();
|
||||
} else if ($val instanceof Column) {
|
||||
return $this->columnName($val->getName());
|
||||
} else if ($val instanceof Expression) {
|
||||
return $this->createExpression($val, $params);
|
||||
if ($val instanceof Expression) {
|
||||
return $val->getExpression($this, $params);
|
||||
} else {
|
||||
if ($unsafe) {
|
||||
return $this->getUnsafeValue($val);
|
||||
@ -460,24 +451,6 @@ class MySQL extends SQL {
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
|
||||
$lhs = $this->addValue($exp->getLHS(), $params);
|
||||
$rhs = $this->addValue($exp->getRHS(), $params);
|
||||
$unit = $exp->getUnit();
|
||||
$dateFunction = ($exp instanceof DateAdd ? "DATE_ADD" : "DATE_SUB");
|
||||
return "$dateFunction($lhs, INTERVAL $rhs $unit)";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "NOW()";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_ARRAYAGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RowIteratorMySQL extends RowIterator {
|
||||
|
@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn;
|
||||
use Core\Driver\SQL\Condition\CondRegex;
|
||||
use Core\Driver\SQL\Expression\Add;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\DateAdd;
|
||||
use Core\Driver\SQL\Expression\DateSub;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
use Core\Driver\SQL\Query\CreateTrigger;
|
||||
use Core\Driver\SQL\Query\Insert;
|
||||
@ -301,16 +298,13 @@ class PostgreSQL extends SQL {
|
||||
}
|
||||
|
||||
public function addValue($val, &$params = NULL, bool $unsafe = false) {
|
||||
if ($val instanceof Keyword) {
|
||||
return $val->getValue();
|
||||
} else if ($val instanceof CurrentTable) {
|
||||
// I don't remember we need this here?
|
||||
/*if ($val instanceof CurrentTable) {
|
||||
return "TG_TABLE_NAME";
|
||||
} else if ($val instanceof CurrentColumn) {
|
||||
return "NEW." . $this->columnName($val->getName());
|
||||
} else if ($val instanceof Column) {
|
||||
return $this->columnName($val->getName());
|
||||
} else if ($val instanceof Expression) {
|
||||
return $this->createExpression($val, $params);
|
||||
} else */if ($val instanceof Expression) {
|
||||
return $val->getExpression($this, $params);
|
||||
} else {
|
||||
if ($unsafe) {
|
||||
return $this->getUnsafeValue($val);
|
||||
@ -450,31 +444,6 @@ class PostgreSQL extends SQL {
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
|
||||
$lhs = $this->addValue($exp->getLHS(), $params);
|
||||
$rhs = $this->addValue($exp->getRHS(), $params);
|
||||
$unit = $exp->getUnit();
|
||||
|
||||
if ($exp->getRHS() instanceof Column) {
|
||||
$rhs = "$rhs * INTERVAL '1 $unit'";
|
||||
} else {
|
||||
$rhs = "$rhs $unit";
|
||||
}
|
||||
|
||||
$operator = ($exp instanceof DateAdd ? "+" : "-");
|
||||
return "$lhs $operator $rhs";
|
||||
} else if ($exp instanceof CurrentTimeStamp) {
|
||||
return "CURRENT_TIMESTAMP";
|
||||
} else if ($exp instanceof JsonArrayAgg) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "JSON_AGG($value) as $alias";
|
||||
} else {
|
||||
return parent::createExpression($exp, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RowIteratorPostgreSQL extends RowIterator {
|
||||
|
@ -26,4 +26,8 @@ abstract class Query extends Expression {
|
||||
}
|
||||
|
||||
public abstract function build(array &$params): ?string;
|
||||
|
||||
public function getExpression(SQL $sql, array &$params): string {
|
||||
return "(" . $this->build($params) . ")";
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace Core\Driver\SQL\Query;
|
||||
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\JsonArrayAgg;
|
||||
use Core\Driver\SQL\Join\InnerJoin;
|
||||
use Core\Driver\SQL\Join\Join;
|
||||
@ -38,8 +39,13 @@ class Select extends ConditionalQuery {
|
||||
$this->fetchType = SQL::FETCH_ALL;
|
||||
}
|
||||
|
||||
public function addColumn(string $columnName): Select {
|
||||
$this->selectValues[] = $columnName;
|
||||
public function select(...$selectValues): Select {
|
||||
$this->selectValues = (!empty($selectValues) && is_array($selectValues[0])) ? $selectValues[0] : $selectValues;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addSelectValue(...$selectValues): Select {
|
||||
$this->selectValues = array_merge($this->selectValues, $selectValues);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -142,25 +148,6 @@ class Select extends ConditionalQuery {
|
||||
foreach ($this->selectValues as $value) {
|
||||
if (is_string($value)) {
|
||||
$selectValues[] = $this->sql->columnName($value);
|
||||
} else if ($value instanceof Select) {
|
||||
$subSelect = $value->build($params);
|
||||
if (count($value->getSelectValues()) !== 1) {
|
||||
$selectValues[] = "($subSelect)";
|
||||
} else {
|
||||
$columnAlias = null;
|
||||
$subSelectColumn = $value->getSelectValues()[0];
|
||||
if (is_string($subSelectColumn) && ($index = stripos($subSelectColumn, " as ")) !== FALSE) {
|
||||
$columnAlias = substr($subSelectColumn, $index + 4);
|
||||
} else if ($subSelectColumn instanceof JsonArrayAgg) {
|
||||
$columnAlias = $subSelectColumn->getAlias();
|
||||
}
|
||||
|
||||
if ($columnAlias) {
|
||||
$selectValues[] = "($subSelect) as $columnAlias";
|
||||
} else {
|
||||
$selectValues[] = "($subSelect)";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$selectValues[] = $this->sql->addValue($value, $params);
|
||||
}
|
||||
|
@ -4,24 +4,12 @@ namespace Core\Driver\SQL;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Condition\Compare;
|
||||
use Core\Driver\SQL\Condition\CondAnd;
|
||||
use Core\Driver\SQL\Condition\CondBool;
|
||||
use Core\Driver\SQL\Condition\CondIn;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\Condition\CondKeyword;
|
||||
use Core\Driver\SQL\Condition\CondNot;
|
||||
use Core\Driver\Sql\Condition\CondNull;
|
||||
use Core\Driver\SQL\Condition\CondOr;
|
||||
use Core\Driver\SQL\Condition\Exists;
|
||||
use Core\Driver\SQL\Constraint\Constraint;
|
||||
use Core\Driver\SQL\Constraint\Unique;
|
||||
use Core\Driver\SQL\Constraint\PrimaryKey;
|
||||
use Core\Driver\SQL\Constraint\ForeignKey;
|
||||
use Core\Driver\SQL\Expression\CaseWhen;
|
||||
use Core\Driver\SQL\Expression\CurrentTimeStamp;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\Expression\Sum;
|
||||
use Core\Driver\SQL\Query\AlterTable;
|
||||
use Core\Driver\SQL\Query\Commit;
|
||||
use Core\Driver\SQL\Query\CreateProcedure;
|
||||
@ -236,6 +224,7 @@ abstract class SQL {
|
||||
public abstract function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string;
|
||||
public abstract function getProcedureHead(CreateProcedure $procedure): ?string;
|
||||
public abstract function getColumnType(Column $column): ?string;
|
||||
public abstract function getStatus();
|
||||
public function getProcedureTail(): string { return ""; }
|
||||
public function getReturning(?string $columns): string { return ""; }
|
||||
|
||||
@ -247,6 +236,10 @@ abstract class SQL {
|
||||
return $statements;
|
||||
}
|
||||
|
||||
public function getLogger(): Logger {
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
protected function getUnsafeValue($value): ?string {
|
||||
if (is_string($value)) {
|
||||
return "'" . addslashes("$value") . "'"; // unsafe operation here...
|
||||
@ -273,60 +266,15 @@ abstract class SQL {
|
||||
public function now(): CurrentTimeStamp { return new CurrentTimeStamp(); }
|
||||
public function currentTimestamp(): CurrentTimeStamp { return new CurrentTimeStamp(); }
|
||||
|
||||
public function count($col = NULL): Keyword {
|
||||
if (is_null($col)) {
|
||||
return new Keyword("COUNT(*) AS count");
|
||||
} else if($col instanceof Keyword) {
|
||||
return new Keyword("COUNT(" . $col->getValue() . ") AS count");
|
||||
} else {
|
||||
$countCol = strtolower(str_replace(".","_", $col)) . "_count";
|
||||
$col = $this->columnName($col);
|
||||
return new Keyword("COUNT($col) AS $countCol");
|
||||
}
|
||||
}
|
||||
|
||||
public function distinct($col): Keyword {
|
||||
$col = $this->columnName($col);
|
||||
return new Keyword("DISTINCT($col)");
|
||||
}
|
||||
|
||||
// Statements
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE);
|
||||
|
||||
public function buildCondition($condition, &$params) {
|
||||
public function buildCondition(Condition|array $condition, &$params): string {
|
||||
|
||||
if ($condition instanceof CondOr) {
|
||||
$conditions = array();
|
||||
foreach($condition->getConditions() as $cond) {
|
||||
$conditions[] = $this->buildCondition($cond, $params);
|
||||
}
|
||||
return "(" . implode(" OR ", $conditions) . ")";
|
||||
} else if ($condition instanceof CondAnd) {
|
||||
$conditions = array();
|
||||
foreach($condition->getConditions() as $cond) {
|
||||
$conditions[] = $this->buildCondition($cond, $params);
|
||||
}
|
||||
return "(" . implode(" AND ", $conditions) . ")";
|
||||
} else if ($condition instanceof Compare) {
|
||||
$column = $this->columnName($condition->getColumn());
|
||||
$value = $condition->getValue();
|
||||
$operator = $condition->getOperator();
|
||||
|
||||
if ($value === null) {
|
||||
if ($operator === "=") {
|
||||
return "$column IS NULL";
|
||||
} else if ($operator === "!=") {
|
||||
return "$column IS NOT NULL";
|
||||
}
|
||||
}
|
||||
|
||||
return $column . $operator . $this->addValue($value, $params);
|
||||
} else if ($condition instanceof CondBool) {
|
||||
return $this->columnName($condition->getValue());
|
||||
} else if (is_array($condition)) {
|
||||
if (is_array($condition)) {
|
||||
if (count($condition) === 1) {
|
||||
return $this->buildCondition($condition[0], $params);
|
||||
} else {
|
||||
@ -336,77 +284,8 @@ abstract class SQL {
|
||||
}
|
||||
return implode(" AND ", $conditions);
|
||||
}
|
||||
} else if($condition instanceof CondIn) {
|
||||
|
||||
$needle = $condition->getNeedle();
|
||||
$haystack = $condition->getHaystack();
|
||||
if (is_array($haystack)) {
|
||||
$values = array();
|
||||
foreach ($haystack as $value) {
|
||||
$values[] = $this->addValue($value, $params);
|
||||
}
|
||||
|
||||
$values = implode(",", $values);
|
||||
} else if($haystack instanceof Select) {
|
||||
$values = $haystack->build($params);
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported in-expression value: " . get_class($condition));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($needle instanceof Column) {
|
||||
$lhs = $this->createExpression($needle, $params);
|
||||
} else {
|
||||
$lhs = $this->addValue($needle, $params);
|
||||
}
|
||||
|
||||
return "$lhs IN ($values)";
|
||||
} else if($condition instanceof CondKeyword) {
|
||||
$left = $condition->getLeftExp();
|
||||
$right = $condition->getRightExp();
|
||||
$keyword = $condition->getKeyword();
|
||||
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
|
||||
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
|
||||
return "$left $keyword $right ";
|
||||
} else if($condition instanceof CondNot) {
|
||||
$expression = $condition->getExpression();
|
||||
if ($expression instanceof Condition) {
|
||||
$expression = $this->buildCondition($expression, $params);
|
||||
} else {
|
||||
$expression = $this->columnName($expression);
|
||||
}
|
||||
|
||||
return "NOT $expression";
|
||||
} else if ($condition instanceof CondNull) {
|
||||
return $this->columnName($condition->getColumn()) . " IS NULL";
|
||||
} else if ($condition instanceof Exists) {
|
||||
return "EXISTS(" .$condition->getSubQuery()->build($params) . ")";
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported condition type: " . gettype($condition));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createExpression(Expression $exp, array &$params): ?string {
|
||||
if ($exp instanceof Column) {
|
||||
return $this->columnName($exp->getName());
|
||||
} else if ($exp instanceof Query) {
|
||||
return "(" . $exp->build($params) . ")";
|
||||
} else if ($exp instanceof CaseWhen) {
|
||||
$condition = $this->buildCondition($exp->getCondition(), $params);
|
||||
|
||||
// psql requires constant values here
|
||||
$trueCase = $this->addValue($exp->getTrueCase(), $params, true);
|
||||
$falseCase = $this->addValue($exp->getFalseCase(), $params, true);
|
||||
|
||||
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
|
||||
} else if ($exp instanceof Sum) {
|
||||
$value = $this->addValue($exp->getValue(), $params);
|
||||
$alias = $this->columnName($exp->getAlias());
|
||||
return "SUM($value) AS $alias";
|
||||
} else {
|
||||
$this->lastError = $this->logger->error("Unsupported expression type: " . get_class($exp));
|
||||
return null;
|
||||
return $this->addValue($condition, $params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,8 +320,6 @@ abstract class SQL {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public abstract function getStatus();
|
||||
|
||||
public function parseBool($val) : bool {
|
||||
return in_array($val, array(true, 1, '1', 't', 'true', 'TRUE'), true);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use Core\Driver\SQL\Column\Column;
|
||||
|
||||
class CurrentColumn extends Column {
|
||||
|
||||
public function __construct(string $string) {
|
||||
parent::__construct($string);
|
||||
public function __construct(string $name) {
|
||||
parent::__construct($name);
|
||||
}
|
||||
}
|
@ -2,10 +2,23 @@
|
||||
|
||||
namespace Core\Driver\SQL\Type;
|
||||
|
||||
use Core\Driver\SQL\Column\StringColumn;
|
||||
use Core\Driver\SQL\Expression\Expression;
|
||||
use Core\Driver\SQL\MySQL;
|
||||
use Core\Driver\SQL\PostgreSQL;
|
||||
use Core\Driver\SQL\SQL;
|
||||
|
||||
class CurrentTable extends Expression {
|
||||
|
||||
class CurrentTable extends StringColumn {
|
||||
public function __construct() {
|
||||
parent::__construct("CURRENT_TABLE");
|
||||
}
|
||||
|
||||
function getExpression(SQL $sql, array &$params): string {
|
||||
if ($sql instanceof MySQL) {
|
||||
// CURRENT_TABLE
|
||||
} else if ($sql instanceof PostgreSQL) {
|
||||
return "TG_TABLE_NAME";
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -12,4 +12,7 @@ return [
|
||||
"loading" => "Laden",
|
||||
"logout" => "Ausloggen",
|
||||
"noscript" => "Sie müssen Javascript aktivieren um diese Anwendung zu benutzen",
|
||||
|
||||
# data table
|
||||
"showing_x_of_y_entries" => "Zeige %d von %d Einträgen",
|
||||
];
|
@ -12,4 +12,7 @@ return [
|
||||
"loading" => "Loading",
|
||||
"logout" => "Logout",
|
||||
"noscript" => "You need Javascript enabled to run this app",
|
||||
|
||||
# data table
|
||||
"showing_x_of_y_entries" => "Showing %d of %d entries",
|
||||
];
|
@ -2,10 +2,14 @@
|
||||
|
||||
namespace Core\Objects\DatabaseEntity\Controller;
|
||||
|
||||
use ArrayAccess;
|
||||
use Core\Driver\SQL\Condition\Condition;
|
||||
use Core\Driver\SQL\Expression\Count;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\Transient;
|
||||
use JsonSerializable;
|
||||
|
||||
abstract class DatabaseEntity {
|
||||
abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
|
||||
|
||||
protected static array $entityLogConfig = [
|
||||
"insert" => false,
|
||||
@ -16,11 +20,38 @@ abstract class DatabaseEntity {
|
||||
|
||||
private static array $handlers = [];
|
||||
protected ?int $id;
|
||||
#[Transient] protected array $customData = [];
|
||||
|
||||
public function __construct(?int $id = null) {
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool {
|
||||
return property_exists($this, $offset) || array_key_exists($offset, $this->customData);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed {
|
||||
if (property_exists($this, $offset)) {
|
||||
return $this->{$offset};
|
||||
} else {
|
||||
return $this->customData[$offset];
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void {
|
||||
if (property_exists($this, $offset)) {
|
||||
$this->{$offset} = $value;
|
||||
} else {
|
||||
$this->customData[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void {
|
||||
if (array_key_exists($offset, $this->customData)) {
|
||||
unset($this->customData[$offset]);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract function jsonSerialize(): array;
|
||||
|
||||
public function preInsert(array &$row) { }
|
||||
@ -49,7 +80,7 @@ abstract class DatabaseEntity {
|
||||
|
||||
public static function exists(SQL $sql, int $id): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$res = $sql->select($sql->count())
|
||||
$res = $sql->select(new Count())
|
||||
->from($handler->getTableName())
|
||||
->whereEq($handler->getTableName() . ".id", $id)
|
||||
->execute();
|
||||
@ -148,7 +179,7 @@ abstract class DatabaseEntity {
|
||||
|
||||
public static function count(SQL $sql, ?Condition $condition = null): int|bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$query = $sql->select($sql->count())
|
||||
$query = $sql->select(new Count())
|
||||
->from($handler->getTableName());
|
||||
|
||||
if ($condition) {
|
||||
|
@ -269,7 +269,7 @@ class DatabaseEntityHandler implements Persistable {
|
||||
return $rel_row;
|
||||
}
|
||||
|
||||
private function getValueFromRow(array $row, string $propertyName, mixed &$value): bool {
|
||||
private function getValueFromRow(array $row, string $propertyName, mixed &$value, bool $initEntities = false): bool {
|
||||
$column = $this->columns[$propertyName] ?? null;
|
||||
if (!$column) {
|
||||
return false;
|
||||
@ -290,8 +290,12 @@ class DatabaseEntityHandler implements Persistable {
|
||||
if (array_key_exists($relColumnPrefix . "id", $row)) {
|
||||
$relId = $row[$relColumnPrefix . "id"];
|
||||
if ($relId !== null) {
|
||||
if ($initEntities) {
|
||||
$relationHandler = $this->relations[$propertyName];
|
||||
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
|
||||
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (!$column->notNull()) {
|
||||
$value = null;
|
||||
} else {
|
||||
@ -305,7 +309,7 @@ class DatabaseEntityHandler implements Persistable {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function entityFromRow(array $row): ?DatabaseEntity {
|
||||
public function entityFromRow(array $row, array $additionalColumns = [], bool $initEntities = false): ?DatabaseEntity {
|
||||
try {
|
||||
|
||||
$constructorClass = $this->entityClass;
|
||||
@ -324,12 +328,18 @@ class DatabaseEntityHandler implements Persistable {
|
||||
}
|
||||
|
||||
foreach ($this->properties as $property) {
|
||||
if ($this->getValueFromRow($row, $property->getName(), $value)) {
|
||||
if ($this->getValueFromRow($row, $property->getName(), $value, $initEntities)) {
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($entity, $value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($additionalColumns as $column) {
|
||||
if (!in_array($column, $this->columns) && !isset($this->properties[$column])) {
|
||||
$entity[$column] = $row[$column];
|
||||
}
|
||||
}
|
||||
|
||||
// init n:m / 1:n properties with empty arrays
|
||||
foreach ($this->nmRelations as $nmRelation) {
|
||||
foreach ($nmRelation->getProperties($this) as $property) {
|
||||
@ -453,6 +463,8 @@ class DatabaseEntityHandler implements Persistable {
|
||||
if ($recursive) {
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($this->relations as $propertyName => $relHandler) {
|
||||
$property = $this->properties[$propertyName];
|
||||
if ($property->isInitialized($entity) || true) {
|
||||
$relEntity = $this->properties[$propertyName]->getValue($entity);
|
||||
if ($relEntity) {
|
||||
$relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true);
|
||||
@ -460,6 +472,7 @@ class DatabaseEntityHandler implements Persistable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->nmRelations)) {
|
||||
return;
|
||||
@ -483,10 +496,10 @@ class DatabaseEntityHandler implements Persistable {
|
||||
->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id"))
|
||||
->where(new CondIn(new Column($thisIdColumn), $entityIds));
|
||||
|
||||
$relEntityQuery->addColumn($thisIdColumn);
|
||||
$relEntityQuery->addSelectValue(new Column($thisIdColumn));
|
||||
foreach ($dataColumns as $tableDataColumns) {
|
||||
foreach ($tableDataColumns as $columnName) {
|
||||
$relEntityQuery->addColumn($columnName);
|
||||
$relEntityQuery->addSelectValue(new Column($columnName));
|
||||
}
|
||||
}
|
||||
|
||||
@ -500,7 +513,7 @@ class DatabaseEntityHandler implements Persistable {
|
||||
foreach ($rows as $row) {
|
||||
$relId = $row["id"];
|
||||
if (!isset($relEntities[$relId])) {
|
||||
$relEntity = $otherHandler->entityFromRow($row);
|
||||
$relEntity = $otherHandler->entityFromRow($row, [], $recursive);
|
||||
$relEntities[$relId] = $relEntity;
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,11 @@
|
||||
namespace Core\Objects\DatabaseEntity\Controller;
|
||||
|
||||
use Core\Driver\Logger\Logger;
|
||||
use Core\Driver\SQL\Column\Column;
|
||||
use Core\Driver\SQL\Expression\Alias;
|
||||
use Core\Driver\SQL\Query\Select;
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\External\PHPMailer\Exception;
|
||||
|
||||
/**
|
||||
* this class is similar to \Driver\SQL\Query\Select but with reduced functionality
|
||||
@ -20,6 +23,7 @@ class DatabaseEntityQuery extends Select {
|
||||
private DatabaseEntityHandler $handler;
|
||||
private int $resultType;
|
||||
private bool $logVerbose;
|
||||
private array $additionalColumns;
|
||||
|
||||
private int $fetchSubEntities;
|
||||
|
||||
@ -29,6 +33,7 @@ class DatabaseEntityQuery extends Select {
|
||||
$this->logger = new Logger("DB-EntityQuery", $handler->getSQL());
|
||||
$this->resultType = $resultType;
|
||||
$this->logVerbose = false;
|
||||
$this->additionalColumns = [];
|
||||
|
||||
$this->from($handler->getTableName());
|
||||
$this->fetchSubEntities = self::FETCH_NONE;
|
||||
@ -37,6 +42,26 @@ class DatabaseEntityQuery extends Select {
|
||||
}
|
||||
}
|
||||
|
||||
public function addCustomValue(mixed $selectValue): Select {
|
||||
if (is_string($selectValue)) {
|
||||
$this->additionalColumns[] = $selectValue;
|
||||
} else if ($selectValue instanceof Alias) {
|
||||
$this->additionalColumns[] = $selectValue->getAlias();
|
||||
} else if ($selectValue instanceof Column) {
|
||||
$this->additionalColumns[] = $selectValue->getName();
|
||||
} else {
|
||||
$this->logger->debug("Cannot get selected column name from custom value of type: " . get_class($selectValue));
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->addSelectValue($selectValue);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHandler(): DatabaseEntityHandler {
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function debug(): DatabaseEntityQuery {
|
||||
$this->logVerbose = true;
|
||||
return $this;
|
||||
@ -112,7 +137,7 @@ class DatabaseEntityQuery extends Select {
|
||||
if ($this->resultType === SQL::FETCH_ALL) {
|
||||
$entities = [];
|
||||
foreach ($res as $row) {
|
||||
$entity = $this->handler->entityFromRow($row);
|
||||
$entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE);
|
||||
if ($entity) {
|
||||
$entities[$entity->getId()] = $entity;
|
||||
}
|
||||
@ -124,7 +149,7 @@ class DatabaseEntityQuery extends Select {
|
||||
|
||||
return $entities;
|
||||
} else if ($this->resultType === SQL::FETCH_ONE) {
|
||||
$entity = $this->handler->entityFromRow($res);
|
||||
$entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE);
|
||||
if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) {
|
||||
$this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE);
|
||||
}
|
||||
|
@ -104,6 +104,8 @@ class NMRelation implements Persistable {
|
||||
}
|
||||
|
||||
public static function buildTableName(string ...$tables): string {
|
||||
// in case of class passed here
|
||||
$tables = array_map(function ($t) { return isClass($t) ? getClassName($t) : $t; }, $tables);
|
||||
sort($tables);
|
||||
return "NM_" . implode("_", $tables);
|
||||
}
|
||||
|
@ -2,8 +2,11 @@
|
||||
|
||||
namespace Core\Objects\DatabaseEntity;
|
||||
|
||||
use Core\Driver\SQL\SQL;
|
||||
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
|
||||
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
|
||||
use Core\Objects\DatabaseEntity\Controller\NMRelation;
|
||||
|
||||
class Group extends DatabaseEntity {
|
||||
|
||||
@ -33,4 +36,11 @@ class Group extends DatabaseEntity {
|
||||
"color" => $this->color
|
||||
];
|
||||
}
|
||||
|
||||
public function getMembers(SQL $sql): array {
|
||||
$nmTable = NMRelation::buildTableName(User::class, Group::class);
|
||||
return User::findBy(User::createBuilder($sql, false)
|
||||
->innerJoin($nmTable, "user_id", "User.id")
|
||||
->whereEq("group_id", $this->id));
|
||||
}
|
||||
}
|
@ -227,7 +227,7 @@ function getClassPath($class, string $suffix = ".class"): string {
|
||||
if ($pathCount >= 3) {
|
||||
if (strcasecmp($pathParts[$pathCount - 3], "API") === 0) {
|
||||
$group = $pathParts[$pathCount - 2];
|
||||
if (strcasecmp($group, "Parameter") !== 0) {
|
||||
if (strcasecmp($group, "Parameter") !== 0 && strcasecmp($group, "Traits") !== 0) {
|
||||
$pathParts = array_slice($pathParts, 0, $pathCount - 2);
|
||||
$pathParts[] = "${group}API";
|
||||
}
|
||||
|
@ -11,7 +11,11 @@ import {LocaleContext} from "shared/locale";
|
||||
import './res/adminlte.min.css';
|
||||
|
||||
// views
|
||||
import View404 from "./views/404";
|
||||
const Overview = lazy(() => import('./views/overview'));
|
||||
const UserListView = lazy(() => import('./views/user-list'));
|
||||
const GroupListView = lazy(() => import('./views/group-list'));
|
||||
const EditGroupView = lazy(() => import('./views/group-edit'));
|
||||
|
||||
|
||||
export default function AdminDashboard(props) {
|
||||
@ -22,14 +26,21 @@ export default function AdminDashboard(props) {
|
||||
|
||||
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
|
||||
|
||||
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
|
||||
setDialog({ show: true, message: message, title: title, options: options, onOption: onOption });
|
||||
}, []);
|
||||
|
||||
const hideDialog = useCallback(() => {
|
||||
setDialog({show: false});
|
||||
}, []);
|
||||
|
||||
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
|
||||
setDialog({
|
||||
show: true, message:
|
||||
message,
|
||||
title: title,
|
||||
options: options,
|
||||
onOption: onOption,
|
||||
onClose: hideDialog
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(api, ["general", "admin"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
@ -52,7 +63,12 @@ export default function AdminDashboard(props) {
|
||||
<section className={"content"}>
|
||||
<Suspense fallback={<div>{L("general.loading")}... </div>}>
|
||||
<Routes>
|
||||
<Route path={"/admin"} element={<Overview {...controlObj} />}/>
|
||||
<Route path={"/admin/dashboard"} element={<Overview {...controlObj} />}/>
|
||||
<Route path={"/admin/users"} element={<UserListView {...controlObj} />}/>
|
||||
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
||||
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
||||
<Route path={"*"} element={<View404 />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{/*<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
|
@ -85,8 +85,6 @@ export default function App() {
|
||||
|
||||
*/
|
||||
|
||||
console.log(loaded, user, api.loggedIn);
|
||||
|
||||
if (!loaded) {
|
||||
if (error) {
|
||||
return <Alert severity={"error"} title={L("general.error_occurred")}>
|
||||
|
@ -92,7 +92,9 @@ export default function Sidebar(props) {
|
||||
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
||||
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
||||
<div className="info">
|
||||
<a href="#" className="d-block">Logged in as: {api.user.name}</a>
|
||||
<a href="#" className="d-block">{L("account.logged_in_as")}:
|
||||
<Link to={"/admin/user/" + api.user.id}>{api.user.name}</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={"mt-2"}>
|
||||
|
5
react/admin-panel/src/views/404.js
Normal file
5
react/admin-panel/src/views/404.js
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export default function View404(props) {
|
||||
return <b>Not found</b>
|
||||
}
|
35
react/admin-panel/src/views/group-edit.js
Normal file
35
react/admin-panel/src/views/group-edit.js
Normal file
@ -0,0 +1,35 @@
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
|
||||
|
||||
export default function EditGroupView(props) {
|
||||
|
||||
// const [groupId, setGroupId] = useState(props?.match?.groupId !== "new" ? parseInt(props.match.groupId) : null);
|
||||
|
||||
const { groupId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [fetchGroup, setFetchGroup] = useState(groupId !== "new");
|
||||
const [group, setGroup] = useState(null);
|
||||
|
||||
const onFetchGroup = useCallback((force = false) => {
|
||||
if (force || fetchGroup) {
|
||||
setFetchGroup(false);
|
||||
props.api.getGroup(groupId).then(res => {
|
||||
if (!res.success) {
|
||||
props.showDialog(res.msg, "Error fetching group");
|
||||
navigate("/admin/groups");
|
||||
} else {
|
||||
setGroup(res.group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onFetchGroup();
|
||||
}, []);
|
||||
|
||||
return <></>
|
||||
|
||||
}
|
80
react/admin-panel/src/views/group-list.js
Normal file
80
react/admin-panel/src/views/group-list.js
Normal file
@ -0,0 +1,80 @@
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Button, IconButton} from "@material-ui/core";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
|
||||
export default function GroupListView(props) {
|
||||
|
||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
alert(data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onFetchGroups = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||
let res = await props.api.fetchGroups(page, count, orderBy, sortOrder);
|
||||
if (res.success) {
|
||||
return Promise.resolve([res.groups, res.pagination]);
|
||||
} else {
|
||||
props.showAlert("Error fetching groups", res.msg);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const actionColumn = (() => {
|
||||
let column = new DataColumn(L("general.actions"), null, false);
|
||||
column.renderData = (entry) => <>
|
||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/group/" + entry.id)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
return column;
|
||||
})();
|
||||
|
||||
const columnDefinitions = [
|
||||
new NumericColumn(L("general.id"), "id"),
|
||||
new StringColumn(L("group.name"), "name"),
|
||||
new NumericColumn(L("group.member_count"), "memberCount"),
|
||||
actionColumn,
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>Users</h1>
|
||||
</div>
|
||||
<div className={"col-sm-6"}>
|
||||
<ol className={"breadcrumb float-sm-right"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active">Groups</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<Link to="/admin/group/new">
|
||||
<Button variant={"outlined"} startIcon={<AddIcon />} size={"small"}>
|
||||
{L("general.create_new")}
|
||||
</Button>
|
||||
</Link>
|
||||
<DataTable className={"table table-striped"}
|
||||
fetchData={onFetchGroups}
|
||||
placeholder={"No groups to display"}
|
||||
columns={columnDefinitions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
@ -1,9 +1,33 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {format, getDaysInMonth} from "date-fns";
|
||||
import {Collapse} from "react-collapse";
|
||||
import {Bar} from "react-chartjs-2";
|
||||
import {CircularProgress, Icon} from "@material-ui/core";
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
|
||||
export default function Overview(props) {
|
||||
|
||||
const [fetchStats, setFetchStats] = useState(true);
|
||||
const [stats, setStats] = useState(null);
|
||||
|
||||
const onFetchStats = useCallback((force = false) => {
|
||||
if (force || fetchStats) {
|
||||
setFetchStats(false);
|
||||
props.api.getStats().then((res) => {
|
||||
if (res.success) {
|
||||
setStats(res.data);
|
||||
} else {
|
||||
props.showDialog("Error fetching stats: " + res.msg, "Error fetching stats");
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [fetchStats]);
|
||||
|
||||
useEffect(() => {
|
||||
onFetchStats();
|
||||
}, []);
|
||||
|
||||
const today = new Date();
|
||||
const numDays = getDaysInMonth(today);
|
||||
|
||||
@ -46,6 +70,8 @@ export default function Overview(props) {
|
||||
}
|
||||
*/
|
||||
|
||||
console.log(stats);
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
@ -63,57 +89,28 @@ export default function Overview(props) {
|
||||
</div>
|
||||
</div>
|
||||
<section className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className="small-box bg-info">
|
||||
<div className={"inner"}>
|
||||
{stats ?
|
||||
<>
|
||||
<h3>{stats.userCount}</h3>
|
||||
<p>Users registered</p>
|
||||
</> : <CircularProgress variant={"determinate"} />
|
||||
}
|
||||
</div>
|
||||
<div className="icon">
|
||||
<Icon icon={"users"} />
|
||||
</div>
|
||||
<Link to={"/admin/users"} className="small-box-footer">
|
||||
More info <Icon icon={"arrow-circle-right"}/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
/*
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartVisible : true,
|
||||
statusVisible : true,
|
||||
userCount: 0,
|
||||
notificationCount: 0,
|
||||
visitorsTotal: 0,
|
||||
visitors: { },
|
||||
server: { load_avg: ["Unknown"] },
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
|
||||
removeError(i) {
|
||||
if (i >= 0 && i < this.state.errors.length) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.splice(i, 1);
|
||||
this.setState({...this.state, errors: errors});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getStats().then((res) => {
|
||||
if(!res.success) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ message: res.msg, title: "Error fetching Stats" });
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
} else {
|
||||
this.setState({
|
||||
...this.state,
|
||||
userCount: res.userCount,
|
||||
pageCount: res.pageCount,
|
||||
visitors: res.visitors,
|
||||
visitorsTotal: res.visitorsTotal,
|
||||
server: res.server
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
}
|
||||
}*/
|
90
react/admin-panel/src/views/user-list.js
Normal file
90
react/admin-panel/src/views/user-list.js
Normal file
@ -0,0 +1,90 @@
|
||||
import {Link, Navigate, useNavigate} from "react-router-dom";
|
||||
import {useCallback, useContext, useEffect} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Button, IconButton} from "@material-ui/core";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import {Chip} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
|
||||
export default function UserListView(props) {
|
||||
|
||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog("Error fetching translations: " + data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onFetchUsers = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||
let res = await props.api.fetchUsers(page, count, orderBy, sortOrder);
|
||||
if (res.success) {
|
||||
return Promise.resolve([res.users, res.pagination]);
|
||||
} else {
|
||||
props.showDialog(res.msg, "Error fetching users");
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const groupColumn = (() => {
|
||||
let column = new DataColumn(L("account.groups"), "groups");
|
||||
column.renderData = (entry) => {
|
||||
return Object.values(entry.groups).map(group => <Chip key={"group-" + group.id} label={group.name}/>)
|
||||
}
|
||||
return column;
|
||||
})();
|
||||
|
||||
const actionColumn = (() => {
|
||||
let column = new DataColumn(L("general.actions"), null, false);
|
||||
column.renderData = (entry) => <>
|
||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/user/" + entry.id)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
return column;
|
||||
})();
|
||||
|
||||
const columnDefinitions = [
|
||||
new NumericColumn(L("general.id"), "id"),
|
||||
new StringColumn(L("account.username"), "name"),
|
||||
new StringColumn(L("account.email"), "email"),
|
||||
groupColumn,
|
||||
new StringColumn(L("account.full_name"), "full_name"),
|
||||
actionColumn,
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>Users</h1>
|
||||
</div>
|
||||
<div className={"col-sm-6"}>
|
||||
<ol className={"breadcrumb float-sm-right"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active">Users</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<Link to="/admin/user/new">
|
||||
<Button variant={"outlined"} startIcon={<AddIcon />} size={"small"}>
|
||||
{L("general.create_new")}
|
||||
</Button>
|
||||
</Link>
|
||||
<DataTable className={"table table-striped"}
|
||||
fetchData={onFetchUsers}
|
||||
placeholder={"No users to display"} columns={columnDefinitions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
@ -32,9 +32,12 @@
|
||||
"maxParallelRequests": 1
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/material": "^5.11.3",
|
||||
"chart.js": "^4.0.1",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.29.3",
|
||||
@ -43,7 +46,8 @@
|
||||
"react-chartjs-2": "^5.0.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.3"
|
||||
"react-router-dom": "^6.4.3",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -19,7 +19,7 @@ export default class API {
|
||||
|
||||
let res = await response.json();
|
||||
if (!res.success && res.msg === "You are not logged in.") {
|
||||
document.location.reload();
|
||||
this.loggedIn = false;
|
||||
}
|
||||
|
||||
return res;
|
||||
@ -69,12 +69,16 @@ export default class API {
|
||||
return this.apiCall("user/delete", { id: id });
|
||||
}
|
||||
|
||||
async fetchUsers(pageNum = 1, count = 20) {
|
||||
return this.apiCall("user/fetch", { page: pageNum, count: count });
|
||||
async fetchUsers(pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc') {
|
||||
return this.apiCall("user/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||
}
|
||||
|
||||
async fetchGroups(pageNum = 1, count = 20) {
|
||||
return this.apiCall("groups/fetch", { page: pageNum, count: count });
|
||||
async fetchGroups(pageNum = 1, count = 20, orderBy = 'id', sortOrder = 'asc') {
|
||||
return this.apiCall("groups/fetch", { page: pageNum, count: count, orderBy: orderBy, sortOrder: sortOrder });
|
||||
}
|
||||
|
||||
async getGroup(id) {
|
||||
return this.apiCall("groups/get", { id: id });
|
||||
}
|
||||
|
||||
async inviteUser(username, email) {
|
||||
|
20
react/shared/elements/data-table.css
Normal file
20
react/shared/elements/data-table.css
Normal file
@ -0,0 +1,20 @@
|
||||
.data-table {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.data-table td, .data-table th {
|
||||
padding: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
323
react/shared/elements/data-table.js
Normal file
323
react/shared/elements/data-table.js
Normal file
@ -0,0 +1,323 @@
|
||||
import {Table, TableBody, TableCell, TableHead, TableRow} from "@material-ui/core";
|
||||
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
|
||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||
import usePagination from "../hooks/pagination";
|
||||
import {parse} from "date-fns";
|
||||
import "./data-table.css";
|
||||
import {LocaleContext} from "../locale";
|
||||
import clsx from "clsx";
|
||||
import {Box} from "@mui/material";
|
||||
|
||||
|
||||
export function DataTable(props) {
|
||||
|
||||
const { className, placeholder,
|
||||
fetchData, onClick, onFilter,
|
||||
defaultSortColumn, defaultSortOrder,
|
||||
columns, ...other } = props;
|
||||
|
||||
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
|
||||
|
||||
const [doFetchData, setFetchData] = useState(true);
|
||||
const [data, setData] = useState(null);
|
||||
const [sortAscending, setSortAscending] = useState(["asc","ascending"].includes(defaultSortOrder?.toLowerCase));
|
||||
const [sortColumn, setSortColumn] = useState(defaultSortColumn || null);
|
||||
const pagination = usePagination();
|
||||
const sortable = props.hasOwnProperty("sortable") ? !!props.sortable : true;
|
||||
|
||||
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).then(([data, dataPagination]) => {
|
||||
if (data) {
|
||||
setData(data);
|
||||
pagination.update(dataPagination);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [doFetchData, columns, sortColumn, sortAscending, pagination]);
|
||||
|
||||
// pagination changed?
|
||||
useEffect(() => {
|
||||
let forceFetch = (pagination.getPageSize() < pagination.getTotal());
|
||||
onFetchData(forceFetch);
|
||||
}, [pagination.data.pageSize, pagination.data.current]);
|
||||
|
||||
// sorting changed
|
||||
useEffect(() => {
|
||||
onFetchData(true);
|
||||
}, [sortAscending, sortColumn]);
|
||||
|
||||
let headerRow = [];
|
||||
const onChangeSort = useCallback((index, column) => {
|
||||
if (sortable && column.sortable) {
|
||||
if (sortColumn === index) {
|
||||
setSortAscending(!sortAscending);
|
||||
} else {
|
||||
setSortColumn(index);
|
||||
}
|
||||
}
|
||||
}, [onFetchData, sortColumn, sortAscending]);
|
||||
|
||||
for (const [index, column] of columns.entries()) {
|
||||
if (!(column instanceof DataColumn)) {
|
||||
throw new Error("DataTable can only have DataColumn-objects as column definition, got: " + typeof column);
|
||||
}
|
||||
|
||||
if (sortable && column.sortable) {
|
||||
headerRow.push(<TableCell key={"col-" + index} className={"sortable"}
|
||||
title={L("general.sort_by") + ": " + column.label}
|
||||
onClick={() => onChangeSort(index, column) }>
|
||||
{sortColumn === index ? (sortAscending ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />): <></>}{column.renderHead(index)}
|
||||
</TableCell>);
|
||||
} else {
|
||||
headerRow.push(<TableCell key={"col-" + index}>
|
||||
{column.renderHead(index)}
|
||||
</TableCell>);
|
||||
}
|
||||
}
|
||||
|
||||
const numColumns = columns.length;
|
||||
let rows = [];
|
||||
if (data) {
|
||||
for (const [key, entry] of Object.entries(data)) {
|
||||
let row = [];
|
||||
for (const [index, column] of columns.entries()) {
|
||||
row.push(<TableCell key={"col-" + index}>{column.renderData(entry)}</TableCell>);
|
||||
}
|
||||
|
||||
rows.push(<TableRow key={"row-" + key}>{ row }</TableRow>);
|
||||
}
|
||||
} else if (placeholder) {
|
||||
rows.push(<TableRow key={"row-placeholder"}>
|
||||
<TableCell colSpan={numColumns}>
|
||||
{ placeholder }
|
||||
</TableCell>
|
||||
</TableRow>);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
|
||||
let columnElements = [];
|
||||
if (columns) {
|
||||
for (const [key, column] of Object.entries(columns)) {
|
||||
const centered = column.alignment === "center";
|
||||
const sortable = doSort && (!column.hasOwnProperty("sortable") || !!column.sortable);
|
||||
const label = column.label;
|
||||
|
||||
if (!sortable) {
|
||||
columnElements.push(
|
||||
<TableCell key={"column-" + key} className={clsx(centered && classes.columnCenter)}>
|
||||
{ label }
|
||||
</TableCell>
|
||||
);
|
||||
} else {
|
||||
columnElements.push(
|
||||
<TableCell key={"column-" + key} label={L("Sort By") + ": " + label} className={clsx(classes.clickable, centered && classes.columnCenter)}
|
||||
onClick={() => (key === sortColumn ? setSortAscending(!sortAscending) : setSortColumn(key)) }>
|
||||
{ key === sortColumn ?
|
||||
<Grid container alignItems={"center"} spacing={1} direction={"row"} className={classes.gridSorted}>
|
||||
<Grid item>{ sortAscending ? <ArrowUpwardIcon fontSize={"small"} /> : <ArrowDownwardIcon fontSize={"small"} /> }</Grid>
|
||||
<Grid item>{ label }</Grid>
|
||||
<Grid item/>
|
||||
</Grid> :
|
||||
<span><i/>{label}</span>
|
||||
}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getValue = useCallback((entry, key) => {
|
||||
if (typeof columns[key]?.value === 'function') {
|
||||
return columns[key].value(entry);
|
||||
} else {
|
||||
return entry[columns[key]?.value] ?? null;
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
let numColumns = columns ? Object.keys(columns).length : 0;
|
||||
|
||||
const compare = (a,b,callback) => {
|
||||
let definedA = a !== null && typeof a !== 'undefined';
|
||||
let definedB = b !== null && typeof b !== 'undefined';
|
||||
if (!definedA && !definedB) {
|
||||
return 0;
|
||||
} else if (!definedA) {
|
||||
return 1;
|
||||
} else if (!definedB) {
|
||||
return -1;
|
||||
} else {
|
||||
return callback(a,b);
|
||||
}
|
||||
}
|
||||
|
||||
let rows = [];
|
||||
const hasClickHandler = typeof onClick === 'function';
|
||||
if (data !== null && columns) {
|
||||
let hidden = 0;
|
||||
let sortedEntries = data.slice();
|
||||
|
||||
if (sortColumn && columns[sortColumn]) {
|
||||
let sortFunction;
|
||||
if (typeof columns[sortColumn]?.compare === 'function') {
|
||||
sortFunction = columns[sortColumn].compare;
|
||||
} else if (columns[sortColumn]?.type === Date) {
|
||||
sortFunction = (a, b) => compare(a, b, (a,b) => a.getTime() - b.getTime());
|
||||
} else if (columns[sortColumn]?.type === Number) {
|
||||
sortFunction = (a, b) => compare(a, b, (a,b) => a - b);
|
||||
} else {
|
||||
sortFunction = ((a, b) =>
|
||||
compare(a, b, (a,b) => a.toString().toLowerCase().localeCompare(b.toString().toLowerCase()))
|
||||
)
|
||||
}
|
||||
|
||||
sortedEntries.sort((a, b) => {
|
||||
let entryA = getValue(a, sortColumn);
|
||||
let entryB = getValue(b, sortColumn);
|
||||
return sortFunction(entryA, entryB);
|
||||
});
|
||||
|
||||
if (!sortAscending) {
|
||||
sortedEntries = sortedEntries.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
Array.from(Array(sortedEntries.length).keys()).forEach(rowIndex => {
|
||||
if (typeof props.filter === 'function' && !props.filter(sortedEntries[rowIndex])) {
|
||||
hidden++;
|
||||
return;
|
||||
}
|
||||
|
||||
let rowData = [];
|
||||
for (const [key, column] of Object.entries(columns)) {
|
||||
let value = getValue(sortedEntries[rowIndex], key);
|
||||
if (typeof column.render === 'function') {
|
||||
value = column.render(sortedEntries[rowIndex], value);
|
||||
}
|
||||
|
||||
rowData.push(<TableCell key={"column-" + key} className={clsx(column.alignment === "center" && classes.columnCenter)}>
|
||||
{ value }
|
||||
</TableCell>);
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<TableRow key={"entry-" + rowIndex}
|
||||
className={clsx(hasClickHandler && classes.clickable)}
|
||||
onClick={() => hasClickHandler && onClick(sortedEntries[rowIndex])}>
|
||||
{ rowData }
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
if (hidden > 0) {
|
||||
rows.push(<TableRow key={"row-hidden"}>
|
||||
<TableCell colSpan={numColumns} className={classes.hidden}>
|
||||
{ "(" + (hidden > 1
|
||||
? sprintf(L("%d rows hidden due to filter"), hidden)
|
||||
: L("1 rows hidden due to filter")) + ")"
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>);
|
||||
} else if (rows.length === 0 && placeholder) {
|
||||
rows.push(<TableRow key={"row-placeholder"}>
|
||||
<TableCell colSpan={numColumns} className={classes.hidden}>
|
||||
{ placeholder }
|
||||
</TableCell>
|
||||
</TableRow>);
|
||||
}
|
||||
} else if (columns && data === null) {
|
||||
rows.push(<TableRow key={"loading"}>
|
||||
<TableCell colSpan={numColumns} className={classes.columnCenter}>
|
||||
<Grid container alignItems={"center"} spacing={1} justifyContent={"center"}>
|
||||
<Grid item>{L("Loading")}…</Grid>
|
||||
<Grid item><CircularProgress size={15}/></Grid>
|
||||
</Grid>
|
||||
</TableCell>
|
||||
</TableRow>)
|
||||
}
|
||||
*/
|
||||
|
||||
return <Box position={"relative"}>
|
||||
<Table className={clsx("data-table", className)} size="small" {...other}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{ headerRow }
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ rows }
|
||||
</TableBody>
|
||||
</Table>
|
||||
{pagination.renderPagination(L, rows.length)}
|
||||
</Box>
|
||||
}
|
||||
|
||||
export class DataColumn {
|
||||
constructor(label, field = null, sortable = true) {
|
||||
this.label = label;
|
||||
this.field = field;
|
||||
this.sortable = sortable;
|
||||
}
|
||||
|
||||
compare(a, b) {
|
||||
throw new Error("Not implemented: compare");
|
||||
}
|
||||
|
||||
renderData(entry) {
|
||||
return entry[this.field]
|
||||
}
|
||||
|
||||
renderHead() {
|
||||
return this.label;
|
||||
}
|
||||
}
|
||||
|
||||
export class StringColumn extends DataColumn {
|
||||
constructor(label, field = null, sortable = true, caseSensitive = false) {
|
||||
super(label, field, sortable);
|
||||
this.caseSensitve = caseSensitive;
|
||||
}
|
||||
|
||||
compare(a, b) {
|
||||
if (this.caseSensitve) {
|
||||
return a.toString().localeCompare(b.toString());
|
||||
} else {
|
||||
return a.toString().toLowerCase().localeCompare(b.toString().toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NumericColumn extends DataColumn {
|
||||
constructor(label, field = null, sortable = true) {
|
||||
super(label, field, sortable);
|
||||
}
|
||||
|
||||
compare(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
}
|
||||
|
||||
export class DateTimeColumn extends DataColumn {
|
||||
constructor(label, field = null, sortable = true, format = "YYYY-MM-dd HH:mm:ss") {
|
||||
super(label, field, sortable);
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
compare(a, b) {
|
||||
if (typeof a === 'string') {
|
||||
a = parse(a, this.format, new Date()).getTime();
|
||||
}
|
||||
|
||||
if (typeof b === 'string') {
|
||||
b = parse(b, this.format, new Date()).getTime();
|
||||
}
|
||||
|
||||
return a - b;
|
||||
}
|
||||
}
|
73
react/shared/hooks/pagination.js
Normal file
73
react/shared/hooks/pagination.js
Normal file
@ -0,0 +1,73 @@
|
||||
import React, {useState} from "react";
|
||||
import {Box, MenuItem, Select, Pagination as MuiPagination} from "@mui/material";
|
||||
import {sprintf} from "sprintf-js";
|
||||
|
||||
class Pagination {
|
||||
|
||||
constructor(data, setData) {
|
||||
this.data = data;
|
||||
this.setData = setData;
|
||||
}
|
||||
|
||||
getPage() {
|
||||
return this.data.current;
|
||||
}
|
||||
|
||||
getPageSize() {
|
||||
return this.data.pageSize;
|
||||
}
|
||||
|
||||
setPage(page) {
|
||||
this.setData({...this.data, current: page});
|
||||
}
|
||||
|
||||
setPageSize(pageSize) {
|
||||
this.setData({...this.data, pageSize: pageSize});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setData({current: 1, pageSize: 25, total: 0});
|
||||
}
|
||||
|
||||
getPageCount() {
|
||||
if (this.data.pageSize && this.data.total) {
|
||||
return Math.max(1, Math.ceil(this.data.total / this.data.pageSize));
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
getTotal() {
|
||||
return this.data.total;
|
||||
}
|
||||
|
||||
update(data) {
|
||||
this.setData(data);
|
||||
}
|
||||
|
||||
renderPagination(L, numEntries, options = null) {
|
||||
options = options || [10, 25, 50, 100];
|
||||
|
||||
return <Box>
|
||||
<Select
|
||||
value={this.data.pageSize}
|
||||
label={L("general.entries_per_page")}
|
||||
onChange={(e) => this.setPageSize(parseInt(e.target.value))}
|
||||
size={"small"}
|
||||
>
|
||||
{options.map(size => <MenuItem key={"size-" + size} value={size}>{size}</MenuItem>)}
|
||||
</Select>
|
||||
<MuiPagination count={this.getPageCount()} onChange={(_, page) => this.setPage(page)} />
|
||||
{sprintf(L("general.showing_x_of_y_entries"), numEntries, this.data.total)}
|
||||
</Box>
|
||||
}
|
||||
}
|
||||
|
||||
export default function usePagination() {
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1, pageSize: 25, total: 0
|
||||
});
|
||||
|
||||
return new Pagination(pagination, setPagination);
|
||||
}
|
@ -1,11 +1,36 @@
|
||||
import React from 'react';
|
||||
import React, {useReducer} from 'react';
|
||||
import {createContext, useCallback, useState} from "react";
|
||||
|
||||
const LocaleContext = createContext(null);
|
||||
|
||||
function reducer(entries, action) {
|
||||
let _entries = entries;
|
||||
|
||||
switch (action.type) {
|
||||
case 'loadModule':
|
||||
if (!_entries.hasOwnProperty(action.code)) {
|
||||
_entries[action.code] = {};
|
||||
}
|
||||
if (_entries[action.code].hasOwnProperty(action.module)) {
|
||||
_entries[action.code][action.module] = {..._entries[action.code][action.module], ...action.newEntries};
|
||||
} else {
|
||||
_entries[action.code][action.module] = action.newEntries;
|
||||
}
|
||||
break;
|
||||
case 'loadModules':
|
||||
_entries = {...entries, [action.code]: { ...entries[action.code], ...action.modules }};
|
||||
break;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return _entries;
|
||||
}
|
||||
|
||||
function LocaleProvider(props) {
|
||||
|
||||
const [entries, setEntries] = useState(window.languageEntries || {});
|
||||
// const [entries, setEntries] = useState(window.languageEntries || {});
|
||||
const [entries, dispatch] = useReducer(reducer, window.languageEntries || {});
|
||||
const [currentLocale, setCurrentLocale] = useState(window.languageCode || "en_US");
|
||||
|
||||
const translate = useCallback((key) => {
|
||||
@ -24,23 +49,6 @@ function LocaleProvider(props) {
|
||||
return "[" + key + "]";
|
||||
}, [currentLocale, entries]);
|
||||
|
||||
const loadModule = useCallback((code, module, newEntries) => {
|
||||
let _entries = {...entries};
|
||||
if (!_entries.hasOwnProperty(code)) {
|
||||
_entries[code] = {};
|
||||
}
|
||||
if (_entries[code].hasOwnProperty(module)) {
|
||||
_entries[code][module] = {..._entries[code][module], ...newEntries};
|
||||
} else {
|
||||
_entries[code][module] = newEntries;
|
||||
}
|
||||
setEntries(_entries);
|
||||
}, [entries]);
|
||||
|
||||
const loadModules = useCallback((code, modules) => {
|
||||
setEntries({...entries, [code]: { ...entries[code], ...modules }});
|
||||
}, [entries]);
|
||||
|
||||
const hasModule = useCallback((code, module) => {
|
||||
return entries.hasOwnProperty(code) && !!entries[code][module];
|
||||
}, [entries]);
|
||||
@ -104,7 +112,7 @@ function LocaleProvider(props) {
|
||||
if (useCache) {
|
||||
if (data && data.success) {
|
||||
// insert into cache
|
||||
loadModules(code, data.entries);
|
||||
dispatch({type: "loadModules", code: code, modules: data.entries});
|
||||
data.entries = {...data.entries, ...languageEntries};
|
||||
data.cached = false;
|
||||
}
|
||||
@ -114,7 +122,7 @@ function LocaleProvider(props) {
|
||||
} else {
|
||||
return { success: true, msg: "", entries: languageEntries, code: code, cached: true };
|
||||
}
|
||||
}, [currentLocale, getModule, loadModules]);
|
||||
}, [currentLocale, getModule, dispatch]);
|
||||
|
||||
const ctx = {
|
||||
currentLocale: currentLocale,
|
||||
|
263
react/yarn.lock
263
react/yarn.lock
@ -162,7 +162,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.18.9"
|
||||
|
||||
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6":
|
||||
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
|
||||
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
|
||||
@ -518,7 +518,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.8.0"
|
||||
|
||||
"@babel/plugin-syntax-jsx@^7.18.6":
|
||||
"@babel/plugin-syntax-jsx@^7.17.12", "@babel/plugin-syntax-jsx@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
|
||||
integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
|
||||
@ -1035,6 +1035,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
|
||||
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.18.10", "@babel/template@^7.3.3":
|
||||
version "7.18.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
|
||||
@ -1185,11 +1192,119 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
|
||||
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
|
||||
|
||||
"@emotion/babel-plugin@^11.10.5":
|
||||
version "11.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c"
|
||||
integrity sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.16.7"
|
||||
"@babel/plugin-syntax-jsx" "^7.17.12"
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/hash" "^0.9.0"
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
babel-plugin-macros "^3.1.0"
|
||||
convert-source-map "^1.5.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
find-root "^1.1.0"
|
||||
source-map "^0.5.7"
|
||||
stylis "4.1.3"
|
||||
|
||||
"@emotion/cache@^11.10.5":
|
||||
version "11.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12"
|
||||
integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/sheet" "^1.2.1"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
"@emotion/weak-memoize" "^0.3.0"
|
||||
stylis "4.1.3"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/hash@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7"
|
||||
integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==
|
||||
|
||||
"@emotion/is-prop-valid@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83"
|
||||
integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
|
||||
"@emotion/memoize@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
|
||||
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
|
||||
|
||||
"@emotion/react@^11.10.5":
|
||||
version "11.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d"
|
||||
integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/babel-plugin" "^11.10.5"
|
||||
"@emotion/cache" "^11.10.5"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
"@emotion/weak-memoize" "^0.3.0"
|
||||
hoist-non-react-statics "^3.3.1"
|
||||
|
||||
"@emotion/serialize@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0"
|
||||
integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==
|
||||
dependencies:
|
||||
"@emotion/hash" "^0.9.0"
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/unitless" "^0.8.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
|
||||
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
|
||||
|
||||
"@emotion/styled@^11.10.5":
|
||||
version "11.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79"
|
||||
integrity sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/babel-plugin" "^11.10.5"
|
||||
"@emotion/is-prop-valid" "^1.2.0"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
|
||||
"@emotion/unitless@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
|
||||
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
|
||||
|
||||
"@emotion/use-insertion-effect-with-fallbacks@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
|
||||
integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
|
||||
|
||||
"@emotion/utils@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
|
||||
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
|
||||
|
||||
"@emotion/weak-memoize@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
|
||||
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
|
||||
|
||||
"@eslint/eslintrc@^1.3.3":
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
|
||||
@ -1666,6 +1781,99 @@
|
||||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz#016d855b6bc459fd908095811f6826e45dd4ba64"
|
||||
integrity sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==
|
||||
|
||||
"@mui/base@5.0.0-alpha.112":
|
||||
version "5.0.0-alpha.112"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.112.tgz#80e815430c5df0316e0a549d34628d54215e05f1"
|
||||
integrity sha512-KPwb1iYPXsV/P8uu0SNQrj7v7YU6wdN4Eccc2lZQyRDW+f6PJYjHBuFUTYKc408B98Jvs1XbC/z5MN45a2DWrQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
"@emotion/is-prop-valid" "^1.2.0"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.2"
|
||||
"@popperjs/core" "^2.11.6"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/core-downloads-tracker@^5.11.3":
|
||||
version "5.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.3.tgz#72c04a9f12b29186877992648d7cf1c22b95ba5c"
|
||||
integrity sha512-Bgb6//KtxY7IC7M5Pa5RKFX1wttc213mqpKqydnz3wn4BGDXfA5c0vf5nTu5zqsJeB4T3ysAJTRJhQ/E1GsZDQ==
|
||||
|
||||
"@mui/icons-material@^5.11.0":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.0.tgz#9ea6949278b2266d2683866069cd43009eaf6464"
|
||||
integrity sha512-I2LaOKqO8a0xcLGtIozC9xoXjZAto5G5gh0FYUMAlbsIHNHIjn4Xrw9rvjY20vZonyiGrZNMAlAXYkY6JvhF6A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.6"
|
||||
|
||||
"@mui/material@^5.11.3":
|
||||
version "5.11.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.11.3.tgz#e1be6d26eecde61878280cb6792797a77696b09a"
|
||||
integrity sha512-Oz+rMFiMtxzzDLUxKyyj4mSxF9ShmsBoJ9qvglXCYqklgSrEl1R/Z4hfPZ+2pWd5CriO8U/0CFHr4DksrlTiCw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
"@mui/base" "5.0.0-alpha.112"
|
||||
"@mui/core-downloads-tracker" "^5.11.3"
|
||||
"@mui/system" "^5.11.2"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.2"
|
||||
"@types/react-transition-group" "^4.4.5"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@mui/private-theming@^5.11.2":
|
||||
version "5.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.2.tgz#93eafb317070888a988efa8d6a9ec1f69183a606"
|
||||
integrity sha512-qZwMaqRFPwlYmqwVKblKBGKtIjJRAj3nsvX93pOmatsXyorW7N/0IPE/swPgz1VwChXhHO75DwBEx8tB+aRMNg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
"@mui/utils" "^5.11.2"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/styled-engine@^5.11.0":
|
||||
version "5.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.0.tgz#79afb30c612c7807c4b77602cf258526d3997c7b"
|
||||
integrity sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.6"
|
||||
"@emotion/cache" "^11.10.5"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@^5.11.2":
|
||||
version "5.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.2.tgz#a5a5865dda0f5f360eed8cdc1ab399a493dbd361"
|
||||
integrity sha512-PPkYhrcP2MkhscX6SauIl0wPgra0w1LGPtll+hIKc2Z2JbGRSrUCFif93kxejB7I1cAoCay9jWW4mnNhsOqF/g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
"@mui/private-theming" "^5.11.2"
|
||||
"@mui/styled-engine" "^5.11.0"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.2"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/types@^7.2.3":
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
|
||||
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
|
||||
|
||||
"@mui/utils@^5.11.2":
|
||||
version "5.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.2.tgz#29764311acb99425159b159b1cb382153ad9be1f"
|
||||
integrity sha512-AyizuHHlGdAtH5hOOXBW3kriuIwUIKUIgg0P7LzMvzf6jPhoQbENYqY6zJqfoZ7fAWMNNYT8mgN5EftNGzwE2w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.7"
|
||||
"@types/prop-types" "^15.7.5"
|
||||
"@types/react-is" "^16.7.1 || ^17.0.0"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||
version "5.1.1-v1"
|
||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
|
||||
@ -2269,6 +2477,11 @@
|
||||
schema-utils "^3.0.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@popperjs/core@^2.11.6":
|
||||
version "2.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@remix-run/router@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.3.tgz#953b88c20ea00d0eddaffdc1b115c08474aa295d"
|
||||
@ -2646,7 +2859,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
|
||||
integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
|
||||
|
||||
"@types/prop-types@*":
|
||||
"@types/prop-types@*", "@types/prop-types@^15.7.5":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
@ -2666,7 +2879,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-transition-group@^4.2.0":
|
||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.2.0", "@types/react-transition-group@^4.4.5":
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
|
||||
integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
|
||||
@ -3852,7 +4072,7 @@ content-type@~1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
|
||||
convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
|
||||
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
|
||||
@ -4123,7 +4343,7 @@ csstype@^2.5.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
|
||||
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
|
||||
|
||||
csstype@^3.0.2:
|
||||
csstype@^3.0.2, csstype@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
@ -5036,6 +5256,11 @@ find-cache-dir@^3.3.1:
|
||||
make-dir "^3.0.2"
|
||||
pkg-dir "^4.1.0"
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
|
||||
|
||||
find-up@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
|
||||
@ -5365,7 +5590,7 @@ he@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hoist-non-react-statics@^3.3.2:
|
||||
hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
@ -5620,6 +5845,11 @@ ini@^1.3.5:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
install@^0.13.0:
|
||||
version "0.13.0"
|
||||
resolved "https://registry.yarnpkg.com/install/-/install-0.13.0.tgz#6af6e9da9dd0987de2ab420f78e60d9c17260776"
|
||||
integrity sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==
|
||||
|
||||
internal-slot@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
|
||||
@ -8238,7 +8468,7 @@ react-is@^16.13.1, react-is@^16.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^18.0.0:
|
||||
react-is@^18.0.0, react-is@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
@ -8323,7 +8553,7 @@ react-scripts@^5.0.1:
|
||||
optionalDependencies:
|
||||
fsevents "^2.3.2"
|
||||
|
||||
react-transition-group@^4.4.0:
|
||||
react-transition-group@^4.4.0, react-transition-group@^4.4.5:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
|
||||
@ -8852,6 +9082,11 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
|
||||
|
||||
source-map@^0.7.3:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
|
||||
@ -8892,6 +9127,11 @@ spdy@^4.0.2:
|
||||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
sprintf-js@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
|
||||
integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
@ -9061,6 +9301,11 @@ stylehacks@^5.1.1:
|
||||
browserslist "^4.21.4"
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
stylis@4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
|
||||
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
|
Loading…
Reference in New Issue
Block a user