Some improvements from lan-maestro
All checks were successful
Build on push / prepare (push) Successful in 10s
Build on push / build-redirector (push) Successful in 35s
Build on push / build-api (push) Successful in 36s
Build on push / build-web (push) Successful in 1m8s

This commit is contained in:
Sven Heidemann 2025-03-21 16:21:46 +01:00
parent e68b10933f
commit ab3f0b07c2
10 changed files with 579 additions and 286 deletions

View File

@ -193,7 +193,7 @@ class Query(QueryABC):
if "key" in kwargs:
return await userSettingsDao.find_by(
{UserSetting.user_id: user.id, UserSetting.key: kwargs["key"]}
[{UserSetting.user_id: user.id}, {UserSetting.key: kwargs["key"]}]
)
return await userSettingsDao.find_by({UserSetting.user_id: user.id})

View File

@ -7,6 +7,7 @@ from typing import Generic, Optional, Union, TypeVar, Any, Type
from core.const import DATETIME_FORMAT
from core.database.abc.db_model_abc import DbModelABC
from core.database.database import Database
from core.database.external_data_temp_table_builder import ExternalDataTempTableBuilder
from core.get_value import get_value
from core.logger import DBLogger
from core.string import camel_to_snake
@ -16,6 +17,7 @@ T_DBM = TypeVar("T_DBM", bound=DbModelABC)
class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
_external_fields: dict[str, ExternalDataTempTableBuilder] = {}
@abstractmethod
def __init__(self, source: str, model_type: Type[T_DBM], table_name: str):
@ -30,6 +32,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self.__db_names: dict[str, str] = {}
self.__foreign_tables: dict[str, str] = {}
self.__foreign_table_keys: dict[str, str] = {}
self.__date_attributes: set[str] = set()
self.__ignored_attributes: set[str] = set()
@ -42,12 +45,12 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return self._table_name
def attribute(
self,
attr_name: Attribute,
attr_type: type,
db_name: str = None,
ignore=False,
primary_key=False,
self,
attr_name: Attribute,
attr_type: type,
db_name: str = None,
ignore=False,
primary_key=False,
):
"""
Add an attribute for db and object mapping to the data access object
@ -77,21 +80,20 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
self.__date_attributes.add(db_name)
def reference(
self,
attr: Attribute,
primary_attr: Attribute,
foreign_attr: Attribute,
table_name: str,
self,
attr: Attribute,
primary_attr: Attribute,
foreign_attr: Attribute,
table_name: str,
):
"""
Add a reference to another table for the given attribute
:param Attribute attr: Name of the attribute in the object
:param str primary_attr: Name of the primary key in the foreign object
:param str foreign_attr: Name of the foreign key in the object
:param str table_name: Name of the table to reference
:return:
"""
if table_name == self._table_name:
return
if isinstance(attr, property):
attr = attr.fget.__name__
@ -105,11 +107,18 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
foreign_attr = foreign_attr.lower().replace("_", "")
self.__foreign_table_keys[attr] = foreign_attr
if table_name == self._table_name:
return
self.__joins[foreign_attr] = (
f"LEFT JOIN {table_name} ON {table_name}.{primary_attr} = {self._table_name}.{foreign_attr}"
)
self.__foreign_tables[attr] = table_name
def use_external_fields(self, builder: ExternalDataTempTableBuilder):
self._external_fields[builder.table_name] = builder
def to_object(self, result: dict) -> T_DBM:
"""
Convert a result from the database to an object
@ -136,7 +145,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
query += f" {self.__joins[join]}"
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
query += f" WHERE {self._build_conditions(filters)}"
conditions, external_table_deps = await self._build_conditions(filters)
query = await self._handle_query_external_temp_tables(
query, external_table_deps, ignore_fields=True
)
query += f" WHERE {conditions};"
result = await self._db.select_map(query)
if len(result) == 0:
@ -168,11 +181,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return self.to_object(result[0])
async def get_by(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
) -> list[T_DBM]:
"""
Get all objects by the given filters
@ -185,7 +198,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
:raises ValueError: When no result is found
"""
result = await self._db.select_map(
self._build_conditional_query(filters, sorts, take, skip)
await self._build_conditional_query(filters, sorts, take, skip)
)
if not result or len(result) == 0:
raise ValueError("No result found")
@ -193,11 +206,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return [self.to_object(x) for x in result]
async def get_single_by(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
) -> T_DBM:
"""
Get a single object by the given filters
@ -218,11 +231,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return result[0]
async def find_by(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
) -> list[Optional[T_DBM]]:
"""
Find all objects by the given filters
@ -234,7 +247,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
:rtype: list[Optional[T_DBM]]
"""
result = await self._db.select_map(
self._build_conditional_query(filters, sorts, take, skip)
await self._build_conditional_query(filters, sorts, take, skip)
)
if not result or len(result) == 0:
return []
@ -242,11 +255,11 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return [self.to_object(x) for x in result]
async def find_single_by(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
) -> Optional[T_DBM]:
"""
Find a single object by the given filters
@ -346,7 +359,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
await self._db.execute(query)
async def _build_delete_statement(
self, obj: T_DBM, hard_delete: bool = False
self, obj: T_DBM, hard_delete: bool = False
) -> str:
if hard_delete:
return f"""
@ -424,7 +437,7 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return "NULL"
if isinstance(value, Enum):
return str(value.value)
return f"'{value.value}'"
if isinstance(value, bool):
return "true" if value else "false"
@ -461,77 +474,115 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
return cast_type(value)
def _build_conditional_query(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
async def _handle_query_external_temp_tables(
self, query: str, external_table_deps: list[str], ignore_fields=False
) -> str:
for dep in external_table_deps:
temp_table = self._external_fields[dep]
temp_table_sql = await temp_table.build()
if not ignore_fields:
query = query.replace(
" FROM",
f", {','.join([f'{temp_table.table_name}.{x}' for x in temp_table.fields.keys() if x not in self.__db_names])} FROM",
)
query = f"{temp_table_sql}\n{query}"
query += f" LEFT JOIN {temp_table.table_name} ON {temp_table.join_ref_table}.{self.__primary_key} = {temp_table.table_name}.{temp_table.primary_key}"
return query
async def _build_conditional_query(
self,
filters: AttributeFilters = None,
sorts: AttributeSorts = None,
take: int = None,
skip: int = None,
) -> str:
query = f"SELECT {self._table_name}.* FROM {self._table_name}"
for join in self.__joins:
query += f" {self.__joins[join]}"
if filters is not None and (not isinstance(filters, list) or len(filters) > 0):
query += f" WHERE {self._build_conditions(filters)}"
conditions, external_table_deps = await self._build_conditions(filters)
query = await self._handle_query_external_temp_tables(
query, external_table_deps
)
query += f" WHERE {conditions}"
if sorts is not None and (not isinstance(sorts, list) or len(sorts) > 0):
query += f" ORDER BY {self._build_order_by(sorts)}"
if take is not None:
query += f" LIMIT {take}"
if skip is not None:
query += f" OFFSET {skip}"
if not query.endswith(";"):
query += ";"
return query
def _build_conditions(self, filters: AttributeFilters) -> str:
def _get_external_field_key(self, field_name: str) -> Optional[str]:
"""
Returns the key to get the external field if found, otherwise None.
:param str field_name: The name of the field to search for.
:return: The key if found, otherwise None.
:rtype: Optional[str]
"""
for key, builder in self._external_fields.items():
if field_name in builder.fields and field_name not in self.__db_names:
return key
return None
async def _build_conditions(self, filters: AttributeFilters) -> (str, list[str]):
"""
Build SQL conditions from the given filters
:param filters:
:return:
:return: SQL conditions & External field table dependencies
"""
external_field_table_deps = []
if not isinstance(filters, list):
filters = [filters]
conditions = []
for f in filters:
f_conditions = []
for attr, values in f.items():
if isinstance(attr, property):
attr = attr.fget.__name__
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
conditions.extend(
self._build_foreign_conditions(foreign_table, values)
)
cons, eftd = self._build_foreign_conditions(foreign_table, values)
if eftd:
external_field_table_deps.extend(eftd)
f_conditions.extend(cons)
continue
if attr == "fuzzy":
conditions.append(
" OR ".join(
self._build_fuzzy_conditions(
[
(
self.__db_names[x]
if x in self.__db_names
else self.__db_names[camel_to_snake(x)]
)
for x in get_value(values, "fields", list[str])
],
get_value(values, "term", str),
get_value(values, "threshold", int, 5),
)
)
self._handle_fuzzy_filter_conditions(
f_conditions, external_field_table_deps, values
)
continue
db_name = self.__db_names[attr]
external_fields_table_name = self._get_external_field_key(attr)
if external_fields_table_name is not None:
external_fields_table = self._external_fields[
external_fields_table_name
]
db_name = f"{external_fields_table.table_name}.{attr}"
external_field_table_deps.append(external_fields_table.table_name)
elif (
isinstance(values, dict) or isinstance(values, list)
) and not attr in self.__foreign_tables:
db_name = f"{self._table_name}.{self.__db_names[attr]}"
else:
db_name = self.__db_names[attr]
if isinstance(values, dict):
for operator, value in values.items():
conditions.append(
self._build_condition(
f"{self._table_name}.{db_name}", operator, value
)
f_conditions.append(
self._build_condition(f"{db_name}", operator, value)
)
elif isinstance(values, list):
sub_conditions = []
@ -539,38 +590,42 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if isinstance(value, dict):
for operator, val in value.items():
sub_conditions.append(
self._build_condition(
f"{self._table_name}.{db_name}", operator, val
)
self._build_condition(f"{db_name}", operator, val)
)
else:
sub_conditions.append(
self._get_value_validation_sql(db_name, value)
)
conditions.append(f"({' OR '.join(sub_conditions)})")
f_conditions.append(f"({' OR '.join(sub_conditions)})")
else:
conditions.append(self._get_value_validation_sql(db_name, values))
f_conditions.append(self._get_value_validation_sql(db_name, values))
return " AND ".join(conditions)
conditions.append(f"({' OR '.join(f_conditions)})")
return " AND ".join(conditions), external_field_table_deps
@staticmethod
def _build_fuzzy_conditions(
self, fields: list[str], term: str, threshold: int = 10
fields: list[str], term: str, threshold: int = 10
) -> list[str]:
conditions = []
for field in fields:
conditions.append(
f"levenshtein({field}, '{term}') <= {threshold}"
f"levenshtein({field}::TEXT, '{term}') <= {threshold}"
) # Adjust the threshold as needed
return conditions
def _build_foreign_conditions(self, table: str, values: dict) -> list[str]:
def _build_foreign_conditions(
self, table: str, values: dict
) -> (list[str], list[str]):
"""
Build SQL conditions for foreign key references
:param table: Foreign table name
:param values: Filter values
:return: List of conditions
:return: List of conditions, List of external field tables
"""
external_field_table_deps = []
conditions = []
for attr, sub_values in values.items():
if isinstance(attr, property):
@ -578,25 +633,43 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
if attr in self.__foreign_tables:
foreign_table = self.__foreign_tables[attr]
conditions.extend(
self._build_foreign_conditions(foreign_table, sub_values)
sub_conditions, eftd = self._build_foreign_conditions(
foreign_table, sub_values
)
if len(eftd) > 0:
external_field_table_deps.extend(eftd)
conditions.extend(sub_conditions)
continue
if attr == "fuzzy":
self._handle_fuzzy_filter_conditions(
conditions, external_field_table_deps, sub_values
)
continue
db_name = f"{table}.{attr.lower().replace('_', '')}"
external_fields_table_name = self._get_external_field_key(attr)
if external_fields_table_name is not None:
external_fields_table = self._external_fields[
external_fields_table_name
]
db_name = f"{external_fields_table.table_name}.{attr}"
external_field_table_deps.append(external_fields_table.table_name)
else:
db_name = f"{table}.{attr.lower().replace('_', '')}"
if isinstance(sub_values, dict):
for operator, value in sub_values.items():
conditions.append(
f"({self._build_condition(db_name, operator, value)} OR {self._build_condition(db_name, "isNull", None)})")
f"{self._build_condition(db_name, operator, value)}"
)
elif isinstance(sub_values, list):
sub_conditions = []
for value in sub_values:
if isinstance(value, dict):
for operator, val in value.items():
sub_conditions.append(
f"({self._build_condition(db_name, operator, val)} OR {self._build_condition(db_name, "isNull", None)})"
f"{self._build_condition(db_name, operator, val)}"
)
else:
sub_conditions.append(
@ -606,14 +679,55 @@ class DataAccessObjectABC(ABC, Database, Generic[T_DBM]):
else:
conditions.append(self._get_value_validation_sql(db_name, sub_values))
return conditions
return conditions, external_field_table_deps
def _handle_fuzzy_filter_conditions(
self, conditions, external_field_table_deps, sub_values
):
fuzzy_fields = get_value(sub_values, "fields", list[str])
fuzzy_fields_db_names = []
for fuzzy_field in fuzzy_fields:
external_fields_table_name = self._get_external_field_key(fuzzy_field)
if external_fields_table_name is not None:
external_fields_table = self._external_fields[
external_fields_table_name
]
fuzzy_fields_db_names.append(
f"{external_fields_table.table_name}.{fuzzy_field}"
)
external_field_table_deps.append(external_fields_table.table_name)
elif fuzzy_field in self.__db_names:
fuzzy_fields_db_names.append(
f"{self._table_name}.{self.__db_names[fuzzy_field]}"
)
elif fuzzy_field in self.__foreign_tables:
fuzzy_fields_db_names.append(
f"{self._table_name}.{self.__foreign_table_keys[fuzzy_field]}"
)
else:
fuzzy_fields_db_names.append(
self.__db_names[camel_to_snake(fuzzy_field)]
)
conditions.append(
f"({' OR '.join(
self._build_fuzzy_conditions(
[x for x in fuzzy_fields_db_names],
get_value(sub_values, "term", str),
get_value(sub_values, "threshold", int, 5),
)
)
})"
)
def _get_value_validation_sql(self, field: str, value: Any):
value = self._get_value_sql(value)
field_selector = f"{self._table_name}.{field}"
if field in self.__foreign_tables:
field_selector = self.__db_names[field]
if value == "NULL":
return f"{self._table_name}.{field} IS NULL"
return f"{self._table_name}.{field} = {value}"
return f"{field_selector} IS NULL"
return f"{field_selector} = {value}"
def _build_condition(self, db_name: str, operator: str, value: Any) -> str:
"""

View File

@ -0,0 +1,72 @@
import textwrap
from typing import Callable
class ExternalDataTempTableBuilder:
def __init__(self):
self._table_name = None
self._fields: dict[str, str] = {}
self._primary_key = "id"
self._join_ref_table = None
self._value_getter = None
@property
def table_name(self) -> str:
return self._table_name
@property
def fields(self) -> dict[str, str]:
return self._fields
@property
def primary_key(self) -> str:
return self._primary_key
@property
def join_ref_table(self) -> str:
return self._join_ref_table
def with_table_name(self, table_name: str) -> "ExternalDataTempTableBuilder":
self._join_ref_table = table_name
if "." in table_name:
table_name = table_name.split(".")[-1]
if not table_name.endswith("_temp"):
table_name = f"{table_name}_temp"
self._table_name = table_name
return self
def with_field(
self, name: str, sql_type: str, primary=False
) -> "ExternalDataTempTableBuilder":
if primary:
sql_type += " PRIMARY KEY"
self._primary_key = name
self._fields[name] = sql_type
return self
def with_value_getter(
self, value_getter: Callable
) -> "ExternalDataTempTableBuilder":
self._value_getter = value_getter
return self
async def build(self) -> str:
assert self._table_name is not None, "Table name is required"
assert self._value_getter is not None, "Value getter is required"
values_str = ", ".join([f"{value}" for value in await self._value_getter()])
return textwrap.dedent(
f"""
DROP TABLE IF EXISTS {self._table_name};
CREATE TEMP TABLE {self._table_name} (
{", ".join([f"{k} {v}" for k, v in self._fields.items()])}
);
INSERT INTO {self._table_name} VALUES {values_str};
"""
)

View File

@ -1,12 +1,37 @@
<div class="flex justify-between w-full">
<div *ngFor="let element of elements">
<p-button
*ngIf="element.visible !== false"
type="button"
label="{{ element.label | translate }}"
[icon]="element.icon"
class="icon-btn btn"
[routerLink]="element.routerLink"
(onClick)="element.command?.()"></p-button>
</div>
<div *ngFor="let element of visibleElements" class="flex justify-center">
<ng-container *ngIf="element.visible !== false && element.routerLink">
<a
[routerLink]="element.routerLink"
(click)="element.command?.()"
class="icon-btn btn flex gap-2">
<i *ngIf="element.icon" class="{{ element.icon }}"></i>
<b>{{ element.label | translate }}</b>
</a>
</ng-container>
<ng-container *ngIf="element.visible !== false && element.items">
<p-button
class="icon-btn btn"
icon="{{ element.icon }}"
(onClick)="overlayPanel.toggle($event)"
label="{{ element.label | translate }}">
</p-button>
<p-overlayPanel [styleClass]="theme" #overlayPanel>
<div class="flex flex-col gap-2">
<div *ngFor="let item of element.items" class="flex gap-1">
<a
*ngIf="item.visible !== false"
[routerLink]="item.routerLink"
(click)="overlayPanel.toggle($event); item.command?.()"
class="icon-btn btn flex gap-2">
<i *ngIf="item.icon" class="{{ item.icon }}"></i>
<b>{{ item.label | translate }}</b>
</a>
</div>
</div>
</p-overlayPanel>
</ng-container>
</div>
</div>

View File

@ -1,11 +1,32 @@
import { Component, Input } from "@angular/core";
import { MenuElement } from "src/app/model/view/menu-element";
import { Component, EventEmitter, Input, OnDestroy } from '@angular/core';
import { MenuElement } from 'src/app/model/view/menu-element';
import { GuiService } from 'src/app/service/gui.service';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: "app-menu-bar",
templateUrl: "./menu-bar.component.html",
styleUrl: "./menu-bar.component.scss",
selector: 'app-menu-bar',
templateUrl: './menu-bar.component.html',
styleUrl: './menu-bar.component.scss',
})
export class MenuBarComponent {
export class MenuBarComponent implements OnDestroy {
@Input() elements: MenuElement[] = [];
protected theme = '';
private unsubscribe$ = new EventEmitter<void>();
get visibleElements() {
return this.elements.filter(e => e.visible !== false);
}
constructor(private guiService: GuiService) {
this.guiService.theme$
.pipe(takeUntil(this.unsubscribe$))
.subscribe(theme => {
this.theme = theme;
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}

View File

@ -1,147 +1,172 @@
<p-table
[dataKey]="dataKey"
[value]="rows"
[paginator]="true"
[rowsPerPageOptions]="rowsPerPageOptions"
[rows]="take"
(rowsChange)="takeChange.emit($event)"
[first]="skip"
(firstChange)="skipChange.emit($event)"
[totalRecords]="totalCount"
[lazy]="true"
(onLazyLoad)="loadData($event)"
[loading]="loading"
[resizableColumns]="false"
[reorderableColumns]="false"
[responsiveLayout]="responsiveLayout"
columnResizeMode="expand"
[breakpoint]="'900px'"
[rowHover]="true">
<ng-template pTemplate="caption">
<div class="flex justify-between items-center">
<div>
<div *ngIf="countHeaderTranslation" class="table-caption-table-info">
<div class="table-caption-text">
<ng-container *ngIf="!loading"
>{{ skip + 1 }} {{ 'table.to' | translate }}
{{ skip + rows.length }} {{ 'table.of' | translate }}
{{ totalCount }}
</ng-container>
{{ countHeaderTranslation | translate }}
</div>
</div>
</div>
<div class="flex space-x-2">
<ng-container *ngTemplateOutlet="customActions"></ng-container>
<p-button
*ngIf="create && hasPermissions.create"
class="icon-btn btn"
icon="pi pi-plus"
tooltipPosition="left"
pTooltip="{{ 'table.create' | translate }}"
routerLink="{{ updateBaseUrl }}create"></p-button>
<p-button
class="icon-btn btn"
[styleClass]="showDeleted ? 'highlight2' : ''"
icon="pi pi-trash"
tooltipPosition="left"
pTooltip="{{
[dataKey]="dataKey"
[value]="rows"
[paginator]="true"
[rowsPerPageOptions]="rowsPerPageOptions"
[rows]="take"
(rowsChange)="takeChange.emit($event)"
[first]="skip"
(firstChange)="skipChange.emit($event)"
[totalRecords]="totalCount"
[lazy]="true"
(onLazyLoad)="loadData($event)"
[loading]="loading"
[resizableColumns]="false"
[reorderableColumns]="false"
[responsiveLayout]="responsiveLayout"
columnResizeMode="expand"
[breakpoint]="'900px'"
[rowHover]="true">
<ng-template pTemplate="caption">
<div class="flex justify-between items-center">
<div>
<div *ngIf="countHeaderTranslation" class="table-caption-table-info">
<div class="table-caption-text">
<ng-container *ngIf="!loading"
>{{ skip + 1 }} {{ 'table.to' | translate }}
{{ skip + rows.length }} {{ 'table.of' | translate }}
{{ totalCount }}
</ng-container>
{{ countHeaderTranslation | translate }}
</div>
</div>
</div>
<div class="flex space-x-2">
<ng-container *ngTemplateOutlet="customActions"></ng-container>
<p-button
*ngIf="create && hasPermissions.create"
class="icon-btn btn"
icon="pi pi-plus"
tooltipPosition="left"
pTooltip="{{ 'table.create' | translate }}"
routerLink="{{ updateBaseUrl }}create"></p-button>
<p-button
class="icon-btn btn"
[styleClass]="showDeleted ? 'highlight2' : ''"
icon="pi pi-trash"
tooltipPosition="left"
pTooltip="{{
(showDeleted ? 'table.hide_deleted' : 'table.show_deleted')
| translate
}}"
(click)="toggleShowDeleted()"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-sort-alt-slash"
tooltipPosition="left"
pTooltip="{{ 'table.reset_sort' | translate }}"
(click)="resetSort()"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-filter-slash"
tooltipPosition="left"
pTooltip="{{ 'table.reset_filters' | translate }}"
(click)="resetFilters()"></p-button>
<app-column-selector
*ngIf="selectableColumns"
[columns]="columns"
(selectChange)="resolveColumns()"></app-column-selector>
</div>
</div>
</ng-template>
<ng-template pTemplate="header">
<tr>
<th
*ngFor="let column of visibleColumns"
[pSortableColumn]="column.name"
[class]="column.class ?? ''"
[style.min-width]="
column.minWidth ? column.minWidth + ' !important' : ''
"
[style.width]="column.width ? column.width + ' !important' : ''"
[style.max-width]="
column.maxWidth ? column.maxWidth + ' !important' : ''
">
<div class="flex items-center space-x-2">
<span>{{ column.translationKey | translate }}</span>
<p-sortIcon [field]="column.name"></p-sortIcon>
(click)="toggleShowDeleted()"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-sort-alt-slash"
tooltipPosition="left"
pTooltip="{{ 'table.reset_sort' | translate }}"
(click)="resetSort()"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-filter-slash"
tooltipPosition="left"
pTooltip="{{ 'table.reset_filters' | translate }}"
(click)="resetFilters()"></p-button>
<app-column-selector
*ngIf="selectableColumns"
[columns]="columns"
(selectChange)="resolveColumns()"></app-column-selector>
</div>
</div>
</th>
<th *ngIf="update || delete.observed"></th>
</tr>
</ng-template>
<tr *ngIf="showFilters">
<th
*ngFor="let column of visibleColumns"
[class]="column.class ?? ''"
[style.min-width]="
<ng-template pTemplate="header">
<tr>
<th
*ngFor="let column of visibleColumns"
[pSortableColumn]="column.name"
[class]="column.class ?? ''"
[style.min-width]="
column.minWidth ? column.minWidth + ' !important' : ''
"
[style.width]="column.width ? column.width + ' !important' : ''"
[style.max-width]="
[style.width]="column.width ? column.width + ' !important' : ''"
[style.max-width]="
column.maxWidth ? column.maxWidth + ' !important' : ''
">
<form *ngIf="filterForm && column.filterable" [formGroup]="filterForm">
<ng-container [ngSwitch]="column.type">
<input
*ngSwitchCase="'date'"
pInputText
type="text"
[formControlName]="column.name"
class="w-full box-border" />
<p-triStateCheckbox
*ngSwitchCase="'bool'"
[formControlName]="column.name"
class="w-full box-border"></p-triStateCheckbox>
<input
*ngSwitchDefault
pInputText
[formControlName]="column.name"
class="w-full box-border" />
</ng-container>
</form>
</th>
<th *ngIf="update || delete.observed"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row let-i="rowIndex">
<tr *ngIf="hasPermissions.read && resolvedColumns.length > 0">
<ng-container *ngFor="let r of resolvedColumns[i - take * (skip / take)]">
<td
[ngClass]="{ deleted: row?.deleted }"
[style.min-width]="
<div class="flex items-center space-x-2">
<span>{{ column.translationKey | translate }}</span>
<p-sortIcon [field]="column.name"></p-sortIcon>
</div>
</th>
<th *ngIf="update || delete.observed || customRowActionsVisible"></th>
</tr>
<tr *ngIf="fuzzyFilter">
<th [attr.colspan]="visibleColumns.length">
<form
*ngIf="filterForm"
[formGroup]="filterForm"
class="flex gap-2 justify-between items-center">
<input
pInputText
type="text"
class="w-full box-border"
formControlName="fuzzy"
placeholder="{{ 'table.search' | translate }}" />
<p-button
class="btn icon-btn"
icon="pi pi-times"
(onClick)="filterForm.get('fuzzy')?.setValue(undefined)"
[disabled]="!filterForm.get('fuzzy')?.value"></p-button>
</form>
</th>
<th *ngIf="update || delete.observed || customRowActionsVisible"></th>
</tr>
<tr *ngIf="showFilters">
<th
*ngFor="let column of visibleColumns"
[class]="column.class ?? ''"
[style.min-width]="
column.minWidth ? column.minWidth + ' !important' : ''
"
[style.width]="column.width ? column.width + ' !important' : ''"
[style.max-width]="
column.maxWidth ? column.maxWidth + ' !important' : ''
">
<form *ngIf="filterForm && column.filterable" [formGroup]="filterForm">
<ng-container [ngSwitch]="column.type">
<input
*ngSwitchCase="'date'"
pInputText
type="text"
[formControlName]="column.name"
class="w-full box-border" />
<p-triStateCheckbox
*ngSwitchCase="'bool'"
[formControlName]="column.name"
class="w-full box-border"></p-triStateCheckbox>
<input
*ngSwitchDefault
pInputText
[formControlName]="column.name"
class="w-full box-border" />
</ng-container>
</form>
</th>
<th *ngIf="update || delete.observed || customRowActionsVisible"></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-row let-i="rowIndex">
<tr *ngIf="hasPermissions.read && resolvedColumns.length > 0">
<ng-container *ngFor="let r of resolvedColumns[i - take * (skip / take)]">
<td
[ngClass]="{
deleted: row?.deleted,
'row-disabled': rowDisabled(row),
}"
[style.min-width]="
r.column.minWidth ? r.column.minWidth + ' !important' : ''
"
[style.width]="r.column.width ? r.column.width + ' !important' : ''"
[style.max-width]="
[style.width]="r.column.width ? r.column.width + ' !important' : ''"
[style.max-width]="
r.column.maxWidth ? r.column.maxWidth + ' !important' : ''
">
<ng-container [ngSwitch]="r.column.type">
<ng-container [ngSwitch]="r.column.type">
<span *ngSwitchCase="'date'">
{{ r.value | customDate: 'dd.MM.yyyy HH:mm:ss' }}
</span>
<span *ngSwitchCase="'bool'">
<span *ngSwitchCase="'bool'">
<ng-container [ngSwitch]="r.value">
<span *ngSwitchCase="true">
<span class="pi pi-check-circle info"></span>
@ -151,56 +176,57 @@
</span>
</ng-container>
</span>
<span *ngSwitchCase="'password'">
<span *ngSwitchCase="'password'">
{{ r.value | protect }}
<p-button
class="btn icon-btn"
icon="pi pi-copy"
(onClick)="copy(r.value)"></p-button>
<p-button
class="btn icon-btn"
icon="pi pi-copy"
(onClick)="copy(r.value)"></p-button>
</span>
<span *ngSwitchDefault>{{ r.value }}</span>
</ng-container>
</td>
</ng-container>
<td class="flex max-w-32">
<ng-container
*ngTemplateOutlet="
<span *ngSwitchDefault>{{ r.value }}</span>
</ng-container>
</td>
</ng-container>
<td class="flex max-w-32">
<ng-container
*ngTemplateOutlet="
customRowActions;
context: { $implicit: row }
context: { $implicit: row, rowDisabled: rowDisabled(row) }
"></ng-container>
<p-button
*ngIf="update && hasPermissions.update"
class="icon-btn btn"
icon="pi pi-pencil"
tooltipPosition="left"
pTooltip="{{ 'table.update' | translate }}"
[disabled]="row?.deleted"
routerLink="{{ updateBaseUrl }}edit/{{ row.id }}"></p-button>
<p-button
*ngIf="delete.observed && hasPermissions.delete"
class="icon-btn btn danger-icon-btn"
icon="pi pi-trash"
tooltipPosition="left"
pTooltip="{{ 'table.delete' | translate }}"
[disabled]="row?.deleted"
(click)="delete.emit(row)"></p-button>
<p-button
*ngIf="restore.observed && row?.deleted && hasPermissions.restore"
class="icon-btn btn"
icon="pi pi-undo"
tooltipPosition="left"
pTooltip="{{ 'table.restore' | translate }}"
(click)="restore.emit(row)"></p-button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr></tr>
<tr>
<td [attr.colspan]="columns.length + 1">
{{ 'table.no_entries_found' | translate }}
</td>
</tr>
<tr></tr>
</ng-template>
<p-button
*ngIf="update && hasPermissions.update"
class="icon-btn btn"
icon="pi pi-pencil"
tooltipPosition="left"
pTooltip="{{ 'table.update' | translate }}"
[disabled]="rowDisabled(row) || row?.deleted"
routerLink="{{ updateBaseUrl }}edit/{{ row.id }}"></p-button>
<p-button
*ngIf="delete.observed && hasPermissions.delete"
class="icon-btn btn danger-icon-btn"
icon="pi pi-trash"
tooltipPosition="left"
pTooltip="{{ 'table.delete' | translate }}"
[disabled]="rowDisabled(row) || row?.deleted"
(click)="delete.emit(row)"></p-button>
<p-button
*ngIf="restore.observed && row?.deleted && hasPermissions.restore"
class="icon-btn btn"
icon="pi pi-undo"
tooltipPosition="left"
pTooltip="{{ 'table.restore' | translate }}"
(click)="restore.emit(row)"
[disabled]="rowDisabled(row)"></p-button>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr></tr>
<tr>
<td [attr.colspan]="columns.length + 1">
{{ 'table.no_entries_found' | translate }}
</td>
</tr>
<tr></tr>
</ng-template>
</p-table>

View File

@ -10,6 +10,7 @@ import {
import {
ResolvedTableColumn,
TableColumn,
TableColumnFuzzyFilter,
TableRequireAnyPermissions,
} from 'src/app/modules/shared/components/table/table.model';
import { TableLazyLoadEvent } from 'primeng/table';
@ -46,6 +47,7 @@ export class TableComponent<T> implements OnInit {
}
@Input({ required: true }) columns: TableColumn<T>[] = [];
@Input() fuzzyFilter?: TableColumnFuzzyFilter;
get visibleColumns() {
return this.columns.filter(x => x.visible);
@ -70,6 +72,8 @@ export class TableComponent<T> implements OnInit {
@Input() sort: Sort[] = [];
@Output() sortChange = new EventEmitter<Sort[]>();
@Input() rowDisabled: (row: T) => boolean = () => false;
// eslint-disable-next-line @angular-eslint/no-output-native
@Output() load = new EventEmitter<void>();
@Input() loading = true;
@ -92,6 +96,10 @@ export class TableComponent<T> implements OnInit {
@ContentChild(CustomRowActionsDirective, { read: TemplateRef })
customRowActions!: TemplateRef<never>;
get customRowActionsVisible() {
return !!this.customRowActions;
}
protected resolvedColumns: ResolvedTableColumn<T>[][] = [];
protected filterForm!: FormGroup;
protected defaultFilterForm!: FormGroup;
@ -244,6 +252,13 @@ export class TableComponent<T> implements OnInit {
buildDefaultFilterForm() {
this.defaultFilterForm = new FormGroup({});
if (this.fuzzyFilter) {
this.defaultFilterForm.addControl(
'fuzzy',
new FormControl<string | undefined>(undefined)
);
}
this.columns
.filter(x => x.filterable)
.forEach(x => {
@ -301,6 +316,17 @@ export class TableComponent<T> implements OnInit {
changes[key] !== ''
)
.map(key => {
if (key === 'fuzzy') {
if (!this.fuzzyFilter) return {};
return {
fuzzy: {
term: changes[key],
fields: this.fuzzyFilter?.columns,
threshold: this.fuzzyFilter?.threshold,
},
};
}
const column = this.columns.find(x => x.name === key);
if (!column || !column.filterable) {
return {};

View File

@ -22,6 +22,11 @@ export interface TableColumn<T> {
class?: string;
}
export interface TableColumnFuzzyFilter {
columns: string[];
threshold?: number;
}
export interface ResolvedTableColumn<T> {
value: TableColumnValue<T>;
data: T;

View File

@ -15,8 +15,8 @@ export class ConfigService {
imprintURL: '',
themes: [
{
label: 'Maxlan',
name: 'maxlan',
label: 'Open-redirect',
name: 'open-redirect',
},
],
loadingScreen: {
@ -51,8 +51,8 @@ export class ConfigService {
this.settings = settings;
if (this.settings.themes.length === 0) {
this.settings.themes.push({
label: 'Maxlan',
name: 'maxlan',
label: 'Open-redirect',
name: 'open-redirect',
});
}
resolve();

View File

@ -29,6 +29,10 @@ export class GuiService {
this.sidebarService.hide();
}
});
this.theme$.subscribe(theme => {
console.warn('theme changed', theme);
});
}
@HostListener('window:resize', ['$event'])