Handle redirects & First design steps

This commit is contained in:
Sven Heidemann 2024-12-14 18:43:51 +01:00
parent 7b8e818339
commit 99960572ea
120 changed files with 1948 additions and 1537 deletions

View File

@ -1,16 +1,27 @@
from flask import redirect from flask import redirect, jsonify
from api.route import Route from api.route import Route
from core.logger import Logger from core.logger import Logger
from data.schemas.public.short_url import ShortUrl from data.schemas.public.short_url import ShortUrl
from data.schemas.public.short_url_dao import shortUrlDao from data.schemas.public.short_url_dao import shortUrlDao
BasePath = f"/"
logger = Logger(__name__) logger = Logger(__name__)
@Route.get(f"{BasePath}/<path:path>") @Route.get(f"/api/find/<path:path>")
async def find(path: str):
from_db = await shortUrlDao.find_single_by({ShortUrl.short_url: path})
if from_db is None:
return jsonify(None)
return jsonify(from_db.to_dto())
@Route.get(f"/api/redirect/<path:path>")
async def handle_short_url(path: str): async def handle_short_url(path: str):
if path.startswith("api/"):
path = path.replace("api/", "")
from_db = await shortUrlDao.find_single_by({ShortUrl.short_url: path}) from_db = await shortUrlDao.find_single_by({ShortUrl.short_url: path})
if from_db is None: if from_db is None:
return {"error": "Short URL not found"}, 404 return {"error": "Short URL not found"}, 404

View File

@ -22,7 +22,7 @@ class FilterABC[T](ABC):
filter_type: Union[Type["FilterABC"], Type[Union[int, str, bool, datetime]]], filter_type: Union[Type["FilterABC"], Type[Union[int, str, bool, datetime]]],
db_name=None, db_name=None,
): ):
if field not in self._obj: if field not in self._obj and db_name not in self._obj:
return return
if db_name is None: if db_name is None:

View File

@ -4,8 +4,8 @@ from api_graphql.abc.filter.string_filter import StringFilter
class ShortUrlFilter(DbModelFilterABC): class ShortUrlFilter(DbModelFilterABC):
def __init__( def __init__(
self, self,
obj: dict, obj: dict,
): ):
DbModelFilterABC.__init__(self, obj) DbModelFilterABC.__init__(self, obj)

View File

@ -8,6 +8,8 @@ type Group implements DbModel {
id: ID id: ID
name: String name: String
shortUrls: [ShortUrl]
deleted: Boolean deleted: Boolean
editor: User editor: User
createdUtc: String createdUtc: String

View File

@ -21,7 +21,6 @@ input ShortUrlSort {
id: SortOrder id: SortOrder
name: SortOrder name: SortOrder
description: SortOrder description: SortOrder
group: GroupSort
deleted: SortOrder deleted: SortOrder
editorId: SortOrder editorId: SortOrder
@ -33,7 +32,6 @@ input ShortUrlFilter {
id: IntFilter id: IntFilter
name: StringFilter name: StringFilter
description: StringFilter description: StringFilter
group: GroupFilter
deleted: BooleanFilter deleted: BooleanFilter
editor: IntFilter editor: IntFilter

View File

@ -1,4 +1,7 @@
from api_graphql.abc.db_model_query_abc import DbModelQueryABC from api_graphql.abc.db_model_query_abc import DbModelQueryABC
from data.schemas.public.group import Group
from data.schemas.public.short_url import ShortUrl
from data.schemas.public.short_url_dao import shortUrlDao
class GroupQuery(DbModelQueryABC): class GroupQuery(DbModelQueryABC):
@ -6,3 +9,8 @@ class GroupQuery(DbModelQueryABC):
DbModelQueryABC.__init__(self, "Group") DbModelQueryABC.__init__(self, "Group")
self.set_field("name", lambda x, *_: x.name) self.set_field("name", lambda x, *_: x.name)
self.set_field("shortUrls", self._get_urls)
@staticmethod
async def _get_urls(group: Group, *_):
return await shortUrlDao.find_by({ShortUrl.group_id: group.id})

View File

@ -85,7 +85,13 @@ class Query(QueryABC):
.with_dao(groupDao) .with_dao(groupDao)
.with_filter(GroupFilter) .with_filter(GroupFilter)
.with_sort(Sort[Group]) .with_sort(Sort[Group])
.with_require_any_permission([Permissions.groups, Permissions.short_urls_create, Permissions.short_urls_update]) .with_require_any_permission(
[
Permissions.groups,
Permissions.short_urls_create,
Permissions.short_urls_update,
]
)
) )
# partially public to load redirect if not resolved/redirected by api # partially public to load redirect if not resolved/redirected by api
self.field( self.field(

View File

@ -5,6 +5,7 @@ from async_property import async_property
from core.database.abc.db_model_abc import DbModelABC from core.database.abc.db_model_abc import DbModelABC
from core.typing import SerialId from core.typing import SerialId
from data.schemas.public.group import Group
class ShortUrl(DbModelABC): class ShortUrl(DbModelABC):
@ -59,7 +60,16 @@ class ShortUrl(DbModelABC):
self._group_id = value self._group_id = value
@async_property @async_property
async def group(self): async def group(self) -> Optional[Group]:
if self._group_id is None:
return None
from data.schemas.public.group_dao import groupDao from data.schemas.public.group_dao import groupDao
return await groupDao.get_by_id(self._group_id) return await groupDao.get_by_id(self._group_id)
def to_dto(self) -> dict:
return {
"id": self.id,
"target_url": self.target_url,
}

View File

@ -1,34 +1,23 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component";
import { AuthGuard } from 'src/app/core/guard/auth.guard'; import { AuthGuard } from "src/app/core/guard/auth.guard";
import {HomeComponent} from "src/app/components/home/home.component"; import { HomeComponent } from "src/app/components/home/home.component";
import { RedirectComponent } from "src/app/components/redirect/redirect.component";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: "",
redirectTo: 'home',
pathMatch: 'full',
},
{
path: 'about',
redirectTo: 'home',
},
{
path: 'home',
component: HomeComponent, component: HomeComponent,
}, },
{ {
path: 'admin', path: "admin",
loadChildren: () => loadChildren: () =>
import('./modules/admin/admin.module').then(m => m.AdminModule), import("./modules/admin/admin.module").then((m) => m.AdminModule),
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },
{ path: '404', component: NotFoundComponent }, { path: "404", component: NotFoundComponent },
{ { path: "**", component: RedirectComponent },
path: '**',
redirectTo: '404',
},
]; ];
@NgModule({ @NgModule({

View File

@ -1,32 +1,36 @@
<main> <main *ngIf="isLoggedIn; else home">
<app-header></app-header> <app-header></app-header>
<div class="app"> <div class="app">
<aside *ngIf="showSidebar"> <aside *ngIf="showSidebar">
<app-sidebar></app-sidebar> <app-sidebar></app-sidebar>
</aside> </aside>
<section class="component"> <section class="component">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</section> </section>
</div> </div>
<app-footer></app-footer> <app-footer></app-footer>
<p-toast></p-toast> <p-toast></p-toast>
<p-confirmDialog #cd key="confirmConfirmationDialog" [baseZIndex]="10000"> <p-confirmDialog #cd key="confirmConfirmationDialog" [baseZIndex]="10000">
<ng-template pTemplate="footer"> <ng-template pTemplate="footer">
<div class="flex gap-2.5 items-center justify-end"> <div class="flex gap-2.5 items-center justify-end">
<p-button <p-button
label="{{ 'dialog.abort' | translate }}" label="{{ 'dialog.abort' | translate }}"
class="btn icon-btn danger-icon-btn" class="btn icon-btn danger-icon-btn"
icon="pi pi-times-circle" icon="pi pi-times-circle"
(onClick)="cd.reject()"></p-button> (onClick)="cd.reject()"></p-button>
<p-button <p-button
label="{{ 'dialog.confirm' | translate }}" label="{{ 'dialog.confirm' | translate }}"
class="btn" class="btn"
icon="pi pi-check-circle" icon="pi pi-check-circle"
(onClick)="cd.accept()"></p-button> (onClick)="cd.accept()"></p-button>
</div> </div>
</ng-template> </ng-template>
</p-confirmDialog> </p-confirmDialog>
</main> </main>
<app-spinner></app-spinner> <app-spinner></app-spinner>
<ng-template #home>
<router-outlet></router-outlet>
</ng-template>

View File

@ -1,22 +1,22 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from "@angular/core/testing";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { AppComponent } from './app.component'; import { AppComponent } from "./app.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { of } from 'rxjs'; import { of } from "rxjs";
import { HomeComponent } from 'src/app/components/home/home.component'; import { HomeComponent } from "src/app/components/home/home.component";
import { FooterComponent } from 'src/app/components/footer/footer.component'; import { FooterComponent } from "src/app/components/footer/footer.component";
import { HeaderComponent } from 'src/app/components/header/header.component'; import { HeaderComponent } from "src/app/components/header/header.component";
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component";
import { SpinnerComponent } from 'src/app/components/spinner/spinner.component'; import { SpinnerComponent } from "src/app/components/spinner/spinner.component";
import { SidebarComponent } from 'src/app/components/sidebar/sidebar.component'; import { SidebarComponent } from "src/app/components/sidebar/sidebar.component";
describe('AppComponent', () => { describe("AppComponent", () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ declarations: [
@ -46,7 +46,7 @@ describe('AppComponent', () => {
}).compileComponents(); }).compileComponents();
}); });
it('should create the app', () => { it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app).toBeTruthy(); expect(app).toBeTruthy();

View File

@ -1,27 +1,32 @@
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from "@angular/core";
import { SidebarService } from 'src/app/service/sidebar.service'; import { SidebarService } from "src/app/service/sidebar.service";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from "rxjs/operators";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
@Component({ @Component({
selector: 'app-root', selector: "app-root",
templateUrl: './app.component.html', templateUrl: "./app.component.html",
styleUrl: './app.component.scss', styleUrl: "./app.component.scss",
}) })
export class AppComponent implements OnDestroy { export class AppComponent implements OnDestroy {
showSidebar = false; showSidebar = false;
isLoggedIn = false;
unsubscribe$ = new Subject<void>(); unsubscribe$ = new Subject<void>();
constructor( constructor(
private sidebar: SidebarService, private sidebar: SidebarService,
private auth: AuthService private auth: AuthService,
) { ) {
this.auth.loadUser(); this.auth.loadUser();
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe((user) => {
this.isLoggedIn = user !== null && user !== undefined;
});
this.sidebar.visible$ this.sidebar.visible$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(visible => { .subscribe((visible) => {
this.showSidebar = visible; this.showSidebar = visible;
}); });
} }

View File

@ -1,26 +1,27 @@
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core";
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from './app.component'; import { AppComponent } from "./app.component";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { HomeComponent } from './components/home/home.component'; import { initializeKeycloak } from "./core/init-keycloak";
import { initializeKeycloak } from './core/init-keycloak'; import { HttpClient } from "@angular/common/http";
import { HttpClient } from '@angular/common/http'; import { environment } from "../environments/environment";
import { environment } from '../environments/environment'; import { FooterComponent } from "src/app/components/footer/footer.component";
import { FooterComponent } from 'src/app/components/footer/footer.component'; import { HeaderComponent } from "src/app/components/header/header.component";
import { HeaderComponent } from 'src/app/components/header/header.component'; import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component";
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; import { TranslateLoader, TranslateModule } from "@ngx-translate/core";
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateHttpLoader } from "@ngx-translate/http-loader";
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { Logger } from "src/app/service/logger.service";
import { Logger } from 'src/app/service/logger.service'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SpinnerComponent } from "src/app/components/spinner/spinner.component";
import { SpinnerComponent } from 'src/app/components/spinner/spinner.component'; import { ConfirmationService, MessageService } from "primeng/api";
import { ConfirmationService, MessageService } from 'primeng/api'; import { DialogService } from "primeng/dynamicdialog";
import { DialogService } from 'primeng/dynamicdialog'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { SidebarComponent } from "./components/sidebar/sidebar.component";
import { SidebarComponent } from './components/sidebar/sidebar.component'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { HomeComponent } from "./components/home/home.component";
import { RedirectComponent } from "./components/redirect/redirect.component";
if (environment.production) { if (environment.production) {
Logger.enableProductionMode(); Logger.enableProductionMode();
@ -35,12 +36,13 @@ export function HttpLoaderFactory(http: HttpClient) {
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
HomeComponent,
FooterComponent, FooterComponent,
HeaderComponent, HeaderComponent,
NotFoundComponent, NotFoundComponent,
SpinnerComponent, SpinnerComponent,
SidebarComponent, SidebarComponent,
HomeComponent,
RedirectComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -48,7 +50,7 @@ export function HttpLoaderFactory(http: HttpClient) {
AppRoutingModule, AppRoutingModule,
SharedModule, SharedModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
defaultLanguage: 'en', defaultLanguage: "en",
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useFactory: HttpLoaderFactory, useFactory: HttpLoaderFactory,

View File

@ -1,9 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NotFoundComponent } from 'src/app/components/error/not-found/not-found.component'; import { NotFoundComponent } from "src/app/components/error/not-found/not-found.component";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
describe('NotFoundComponent', () => { describe("NotFoundComponent", () => {
let component: NotFoundComponent; let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>; let fixture: ComponentFixture<NotFoundComponent>;
@ -20,7 +20,7 @@ describe('NotFoundComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
@Component({ @Component({
selector: 'app-not-found', selector: "app-not-found",
templateUrl: './not-found.component.html', templateUrl: "./not-found.component.html",
styleUrls: ['./not-found.component.scss'], styleUrls: ["./not-found.component.scss"],
}) })
export class NotFoundComponent {} export class NotFoundComponent {}

View File

@ -1,12 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FooterComponent } from 'src/app/components/footer/footer.component'; import { FooterComponent } from "src/app/components/footer/footer.component";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
describe('FooterComponent', () => { describe("FooterComponent", () => {
let component: FooterComponent; let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>; let fixture: ComponentFixture<FooterComponent>;
@ -28,7 +28,7 @@ describe('FooterComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,10 +1,10 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { SettingsService } from 'src/app/service/settings.service'; import { SettingsService } from "src/app/service/settings.service";
@Component({ @Component({
selector: 'app-footer', selector: "app-footer",
templateUrl: './footer.component.html', templateUrl: "./footer.component.html",
styleUrls: ['./footer.component.scss'], styleUrls: ["./footer.component.scss"],
}) })
export class FooterComponent { export class FooterComponent {
constructor(private settings: SettingsService) {} constructor(private settings: SettingsService) {}

View File

@ -1,47 +1,53 @@
<header> <header>
<div class="header"> <div class="header">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<p-button
*ngIf="user"
icon="pi pi-bars"
class="btn icon-btn p-button-text"
(onClick)="toggleSidebar()"
></p-button>
</div>
<div class="logo">
<!-- <img src="/assets/images/logo.svg" alt="logo"/>-->
</div>
<div class="app-name">
<h1>Open-redirect</h1>
</div>
</div> </div>
<div class="logo">
<!-- <img src="/assets/images/logo.svg" alt="logo"/>-->
</div>
<div class="app-name">
<h1>Open-redirect</h1>
</div>
</div>
<div class="flex items-center justify-center">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<p-button <div class="flex items-center justify-center">
type="button" <p-button
icon="pi pi-globe" type="button"
class="btn icon-btn p-button-text" icon="pi pi-globe"
(onClick)="langMenu.toggle($event)"></p-button> class="btn icon-btn p-button-text"
<p-menu (onClick)="langMenu.toggle($event)"></p-button>
#langMenu <p-menu
[popup]="true" #langMenu
[model]="langList" [popup]="true"
class="lang-menu"></p-menu> [model]="langList"
</div> class="lang-menu"></p-menu>
<div class="flex items-center justify-center"> </div>
<p-button <div class="flex items-center justify-center">
*ngIf="!user; else loggedIn" <p-button
icon="pi pi-sign-in" *ngIf="!user; else loggedIn"
class="btn icon-btn p-button-text" icon="pi pi-sign-in"
(onClick)="login()"></p-button> class="btn icon-btn p-button-text"
(onClick)="login()"></p-button>
<ng-template #loggedIn> <ng-template #loggedIn>
<p-button <p-button
type="button" type="button"
icon="pi pi-user" icon="pi pi-user"
class="btn icon-btn p-button-text" class="btn icon-btn p-button-text"
(onClick)="userMenu.toggle($event)"></p-button> (onClick)="userMenu.toggle($event)"></p-button>
<p-menu <p-menu
#userMenu #userMenu
[popup]="true" [popup]="true"
[model]="userMenuList" [model]="userMenuList"
class="user-menu"></p-menu> class="user-menu"></p-menu>
</ng-template> </ng-template>
</div>
</div> </div>
</div>
</header> </header>

View File

@ -1,16 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { HeaderComponent } from 'src/app/components/header/header.component'; import { HeaderComponent } from "src/app/components/header/header.component";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
describe('HeaderComponent', () => { describe("HeaderComponent", () => {
let component: HeaderComponent; let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>; let fixture: ComponentFixture<HeaderComponent>;
@ -41,7 +41,7 @@ describe('HeaderComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,17 +1,18 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from "@angular/core";
import { MenuItem, PrimeNGConfig } from 'primeng/api'; import { MenuItem, PrimeNGConfig } from "primeng/api";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from "rxjs/operators";
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from "@ngx-translate/core";
import { GuiService } from 'src/app/service/gui.service'; import { GuiService } from "src/app/service/gui.service";
import { User } from 'src/app/model/auth/user'; import { User } from "src/app/model/auth/user";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { MenuElement } from 'src/app/model/view/menu-element'; import { MenuElement } from "src/app/model/view/menu-element";
import { SidebarService } from "src/app/service/sidebar.service";
@Component({ @Component({
selector: 'app-header', selector: "app-header",
templateUrl: './header.component.html', templateUrl: "./header.component.html",
styleUrls: ['./header.component.scss'], styleUrls: ["./header.component.scss"],
}) })
export class HeaderComponent implements OnInit, OnDestroy { export class HeaderComponent implements OnInit, OnDestroy {
langList: MenuItem[] = []; langList: MenuItem[] = [];
@ -26,18 +27,24 @@ export class HeaderComponent implements OnInit, OnDestroy {
private translateService: TranslateService, private translateService: TranslateService,
private config: PrimeNGConfig, private config: PrimeNGConfig,
private guiService: GuiService, private guiService: GuiService,
private auth: AuthService private auth: AuthService,
private sidebarService: SidebarService,
) { ) {
this.guiService.isMobile$ this.guiService.isMobile$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(isMobile => { .subscribe((isMobile) => {
this.isMobile = isMobile; this.isMobile = isMobile;
if (isMobile) {
this.sidebarService.hide();
}
}); });
this.auth.user$.pipe(takeUntil(this.unsubscribe$)).subscribe(async user => { this.auth.user$
this.user = user; .pipe(takeUntil(this.unsubscribe$))
await this.initMenuLists(); .subscribe(async (user) => {
}); this.user = user;
await this.initMenuLists();
});
} }
async ngOnInit() { async ngOnInit() {
@ -55,49 +62,24 @@ export class HeaderComponent implements OnInit, OnDestroy {
} }
async initMenuLists() { async initMenuLists() {
await this.initMenuList();
await this.initLangMenuList(); await this.initLangMenuList();
await this.initUserMenuList(); await this.initUserMenuList();
} }
async initMenuList() {
this.menu = [
{
label: 'common.news',
routerLink: ['/'],
icon: 'pi pi-home',
},
{
label: 'header.menu.about',
routerLink: ['/about'],
icon: 'pi pi-info',
},
];
if (this.auth.user$.value) {
this.menu.push({
label: 'header.menu.admin',
routerLink: ['/admin'],
icon: 'pi pi-cog',
visible: await this.auth.isAdmin(),
});
}
}
async initLangMenuList() { async initLangMenuList() {
this.langList = [ this.langList = [
{ {
label: 'English', label: "English",
command: () => { command: () => {
this.translate('en'); this.translate("en");
this.setLang('en'); this.setLang("en");
}, },
}, },
{ {
label: 'Deutsch', label: "Deutsch",
command: () => { command: () => {
this.translate('de'); this.translate("de");
this.setLang('de'); this.setLang("de");
}, },
}, },
]; ];
@ -114,13 +96,13 @@ export class HeaderComponent implements OnInit, OnDestroy {
separator: true, separator: true,
}, },
{ {
label: this.translateService.instant('header.logout'), label: this.translateService.instant("header.logout"),
command: () => { command: () => {
this.auth.logout().then(() => { this.auth.logout().then(() => {
console.log('logout'); console.log("logout");
}); });
}, },
icon: 'pi pi-sign-out', icon: "pi pi-sign-out",
}, },
]; ];
} }
@ -128,18 +110,27 @@ export class HeaderComponent implements OnInit, OnDestroy {
translate(lang: string) { translate(lang: string) {
this.translateService.use(lang); this.translateService.use(lang);
this.translateService this.translateService
.get('primeng') .get("primeng")
.subscribe(res => this.config.setTranslation(res)); .subscribe((res) => this.config.setTranslation(res));
} }
async loadLang() { async loadLang() {
const lang = 'en'; const lang = "en";
this.setLang(lang); this.setLang(lang);
this.translate(lang); this.translate(lang);
} }
setLang(lang: string) { setLang(lang: string) {
// this.settings.setSetting(`lang`, lang); // this.settings.setSetting(`lang`, lang);
console.log('setLang', lang); console.log("setLang", lang);
}
toggleSidebar() {
if (this.sidebarService.visible$.value) {
this.sidebarService.hide();
return;
}
this.sidebarService.show();
} }
} }

View File

@ -1 +0,0 @@
<p>home works!</p>

View File

@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { HomeComponent } from './home.component'; import { HomeComponent } from "./home.component";
describe('HomeComponent', () => { describe("HomeComponent", () => {
let component: HomeComponent; let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>; let fixture: ComponentFixture<HomeComponent>;
@ -16,7 +16,7 @@ describe('HomeComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,8 +1,17 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { AuthService } from "src/app/service/auth.service";
@Component({ @Component({
selector: 'app-home', selector: "app-home",
templateUrl: './home.component.html', templateUrl: "./home.component.html",
styleUrl: './home.component.scss', styleUrl: "./home.component.scss",
}) })
export class HomeComponent {} export class HomeComponent {
constructor(private auth: AuthService) {
if (this.auth.user$.value !== undefined) {
return;
}
this.auth.login().then(() => {});
}
}

View File

@ -0,0 +1 @@
<p>redirect works!</p>

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RedirectComponent } from "./redirect.component";
describe("RedirectComponent", () => {
let component: RedirectComponent;
let fixture: ComponentFixture<RedirectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RedirectComponent],
}).compileComponents();
fixture = TestBed.createComponent(RedirectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { Component, OnInit } from "@angular/core";
import { SidebarService } from "src/app/service/sidebar.service";
import { Router } from "@angular/router";
import { HttpClient } from "@angular/common/http";
import { environment } from "src/environments/environment";
import { ShortUrlDto } from "src/app/model/entities/short-url";
@Component({
selector: "app-redirect",
templateUrl: "./redirect.component.html",
styleUrl: "./redirect.component.scss",
})
export class RedirectComponent implements OnInit {
constructor(
private sidebar: SidebarService,
private router: Router,
private http: HttpClient,
) {
this.sidebar.hide();
}
ngOnInit() {
this.handleUrl();
}
handleUrl() {
let shortUrl = this.router.url;
if (shortUrl === "/") {
return;
}
if (shortUrl.startsWith("/")) {
shortUrl = shortUrl.substring(1);
}
this.http
.get<ShortUrlDto | undefined>(`${environment.api.url}/find/${shortUrl}`)
.subscribe(async (response) => {
if (!response) {
await this.router.navigate(["/404"]);
return;
}
window.location.href = `${environment.api.url}/redirect/${shortUrl}`;
});
}
}

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { SidebarComponent } from './sidebar.component'; import { SidebarComponent } from "./sidebar.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
describe('SidebarComponent', () => { describe("SidebarComponent", () => {
let component: SidebarComponent; let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>; let fixture: ComponentFixture<SidebarComponent>;
@ -40,7 +40,7 @@ describe('SidebarComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,29 +1,29 @@
import {Component, OnDestroy} from '@angular/core'; import { Component, OnDestroy } from "@angular/core";
import {MenuElement} from 'src/app/model/view/menu-element'; import { MenuElement } from "src/app/model/view/menu-element";
import {Subject} from 'rxjs'; import { Subject } from "rxjs";
import {SidebarService} from 'src/app/service/sidebar.service'; import { SidebarService } from "src/app/service/sidebar.service";
import {takeUntil} from 'rxjs/operators'; import { takeUntil } from "rxjs/operators";
@Component({ @Component({
selector: 'app-sidebar', selector: "app-sidebar",
templateUrl: './sidebar.component.html', templateUrl: "./sidebar.component.html",
styleUrl: './sidebar.component.scss', styleUrl: "./sidebar.component.scss",
}) })
export class SidebarComponent implements OnDestroy { export class SidebarComponent implements OnDestroy {
elements: MenuElement[] = []; elements: MenuElement[] = [];
unsubscribe$ = new Subject<void>(); unsubscribe$ = new Subject<void>();
constructor(private sidebar: SidebarService) { constructor(private sidebar: SidebarService) {
this.sidebar.elements$ this.sidebar.elements$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(elements => { .subscribe((elements) => {
this.elements = elements; this.elements = elements;
}); });
} }
ngOnDestroy() { ngOnDestroy() {
this.unsubscribe$.next(); this.unsubscribe$.next();
this.unsubscribe$.complete(); this.unsubscribe$.complete();
} }
} }

View File

@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { SpinnerComponent } from 'src/app/components/spinner/spinner.component'; import { SpinnerComponent } from "src/app/components/spinner/spinner.component";
describe('SpinnerComponent', () => { describe("SpinnerComponent", () => {
let component: SpinnerComponent; let component: SpinnerComponent;
let fixture: ComponentFixture<SpinnerComponent>; let fixture: ComponentFixture<SpinnerComponent>;
@ -18,7 +18,7 @@ describe('SpinnerComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,16 +1,16 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
@Component({ @Component({
selector: 'app-spinner', selector: "app-spinner",
templateUrl: './spinner.component.html', templateUrl: "./spinner.component.html",
styleUrls: ['./spinner.component.scss'], styleUrls: ["./spinner.component.scss"],
}) })
export class SpinnerComponent { export class SpinnerComponent {
showSpinnerState = false; showSpinnerState = false;
constructor(public spinnerService: SpinnerService) { constructor(public spinnerService: SpinnerService) {
this.spinnerService.showSpinnerState$.subscribe(value => { this.spinnerService.showSpinnerState$.subscribe((value) => {
this.showSpinnerState = value; this.showSpinnerState = value;
}); });
} }

View File

@ -1,9 +1,9 @@
import { Directive, inject } from '@angular/core'; import { Directive, inject } from "@angular/core";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
import { FilterService } from 'src/app/service/filter.service'; import { FilterService } from "src/app/service/filter.service";
import { FormGroup } from '@angular/forms'; import { FormGroup } from "@angular/forms";
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from "@angular/router";
@Directive() @Directive()
export abstract class FormPageBase< export abstract class FormPageBase<
@ -27,7 +27,7 @@ export abstract class FormPageBase<
protected dataService = inject(PageDataService) as S; protected dataService = inject(PageDataService) as S;
protected constructor() { protected constructor() {
const id = this.route.snapshot.params['id']; const id = this.route.snapshot.params["id"];
this.validateRoute(id); this.validateRoute(id);
this.buildForm(); this.buildForm();
@ -35,18 +35,18 @@ export abstract class FormPageBase<
validateRoute(id: string | undefined) { validateRoute(id: string | undefined) {
const url = this.router.url; const url = this.router.url;
if (url.endsWith('create') && id !== undefined) { if (url.endsWith("create") && id !== undefined) {
throw new Error('Route ends with create but id is defined'); throw new Error("Route ends with create but id is defined");
} }
if (url.endsWith('edit') && (id === undefined || isNaN(+id))) { if (url.endsWith("edit") && (id === undefined || isNaN(+id))) {
throw new Error('Route ends with edit but id is not a number'); throw new Error("Route ends with edit but id is not a number");
} }
this.nodeId = id ? +id : undefined; this.nodeId = id ? +id : undefined;
} }
close() { close() {
const backRoute = this.nodeId ? '../..' : '..'; const backRoute = this.nodeId ? "../.." : "..";
this.router.navigate([backRoute], { relativeTo: this.route }).then(() => { this.router.navigate([backRoute], { relativeTo: this.route }).then(() => {
this.filterService.onLoad.emit(); this.filterService.onLoad.emit();

View File

@ -1,21 +1,21 @@
import { Directive, inject, OnDestroy } from '@angular/core'; import { Directive, inject, OnDestroy } from "@angular/core";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { Subject } from 'rxjs'; import { Subject } from "rxjs";
import { Logger } from 'src/app/service/logger.service'; import { Logger } from "src/app/service/logger.service";
import { QueryResult } from 'src/app/model/entities/query-result'; import { QueryResult } from "src/app/model/entities/query-result";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
import { FilterService } from 'src/app/service/filter.service'; import { FilterService } from "src/app/service/filter.service";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from 'src/app/model/graphql/filter/sort.model'; import { Sort } from "src/app/model/graphql/filter/sort.model";
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from "rxjs/operators";
import { PaginatorState } from 'primeng/paginator'; import { PaginatorState } from "primeng/paginator";
import { PageColumns } from 'src/app/core/base/page.columns'; import { PageColumns } from "src/app/core/base/page.columns";
import { import {
TableColumn, TableColumn,
TableRequireAnyPermissions, TableRequireAnyPermissions,
} from 'src/app/modules/shared/components/table/table.model'; } from "src/app/modules/shared/components/table/table.model";
const logger = new Logger('PageBase'); const logger = new Logger("PageBase");
@Directive() @Directive()
export abstract class PageBase< export abstract class PageBase<
@ -96,13 +96,13 @@ export abstract class PageBase<
} }
columns: TableColumn<T>[] = columns: TableColumn<T>[] =
'get' in this.columnsService ? this.columnsService.get() : []; "get" in this.columnsService ? this.columnsService.get() : [];
protected unsubscribe$ = new Subject<void>(); protected unsubscribe$ = new Subject<void>();
protected constructor( protected constructor(
useQueryParams = false, useQueryParams = false,
permissions?: TableRequireAnyPermissions permissions?: TableRequireAnyPermissions,
) { ) {
this.subscribeToFilterService(); this.subscribeToFilterService();
this.filterService.reset({ this.filterService.reset({
@ -113,7 +113,7 @@ export abstract class PageBase<
} }
ngOnDestroy(): void { ngOnDestroy(): void {
logger.trace('Destroy component'); logger.trace("Destroy component");
this.unsubscribe$.next(); this.unsubscribe$.next();
this.unsubscribe$.complete(); this.unsubscribe$.complete();
} }
@ -125,26 +125,26 @@ export abstract class PageBase<
this.filterService.filter$ this.filterService.filter$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(filter => { .subscribe((filter) => {
this._filter = filter; this._filter = filter;
}); });
this.filterService.sort$ this.filterService.sort$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(sort => { .subscribe((sort) => {
this._sort = sort; this._sort = sort;
}); });
this.filterService.skip$ this.filterService.skip$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(skip => { .subscribe((skip) => {
this._skip = skip; this._skip = skip;
}); });
this.filterService.take$ this.filterService.take$
.pipe(takeUntil(this.unsubscribe$)) .pipe(takeUntil(this.unsubscribe$))
.subscribe(take => { .subscribe((take) => {
if (take && Object.prototype.hasOwnProperty.call(take, 'showAll')) { if (take && Object.prototype.hasOwnProperty.call(take, "showAll")) {
this._take = 0; this._take = 0;
return; return;
} }

View File

@ -1,67 +1,67 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { TableColumn } from 'src/app/modules/shared/components/table/table.model'; import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { DbModel } from 'src/app/model/entities/db-model'; import { DbModel } from "src/app/model/entities/db-model";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: "root",
}) })
export abstract class PageColumns<T> { export abstract class PageColumns<T> {
abstract get(): TableColumn<T>[]; abstract get(): TableColumn<T>[];
} }
export const ID_COLUMN = { export const ID_COLUMN = {
name: 'id', name: "id",
label: 'common.id', label: "common.id",
type: 'number', type: "number",
filterable: true, filterable: true,
value: (row: { id?: number }) => row.id, value: (row: { id?: number }) => row.id,
class: 'max-w-24', class: "max-w-24",
}; };
export const NAME_COLUMN = { export const NAME_COLUMN = {
name: 'name', name: "name",
label: 'common.name', label: "common.name",
type: 'text', type: "text",
filterable: true, filterable: true,
value: (row: { name?: string }) => row.name, value: (row: { name?: string }) => row.name,
}; };
export const DESCRIPTION_COLUMN = { export const DESCRIPTION_COLUMN = {
name: 'description', name: "description",
label: 'common.description', label: "common.description",
type: 'text', type: "text",
filterable: true, filterable: true,
value: (row: { description?: string }) => row.description, value: (row: { description?: string }) => row.description,
}; };
export const DELETED_COLUMN = { export const DELETED_COLUMN = {
name: 'deleted', name: "deleted",
label: 'common.deleted', label: "common.deleted",
type: 'bool', type: "bool",
filterable: true, filterable: true,
value: (row: DbModel) => row.deleted, value: (row: DbModel) => row.deleted,
}; };
export const EDITOR_COLUMN = { export const EDITOR_COLUMN = {
name: 'editor', name: "editor",
label: 'common.editor', label: "common.editor",
value: (row: DbModel) => row.editor?.username, value: (row: DbModel) => row.editor?.username,
}; };
export const CREATED_UTC_COLUMN = { export const CREATED_UTC_COLUMN = {
name: 'createdUtc', name: "createdUtc",
label: 'common.created', label: "common.created",
type: 'date', type: "date",
value: (row: DbModel) => row.createdUtc, value: (row: DbModel) => row.createdUtc,
class: 'max-w-32', class: "max-w-32",
}; };
export const UPDATED_UTC_COLUMN = { export const UPDATED_UTC_COLUMN = {
name: 'updatedUtc', name: "updatedUtc",
label: 'common.updated', label: "common.updated",
type: 'date', type: "date",
value: (row: DbModel) => row.updatedUtc, value: (row: DbModel) => row.updatedUtc,
class: 'max-w-32', class: "max-w-32",
}; };
export const DB_MODEL_COLUMNS = [ export const DB_MODEL_COLUMNS = [

View File

@ -1,19 +1,19 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { MutationResult } from 'apollo-angular'; import { MutationResult } from "apollo-angular";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from 'src/app/model/graphql/filter/sort.model'; import { Sort } from "src/app/model/graphql/filter/sort.model";
import { QueryResult } from 'src/app/model/entities/query-result'; import { QueryResult } from "src/app/model/entities/query-result";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: "root",
}) })
export abstract class PageDataService<T> { export abstract class PageDataService<T> {
abstract load( abstract load(
filter?: Filter[], filter?: Filter[],
sort?: Sort[], sort?: Sort[],
skip?: number, skip?: number,
take?: number take?: number,
): Observable<QueryResult<T>>; ): Observable<QueryResult<T>>;
abstract loadById(id: number): Observable<T>; abstract loadById(id: number): Observable<T>;
@ -29,12 +29,12 @@ export interface Update<T, U> {
export interface Delete<T> { export interface Delete<T> {
delete( delete(
object: T object: T,
): Observable<T | undefined | boolean> | Observable<MutationResult>; ): Observable<T | undefined | boolean> | Observable<MutationResult>;
} }
export interface Restore<T> { export interface Restore<T> {
restore( restore(
object: T object: T,
): Observable<T | undefined | boolean> | Observable<MutationResult>; ): Observable<T | undefined | boolean> | Observable<MutationResult>;
} }

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { CanActivate } from '@angular/router'; import { CanActivate } from "@angular/router";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: "root",
}) })
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private keycloak: KeycloakService) {} constructor(private keycloak: KeycloakService) {}

View File

@ -1,24 +1,24 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router } from '@angular/router'; import { ActivatedRouteSnapshot, Router } from "@angular/router";
import { Logger } from 'src/app/service/logger.service'; import { Logger } from "src/app/service/logger.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
const log = new Logger('PermissionGuard'); const log = new Logger("PermissionGuard");
@Injectable({ @Injectable({
providedIn: 'root', providedIn: "root",
}) })
export class PermissionGuard { export class PermissionGuard {
constructor( constructor(
private router: Router, private router: Router,
private toast: ToastService, private toast: ToastService,
private auth: AuthService private auth: AuthService,
) {} ) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> { async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
const permissions = route.data['permissions'] as PermissionsEnum[]; const permissions = route.data["permissions"] as PermissionsEnum[];
if (!permissions || permissions.length === 0) { if (!permissions || permissions.length === 0) {
return true; return true;
@ -26,11 +26,11 @@ export class PermissionGuard {
const validate = await this.auth.hasAnyPermissionLazy(permissions); const validate = await this.auth.hasAnyPermissionLazy(permissions);
if (!validate) { if (!validate) {
log.debug('Permission denied', permissions); log.debug("Permission denied", permissions);
this.toast.warn('common.warning', 'error.permission_denied'); this.toast.warn("common.warning", "error.permission_denied");
this.router.navigate(['/']).then(); this.router.navigate(["/"]).then();
} }
log.debug('Permission granted', permissions); log.debug("Permission granted", permissions);
return validate; return validate;
} }
} }

View File

@ -1,17 +1,17 @@
import {KeycloakService} from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import {environment} from 'src/environments/environment'; import { environment } from "src/environments/environment";
export function initializeKeycloak(keycloak: KeycloakService) { export function initializeKeycloak(keycloak: KeycloakService) {
return () => return () =>
keycloak.init({ keycloak.init({
config: { config: {
url: environment.auth.url, url: environment.auth.url,
realm: environment.auth.realm, realm: environment.auth.realm,
clientId: environment.auth.clientId, clientId: environment.auth.clientId,
}, },
initOptions: { initOptions: {
onLoad: 'check-sso', onLoad: "check-sso",
}, },
enableBearerInterceptor: false, enableBearerInterceptor: false,
}); });
} }

View File

@ -1,7 +1,7 @@
import { HttpInterceptorFn } from '@angular/common/http'; import { HttpInterceptorFn } from "@angular/common/http";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { inject } from '@angular/core'; import { inject } from "@angular/core";
import { from, switchMap } from 'rxjs'; import { from, switchMap } from "rxjs";
export const tokenInterceptor: HttpInterceptorFn = (req, next) => { export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
const keycloak = inject(KeycloakService); const keycloak = inject(KeycloakService);
@ -15,14 +15,14 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
} }
return from(keycloak.getToken()).pipe( return from(keycloak.getToken()).pipe(
switchMap(token => { switchMap((token) => {
const modifiedReq = token const modifiedReq = token
? req.clone({ ? req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`), headers: req.headers.set("Authorization", `Bearer ${token}`),
}) })
: req; : req;
return next(modifiedReq); return next(modifiedReq);
}) }),
); );
}; };

View File

@ -1,5 +1,5 @@
import { Role } from 'src/app/model/entities/role'; import { Role } from "src/app/model/entities/role";
import { DbModel } from 'src/app/model/entities/db-model'; import { DbModel } from "src/app/model/entities/db-model";
export interface NotExistingUser { export interface NotExistingUser {
keycloakId: string; keycloakId: string;

View File

@ -1,4 +1,4 @@
import { Theme } from 'src/app/model/view/theme'; import { Theme } from "src/app/model/view/theme";
export interface AppSettings { export interface AppSettings {
TermsUrl: string; TermsUrl: string;

View File

@ -1,5 +1,5 @@
import { DbModel } from 'src/app/model/entities/db-model'; import { DbModel } from "src/app/model/entities/db-model";
import { Permission } from 'src/app/model/entities/role'; import { Permission } from "src/app/model/entities/role";
export interface ApiKey extends DbModel { export interface ApiKey extends DbModel {
identifier?: string; identifier?: string;

View File

@ -1,4 +1,4 @@
import { User } from 'src/app/model/auth/user'; import { User } from "src/app/model/auth/user";
export interface DbModel { export interface DbModel {
id?: number; id?: number;

View File

@ -2,7 +2,7 @@ import { DbModel } from "src/app/model/entities/db-model";
import { Permission } from "src/app/model/entities/role"; import { Permission } from "src/app/model/entities/role";
export interface Group extends DbModel { export interface Group extends DbModel {
name?: string; name: string;
} }
export interface GroupCreateInput { export interface GroupCreateInput {

View File

@ -1,4 +1,4 @@
import { DbModel } from 'src/app/model/entities/db-model'; import { DbModel } from "src/app/model/entities/db-model";
export interface News extends DbModel { export interface News extends DbModel {
title: string; title: string;

View File

@ -1,4 +1,4 @@
import { DbModel } from 'src/app/model/entities/db-model'; import { DbModel } from "src/app/model/entities/db-model";
export interface Role extends DbModel { export interface Role extends DbModel {
name?: string; name?: string;

View File

@ -9,6 +9,11 @@ export interface ShortUrl extends DbModel {
group?: Group; group?: Group;
} }
export interface ShortUrlDto {
id: number;
targetUrl: string;
}
export interface ShortUrlCreateInput { export interface ShortUrlCreateInput {
shortUrl: string; shortUrl: string;
targetUrl: string; targetUrl: string;

View File

@ -1,5 +1,5 @@
import { gql } from 'apollo-angular'; import { gql } from "apollo-angular";
import { EDITOR_FRAGMENT } from 'src/app/model/graphql/editor.query'; import { EDITOR_FRAGMENT } from "src/app/model/graphql/editor.query";
export const DB_MODEL_FRAGMENT = gql` export const DB_MODEL_FRAGMENT = gql`
fragment DB_MODEL on DbModel { fragment DB_MODEL on DbModel {

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-angular'; import { gql } from "apollo-angular";
export const EDITOR_FRAGMENT = gql` export const EDITOR_FRAGMENT = gql`
fragment EDITOR on User { fragment EDITOR on User {

View File

@ -2,6 +2,6 @@
export type Sort = { [key: string]: any }; export type Sort = { [key: string]: any };
export enum SortOrder { export enum SortOrder {
ASC = 'ASC', ASC = "ASC",
DESC = 'DESC', DESC = "DESC",
} }

View File

@ -1,3 +1,3 @@
export enum Themes { export enum Themes {
Default = 'maxlan-dark-theme', Default = "maxlan-dark-theme",
} }

View File

@ -1,38 +1,38 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common'; import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from 'src/app/core/guard/permission.guard'; import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
const routes: Routes = [ const routes: Routes = [
{ {
path: 'users', path: "users",
title: 'Users | Maxlan', title: "Users | Maxlan",
loadChildren: () => loadChildren: () =>
import('src/app/modules/admin/administration/users/users.module').then( import("src/app/modules/admin/administration/users/users.module").then(
m => m.UsersModule (m) => m.UsersModule,
), ),
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.users] }, data: { permissions: [PermissionsEnum.users] },
}, },
{ {
path: 'roles', path: "roles",
title: 'Roles | Maxlan', title: "Roles | Maxlan",
loadChildren: () => loadChildren: () =>
import('src/app/modules/admin/administration/roles/roles.module').then( import("src/app/modules/admin/administration/roles/roles.module").then(
m => m.RolesModule (m) => m.RolesModule,
), ),
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.roles] }, data: { permissions: [PermissionsEnum.roles] },
}, },
{ {
path: 'api-keys', path: "api-keys",
title: 'API Key | Maxlan', title: "API Key | Maxlan",
loadChildren: () => loadChildren: () =>
import( import(
'src/app/modules/admin/administration/api-keys/api-keys.module' "src/app/modules/admin/administration/api-keys/api-keys.module"
).then(m => m.ApiKeysModule), ).then((m) => m.ApiKeysModule),
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { permissions: [PermissionsEnum.apiKeys] }, data: { permissions: [PermissionsEnum.apiKeys] },
}, },

View File

@ -1,11 +1,11 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { import {
DB_MODEL_COLUMNS, DB_MODEL_COLUMNS,
ID_COLUMN, ID_COLUMN,
PageColumns, PageColumns,
} from 'src/app/core/base/page.columns'; } from "src/app/core/base/page.columns";
import { TableColumn } from 'src/app/modules/shared/components/table/table.model'; import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { ApiKey } from 'src/app/model/entities/api-key'; import { ApiKey } from "src/app/model/entities/api-key";
@Injectable() @Injectable()
export class ApiKeysColumns extends PageColumns<ApiKey> { export class ApiKeysColumns extends PageColumns<ApiKey> {
@ -13,16 +13,16 @@ export class ApiKeysColumns extends PageColumns<ApiKey> {
return [ return [
ID_COLUMN, ID_COLUMN,
{ {
name: 'identifier', name: "identifier",
label: 'common.identifier', label: "common.identifier",
type: 'text', type: "text",
filterable: true, filterable: true,
value: (row: ApiKey) => row.identifier, value: (row: ApiKey) => row.identifier,
}, },
{ {
name: 'key', name: "key",
label: 'common.key', label: "common.key",
type: 'password', type: "password",
value: (row: ApiKey) => row.key, value: (row: ApiKey) => row.key,
}, },
...DB_MODEL_COLUMNS, ...DB_MODEL_COLUMNS,

View File

@ -1,25 +1,25 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { import {
Create, Create,
Delete, Delete,
PageDataService, PageDataService,
Restore, Restore,
Update, Update,
} from 'src/app/core/base/page.data.service'; } from "src/app/core/base/page.data.service";
import { Permission } from 'src/app/model/entities/role'; import { Permission } from "src/app/model/entities/role";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from 'src/app/model/graphql/filter/sort.model'; import { Sort } from "src/app/model/graphql/filter/sort.model";
import { Apollo, gql } from 'apollo-angular'; import { Apollo, gql } from "apollo-angular";
import { QueryResult } from 'src/app/model/entities/query-result'; import { QueryResult } from "src/app/model/entities/query-result";
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query'; import { DB_MODEL_FRAGMENT } from "src/app/model/graphql/db-model.query";
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from "rxjs/operators";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
import { import {
ApiKey, ApiKey,
ApiKeyCreateInput, ApiKeyCreateInput,
ApiKeyUpdateInput, ApiKeyUpdateInput,
} from 'src/app/model/entities/api-key'; } from "src/app/model/entities/api-key";
@Injectable() @Injectable()
export class ApiKeysDataService export class ApiKeysDataService
@ -32,7 +32,7 @@ export class ApiKeysDataService
{ {
constructor( constructor(
private spinner: SpinnerService, private spinner: SpinnerService,
private apollo: Apollo private apollo: Apollo,
) { ) {
super(); super();
} }
@ -41,7 +41,7 @@ export class ApiKeysDataService
filter?: Filter[] | undefined, filter?: Filter[] | undefined,
sort?: Sort[] | undefined, sort?: Sort[] | undefined,
skip?: number | undefined, skip?: number | undefined,
take?: number | undefined take?: number | undefined,
): Observable<QueryResult<ApiKey>> { ): Observable<QueryResult<ApiKey>> {
return this.apollo return this.apollo
.query<{ apiKeys: QueryResult<ApiKey> }>({ .query<{ apiKeys: QueryResult<ApiKey> }>({
@ -78,12 +78,12 @@ export class ApiKeysDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.apiKeys)); .pipe(map((result) => result.data.apiKeys));
} }
loadById(id: number): Observable<ApiKey> { loadById(id: number): Observable<ApiKey> {
@ -109,12 +109,12 @@ export class ApiKeysDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.apiKeys.nodes[0])); .pipe(map((result) => result.data.apiKeys.nodes[0]));
} }
create(object: ApiKeyCreateInput): Observable<ApiKey | undefined> { create(object: ApiKeyCreateInput): Observable<ApiKey | undefined> {
@ -140,17 +140,17 @@ export class ApiKeysDataService
variables: { variables: {
input: { input: {
identifier: object.identifier, identifier: object.identifier,
permissions: object.permissions?.map(x => x.id), permissions: object.permissions?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.apiKey.create)); .pipe(map((result) => result.data?.apiKey.create));
} }
update(object: ApiKeyUpdateInput): Observable<ApiKey | undefined> { update(object: ApiKeyUpdateInput): Observable<ApiKey | undefined> {
@ -177,17 +177,17 @@ export class ApiKeysDataService
input: { input: {
id: object.id, id: object.id,
identifier: object.identifier, identifier: object.identifier,
permissions: object.permissions?.map(x => x.id), permissions: object.permissions?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.apiKey.update)); .pipe(map((result) => result.data?.apiKey.update));
} }
delete(object: ApiKey): Observable<boolean> { delete(object: ApiKey): Observable<boolean> {
@ -205,12 +205,12 @@ export class ApiKeysDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.apiKey.delete ?? false)); .pipe(map((result) => result.data?.apiKey.delete ?? false));
} }
restore(object: ApiKey): Observable<boolean> { restore(object: ApiKey): Observable<boolean> {
@ -228,12 +228,12 @@ export class ApiKeysDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.apiKey.restore ?? false)); .pipe(map((result) => result.data?.apiKey.restore ?? false));
} }
getAllPermissions(): Observable<Permission[]> { getAllPermissions(): Observable<Permission[]> {
@ -251,12 +251,12 @@ export class ApiKeysDataService
`, `,
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.permissions.nodes)); .pipe(map((result) => result.data.permissions.nodes));
} }
static provide(): Provider[] { static provide(): Provider[] {

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common'; import { CommonModule } from "@angular/common";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from 'src/app/core/guard/permission.guard'; import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { ApiKeyFormPageComponent } from 'src/app/modules/admin/administration/api-keys/form-page/api-key-form-page.component'; import { ApiKeyFormPageComponent } from "src/app/modules/admin/administration/api-keys/form-page/api-key-form-page.component";
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page'; import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns'; import { ApiKeysColumns } from "src/app/modules/admin/administration/api-keys/api-keys.columns";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: "",
title: 'Admin - ApiKeys | Maxlan', title: "Admin - ApiKeys | Maxlan",
component: ApiKeysPage, component: ApiKeysPage,
children: [ children: [
{ {
path: 'create', path: "create",
component: ApiKeyFormPageComponent, component: ApiKeyFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
@ -24,7 +24,7 @@ const routes: Routes = [
}, },
}, },
{ {
path: 'edit/:id', path: "edit/:id",
component: ApiKeyFormPageComponent, component: ApiKeyFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page'; import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
describe('ApiKeysComponent', () => { describe("ApiKeysComponent", () => {
let component: ApiKeysPage; let component: ApiKeysPage;
let fixture: ComponentFixture<ApiKeysPage>; let fixture: ComponentFixture<ApiKeysPage>;
@ -45,7 +45,7 @@ describe('ApiKeysComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,16 +1,16 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { PageBase } from 'src/app/core/base/page-base'; import { PageBase } from "src/app/core/base/page-base";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { ApiKey } from 'src/app/model/entities/api-key'; import { ApiKey } from "src/app/model/entities/api-key";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { ApiKeysColumns } from 'src/app/modules/admin/administration/api-keys/api-keys.columns'; import { ApiKeysColumns } from "src/app/modules/admin/administration/api-keys/api-keys.columns";
@Component({ @Component({
selector: 'app-api-keys', selector: "app-api-keys",
templateUrl: './api-keys.page.html', templateUrl: "./api-keys.page.html",
styleUrl: './api-keys.page.scss', styleUrl: "./api-keys.page.scss",
}) })
export class ApiKeysPage extends PageBase< export class ApiKeysPage extends PageBase<
ApiKey, ApiKey,
@ -19,7 +19,7 @@ export class ApiKeysPage extends PageBase<
> { > {
constructor( constructor(
private toast: ToastService, private toast: ToastService,
private confirmation: ConfirmationDialogService private confirmation: ConfirmationDialogService,
) { ) {
super(true, { super(true, {
read: [PermissionsEnum.apiKeys], read: [PermissionsEnum.apiKeys],
@ -34,7 +34,7 @@ export class ApiKeysPage extends PageBase<
this.loading = true; this.loading = true;
this.dataService this.dataService
.load(this.filter, this.sort, this.skip, this.take) .load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => { .subscribe((result) => {
this.result = result; this.result = result;
this.loading = false; this.loading = false;
}); });
@ -42,12 +42,12 @@ export class ApiKeysPage extends PageBase<
delete(apiKey: ApiKey): void { delete(apiKey: ApiKey): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.delete.header', header: "dialog.delete.header",
message: 'dialog.delete.message', message: "dialog.delete.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.delete(apiKey).subscribe(() => { this.dataService.delete(apiKey).subscribe(() => {
this.toast.success('action.deleted'); this.toast.success("action.deleted");
this.load(); this.load();
}); });
}, },
@ -57,12 +57,12 @@ export class ApiKeysPage extends PageBase<
restore(apiKey: ApiKey): void { restore(apiKey: ApiKey): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.restore.header', header: "dialog.restore.header",
message: 'dialog.restore.message', message: "dialog.restore.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.restore(apiKey).subscribe(() => { this.dataService.restore(apiKey).subscribe(() => {
this.toast.success('action.restored'); this.toast.success("action.restored");
this.load(); this.load();
}); });
}, },

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('ApiKeyFormpageComponent', () => { describe("ApiKeyFormpageComponent", () => {
let component: RoleFormPageComponent; let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>; let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('ApiKeyFormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { InputSwitchChangeEvent } from 'primeng/inputswitch'; import { InputSwitchChangeEvent } from "primeng/inputswitch";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from "rxjs";
import { FormPageBase } from 'src/app/core/base/form-page-base'; import { FormPageBase } from "src/app/core/base/form-page-base";
import { Permission } from 'src/app/model/entities/role'; import { Permission } from "src/app/model/entities/role";
import { import {
ApiKey, ApiKey,
ApiKeyCreateInput, ApiKeyCreateInput,
ApiKeyUpdateInput, ApiKeyUpdateInput,
} from 'src/app/model/entities/api-key'; } from "src/app/model/entities/api-key";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
@Component({ @Component({
selector: 'app-api-key-form-page', selector: "app-api-key-form-page",
templateUrl: './api-key-form-page.component.html', templateUrl: "./api-key-form-page.component.html",
styleUrl: './api-key-form-page.component.scss', styleUrl: "./api-key-form-page.component.scss",
}) })
export class ApiKeyFormPageComponent extends FormPageBase< export class ApiKeyFormPageComponent extends FormPageBase<
ApiKey, ApiKey,
@ -38,7 +38,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
this.dataService this.dataService
.load([{ id: { equal: this.nodeId } }]) .load([{ id: { equal: this.nodeId } }])
.subscribe(apiKey => { .subscribe((apiKey) => {
this.node = apiKey.nodes[0]; this.node = apiKey.nodes[0];
this.setForm(this.node); this.setForm(this.node);
}); });
@ -47,12 +47,12 @@ export class ApiKeyFormPageComponent extends FormPageBase<
async initializePermissions() { async initializePermissions() {
const permissions = await firstValueFrom( const permissions = await firstValueFrom(
this.dataService.getAllPermissions() this.dataService.getAllPermissions(),
); );
this.allPermissions = permissions; this.allPermissions = permissions;
this.permissionGroups = permissions.reduce( this.permissionGroups = permissions.reduce(
(acc, p) => { (acc, p) => {
const group = p.name.includes('.') ? p.name.split('.')[0] : p.name; const group = p.name.includes(".") ? p.name.split(".")[0] : p.name;
if (!acc[group]) { if (!acc[group]) {
acc[group] = []; acc[group] = [];
@ -60,10 +60,10 @@ export class ApiKeyFormPageComponent extends FormPageBase<
acc[group].push(p); acc[group].push(p);
return acc; return acc;
}, },
{} as { [key: string]: Permission[] } {} as { [key: string]: Permission[] },
); );
permissions.forEach(p => { permissions.forEach((p) => {
this.form.addControl(p.name, new FormControl<boolean>(false)); this.form.addControl(p.name, new FormControl<boolean>(false));
}); });
} }
@ -77,48 +77,48 @@ export class ApiKeyFormPageComponent extends FormPageBase<
id: new FormControl<number | undefined>(undefined), id: new FormControl<number | undefined>(undefined),
identifier: new FormControl<string | undefined>( identifier: new FormControl<string | undefined>(
undefined, undefined,
Validators.required Validators.required,
), ),
}); });
this.form.controls['id'].disable(); this.form.controls["id"].disable();
} }
setForm(node?: ApiKey) { setForm(node?: ApiKey) {
this.form.controls['id'].setValue(node?.id); this.form.controls["id"].setValue(node?.id);
this.form.controls['identifier'].setValue(node?.identifier); this.form.controls["identifier"].setValue(node?.identifier);
if (!node) return; if (!node) return;
const permissions = node.permissions ?? []; const permissions = node.permissions ?? [];
permissions.forEach(p => { permissions.forEach((p) => {
this.form.controls[p.name].setValue(true); this.form.controls[p.name].setValue(true);
}); });
} }
getCreateInput(): ApiKeyCreateInput { getCreateInput(): ApiKeyCreateInput {
return { return {
identifier: this.form.controls['identifier'].pristine identifier: this.form.controls["identifier"].pristine
? undefined ? undefined
: (this.form.controls['identifier'].value ?? undefined), : (this.form.controls["identifier"].value ?? undefined),
permissions: this.allPermissions.filter( permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
), ),
}; };
} }
getUpdateInput(): ApiKeyUpdateInput { getUpdateInput(): ApiKeyUpdateInput {
if (!this.node?.id) { if (!this.node?.id) {
throw new Error('Node id is missing'); throw new Error("Node id is missing");
} }
return { return {
id: this.form.controls['id'].value, id: this.form.controls["id"].value,
identifier: this.form.controls['identifier'].pristine identifier: this.form.controls["identifier"].pristine
? undefined ? undefined
: (this.form.controls['identifier'].value ?? undefined), : (this.form.controls["identifier"].value ?? undefined),
permissions: this.allPermissions.filter( permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
), ),
}; };
} }
@ -126,7 +126,7 @@ export class ApiKeyFormPageComponent extends FormPageBase<
create(apiKey: ApiKeyCreateInput): void { create(apiKey: ApiKeyCreateInput): void {
this.dataService.create(apiKey).subscribe(() => { this.dataService.create(apiKey).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }
@ -134,20 +134,20 @@ export class ApiKeyFormPageComponent extends FormPageBase<
update(apiKey: ApiKeyUpdateInput): void { update(apiKey: ApiKeyUpdateInput): void {
this.dataService.update(apiKey).subscribe(() => { this.dataService.update(apiKey).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }
toggleGroup(event: InputSwitchChangeEvent, group: string) { toggleGroup(event: InputSwitchChangeEvent, group: string) {
this.permissionGroups[group].forEach(p => { this.permissionGroups[group].forEach((p) => {
this.form.controls[p.name].setValue(event.checked); this.form.controls[p.name].setValue(event.checked);
}); });
} }
isGroupChecked(group: string) { isGroupChecked(group: string) {
return this.permissionGroups[group].every( return this.permissionGroups[group].every(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
); );
} }

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('RoleFormpageComponent', () => { describe("RoleFormpageComponent", () => {
let component: RoleFormPageComponent; let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>; let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('RoleFormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import {
Permission, Permission,
Role, Role,
RoleCreateInput, RoleCreateInput,
RoleUpdateInput, RoleUpdateInput,
} from 'src/app/model/entities/role'; } from "src/app/model/entities/role";
import { InputSwitchChangeEvent } from 'primeng/inputswitch'; import { InputSwitchChangeEvent } from "primeng/inputswitch";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from "rxjs";
import { FormPageBase } from 'src/app/core/base/form-page-base'; import { FormPageBase } from "src/app/core/base/form-page-base";
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({ @Component({
selector: 'app-role-form-page', selector: "app-role-form-page",
templateUrl: './role-form-page.component.html', templateUrl: "./role-form-page.component.html",
styleUrl: './role-form-page.component.scss', styleUrl: "./role-form-page.component.scss",
}) })
export class RoleFormPageComponent extends FormPageBase< export class RoleFormPageComponent extends FormPageBase<
Role, Role,
@ -36,7 +36,7 @@ export class RoleFormPageComponent extends FormPageBase<
return; return;
} }
this.dataService.loadById(this.nodeId).subscribe(role => { this.dataService.loadById(this.nodeId).subscribe((role) => {
this.node = role; this.node = role;
this.setForm(this.node); this.setForm(this.node);
}); });
@ -45,12 +45,12 @@ export class RoleFormPageComponent extends FormPageBase<
async initializePermissions() { async initializePermissions() {
const permissions = await firstValueFrom( const permissions = await firstValueFrom(
this.dataService.getAllPermissions() this.dataService.getAllPermissions(),
); );
this.allPermissions = permissions; this.allPermissions = permissions;
this.permissionGroups = permissions.reduce( this.permissionGroups = permissions.reduce(
(acc, p) => { (acc, p) => {
const group = p.name.includes('.') ? p.name.split('.')[0] : p.name; const group = p.name.includes(".") ? p.name.split(".")[0] : p.name;
if (!acc[group]) { if (!acc[group]) {
acc[group] = []; acc[group] = [];
@ -58,10 +58,10 @@ export class RoleFormPageComponent extends FormPageBase<
acc[group].push(p); acc[group].push(p);
return acc; return acc;
}, },
{} as { [key: string]: Permission[] } {} as { [key: string]: Permission[] },
); );
permissions.forEach(p => { permissions.forEach((p) => {
this.form.addControl(p.name, new FormControl<boolean>(false)); this.form.addControl(p.name, new FormControl<boolean>(false));
}); });
} }
@ -76,48 +76,48 @@ export class RoleFormPageComponent extends FormPageBase<
name: new FormControl<string | undefined>(undefined, Validators.required), name: new FormControl<string | undefined>(undefined, Validators.required),
description: new FormControl<string | undefined>(undefined), description: new FormControl<string | undefined>(undefined),
}); });
this.form.controls['id'].disable(); this.form.controls["id"].disable();
} }
setForm(node?: Role) { setForm(node?: Role) {
this.form.controls['id'].setValue(node?.id); this.form.controls["id"].setValue(node?.id);
this.form.controls['name'].setValue(node?.name); this.form.controls["name"].setValue(node?.name);
this.form.controls['description'].setValue(node?.description); this.form.controls["description"].setValue(node?.description);
if (!node) return; if (!node) return;
const permissions = node.permissions ?? []; const permissions = node.permissions ?? [];
permissions.forEach(p => { permissions.forEach((p) => {
this.form.controls[p.name].setValue(true); this.form.controls[p.name].setValue(true);
}); });
} }
getCreateInput(): RoleCreateInput { getCreateInput(): RoleCreateInput {
return { return {
name: this.form.controls['name'].pristine name: this.form.controls["name"].pristine
? undefined ? undefined
: (this.form.controls['name'].value ?? undefined), : (this.form.controls["name"].value ?? undefined),
description: this.form.controls['description'].pristine description: this.form.controls["description"].pristine
? undefined ? undefined
: (this.form.controls['description'].value ?? undefined), : (this.form.controls["description"].value ?? undefined),
permissions: this.allPermissions.filter( permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
), ),
}; };
} }
getUpdateInput(): RoleUpdateInput { getUpdateInput(): RoleUpdateInput {
return { return {
id: this.form.controls['id'].value, id: this.form.controls["id"].value,
name: this.form.controls['name'].pristine name: this.form.controls["name"].pristine
? undefined ? undefined
: this.form.controls['name'].value, : this.form.controls["name"].value,
description: this.form.controls['description'].pristine description: this.form.controls["description"].pristine
? undefined ? undefined
: this.form.controls['description'].value, : this.form.controls["description"].value,
permissions: this.allPermissions.filter( permissions: this.allPermissions.filter(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
), ),
}; };
} }
@ -125,7 +125,7 @@ export class RoleFormPageComponent extends FormPageBase<
create(role: RoleCreateInput): void { create(role: RoleCreateInput): void {
this.dataService.create(role).subscribe(() => { this.dataService.create(role).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }
@ -133,20 +133,20 @@ export class RoleFormPageComponent extends FormPageBase<
update(role: RoleUpdateInput): void { update(role: RoleUpdateInput): void {
this.dataService.update(role).subscribe(() => { this.dataService.update(role).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }
toggleGroup(event: InputSwitchChangeEvent, group: string) { toggleGroup(event: InputSwitchChangeEvent, group: string) {
this.permissionGroups[group].forEach(p => { this.permissionGroups[group].forEach((p) => {
this.form.controls[p.name].setValue(event.checked); this.form.controls[p.name].setValue(event.checked);
}); });
} }
isGroupChecked(group: string) { isGroupChecked(group: string) {
return this.permissionGroups[group].every( return this.permissionGroups[group].every(
p => this.form.controls[p.name].value (p) => this.form.controls[p.name].value,
); );
} }

View File

@ -1,13 +1,13 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { Role } from 'src/app/model/entities/role'; import { Role } from "src/app/model/entities/role";
import { import {
DB_MODEL_COLUMNS, DB_MODEL_COLUMNS,
DESCRIPTION_COLUMN, DESCRIPTION_COLUMN,
ID_COLUMN, ID_COLUMN,
NAME_COLUMN, NAME_COLUMN,
PageColumns, PageColumns,
} from 'src/app/core/base/page.columns'; } from "src/app/core/base/page.columns";
import { TableColumn } from 'src/app/modules/shared/components/table/table.model'; import { TableColumn } from "src/app/modules/shared/components/table/table.model";
@Injectable() @Injectable()
export class RolesColumns extends PageColumns<Role> { export class RolesColumns extends PageColumns<Role> {

View File

@ -1,25 +1,25 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { import {
Create, Create,
Delete, Delete,
PageDataService, PageDataService,
Restore, Restore,
Update, Update,
} from 'src/app/core/base/page.data.service'; } from "src/app/core/base/page.data.service";
import { import {
Permission, Permission,
Role, Role,
RoleCreateInput, RoleCreateInput,
RoleUpdateInput, RoleUpdateInput,
} from 'src/app/model/entities/role'; } from "src/app/model/entities/role";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from 'src/app/model/graphql/filter/sort.model'; import { Sort } from "src/app/model/graphql/filter/sort.model";
import { Apollo, gql } from 'apollo-angular'; import { Apollo, gql } from "apollo-angular";
import { QueryResult } from 'src/app/model/entities/query-result'; import { QueryResult } from "src/app/model/entities/query-result";
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query'; import { DB_MODEL_FRAGMENT } from "src/app/model/graphql/db-model.query";
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from "rxjs/operators";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
@Injectable() @Injectable()
export class RolesDataService export class RolesDataService
@ -32,7 +32,7 @@ export class RolesDataService
{ {
constructor( constructor(
private spinner: SpinnerService, private spinner: SpinnerService,
private apollo: Apollo private apollo: Apollo,
) { ) {
super(); super();
} }
@ -41,7 +41,7 @@ export class RolesDataService
filter?: Filter[] | undefined, filter?: Filter[] | undefined,
sort?: Sort[] | undefined, sort?: Sort[] | undefined,
skip?: number | undefined, skip?: number | undefined,
take?: number | undefined take?: number | undefined,
): Observable<QueryResult<Role>> { ): Observable<QueryResult<Role>> {
return this.apollo return this.apollo
.query<{ roles: QueryResult<Role> }>({ .query<{ roles: QueryResult<Role> }>({
@ -75,12 +75,12 @@ export class RolesDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.roles)); .pipe(map((result) => result.data.roles));
} }
loadById(id: number): Observable<Role> { loadById(id: number): Observable<Role> {
@ -112,12 +112,12 @@ export class RolesDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.roles.nodes[0])); .pipe(map((result) => result.data.roles.nodes[0]));
} }
create(object: RoleCreateInput): Observable<Role | undefined> { create(object: RoleCreateInput): Observable<Role | undefined> {
@ -145,17 +145,17 @@ export class RolesDataService
input: { input: {
name: object.name, name: object.name,
description: object.description, description: object.description,
permissions: object.permissions?.map(x => x.id), permissions: object.permissions?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.role.create)); .pipe(map((result) => result.data?.role.create));
} }
update(object: RoleUpdateInput): Observable<Role | undefined> { update(object: RoleUpdateInput): Observable<Role | undefined> {
@ -184,17 +184,17 @@ export class RolesDataService
id: object.id, id: object.id,
name: object.name, name: object.name,
description: object.description, description: object.description,
permissions: object.permissions?.map(x => x.id), permissions: object.permissions?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.role.update)); .pipe(map((result) => result.data?.role.update));
} }
delete(object: Role): Observable<boolean> { delete(object: Role): Observable<boolean> {
@ -212,12 +212,12 @@ export class RolesDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.role.delete ?? false)); .pipe(map((result) => result.data?.role.delete ?? false));
} }
restore(object: Role): Observable<boolean> { restore(object: Role): Observable<boolean> {
@ -235,12 +235,12 @@ export class RolesDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.role.restore ?? false)); .pipe(map((result) => result.data?.role.restore ?? false));
} }
getAllPermissions(): Observable<Permission[]> { getAllPermissions(): Observable<Permission[]> {
@ -258,12 +258,12 @@ export class RolesDataService
`, `,
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.permissions.nodes)); .pipe(map((result) => result.data.permissions.nodes));
} }
static provide(): Provider[] { static provide(): Provider[] {

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common'; import { CommonModule } from "@angular/common";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from 'src/app/core/guard/permission.guard'; import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page'; import { RolesPage } from "src/app/modules/admin/administration/roles/roles.page";
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns'; import { RolesColumns } from "src/app/modules/admin/administration/roles/roles.columns";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: "",
title: 'Admin - Roles | Maxlan', title: "Admin - Roles | Maxlan",
component: RolesPage, component: RolesPage,
children: [ children: [
{ {
path: 'create', path: "create",
component: RoleFormPageComponent, component: RoleFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
@ -24,7 +24,7 @@ const routes: Routes = [
}, },
}, },
{ {
path: 'edit/:id', path: "edit/:id",
component: RoleFormPageComponent, component: RoleFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RolesPage } from 'src/app/modules/admin/administration/roles/roles.page'; import { RolesPage } from "src/app/modules/admin/administration/roles/roles.page";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
describe('RolesComponent', () => { describe("RolesComponent", () => {
let component: RolesPage; let component: RolesPage;
let fixture: ComponentFixture<RolesPage>; let fixture: ComponentFixture<RolesPage>;
@ -45,7 +45,7 @@ describe('RolesComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { PageBase } from 'src/app/core/base/page-base'; import { PageBase } from "src/app/core/base/page-base";
import { Role } from 'src/app/model/entities/role'; import { Role } from "src/app/model/entities/role";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { RolesColumns } from 'src/app/modules/admin/administration/roles/roles.columns'; import { RolesColumns } from "src/app/modules/admin/administration/roles/roles.columns";
import { RolesDataService } from 'src/app/modules/admin/administration/roles/roles.data.service'; import { RolesDataService } from "src/app/modules/admin/administration/roles/roles.data.service";
@Component({ @Component({
selector: 'app-roles', selector: "app-roles",
templateUrl: './roles.page.html', templateUrl: "./roles.page.html",
styleUrl: './roles.page.scss', styleUrl: "./roles.page.scss",
}) })
export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> { export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
constructor( constructor(
private toast: ToastService, private toast: ToastService,
private confirmation: ConfirmationDialogService private confirmation: ConfirmationDialogService,
) { ) {
super(true, { super(true, {
read: [PermissionsEnum.roles], read: [PermissionsEnum.roles],
@ -30,7 +30,7 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
this.loading = true; this.loading = true;
this.dataService this.dataService
.load(this.filter, this.sort, this.skip, this.take) .load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => { .subscribe((result) => {
this.result = result; this.result = result;
this.loading = false; this.loading = false;
}); });
@ -38,12 +38,12 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
delete(role: Role): void { delete(role: Role): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.delete.header', header: "dialog.delete.header",
message: 'dialog.delete.message', message: "dialog.delete.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.delete(role).subscribe(() => { this.dataService.delete(role).subscribe(() => {
this.toast.success('action.deleted'); this.toast.success("action.deleted");
this.load(); this.load();
}); });
}, },
@ -53,12 +53,12 @@ export class RolesPage extends PageBase<Role, RolesDataService, RolesColumns> {
restore(role: Role): void { restore(role: Role): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.restore.header', header: "dialog.restore.header",
message: 'dialog.restore.message', message: "dialog.restore.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.restore(role).subscribe(() => { this.dataService.restore(role).subscribe(() => {
this.toast.success('action.restored'); this.toast.success("action.restored");
this.load(); this.load();
}); });
}, },

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service'; import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('UserFormpageComponent', () => { describe("UserFormpageComponent", () => {
let component: RoleFormPageComponent; let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>; let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('UserFormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,20 +1,20 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from "@angular/forms";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { FormPageBase } from 'src/app/core/base/form-page-base'; import { FormPageBase } from "src/app/core/base/form-page-base";
import { import {
NotExistingUser, NotExistingUser,
User, User,
UserCreateInput, UserCreateInput,
UserUpdateInput, UserUpdateInput,
} from 'src/app/model/auth/user'; } from "src/app/model/auth/user";
import { Role } from 'src/app/model/entities/role'; import { Role } from "src/app/model/entities/role";
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service'; import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
@Component({ @Component({
selector: 'app-user-form-page', selector: "app-user-form-page",
templateUrl: './user-form-page.component.html', templateUrl: "./user-form-page.component.html",
styleUrl: './user-form-page.component.scss', styleUrl: "./user-form-page.component.scss",
}) })
export class UserFormPageComponent extends FormPageBase< export class UserFormPageComponent extends FormPageBase<
User, User,
@ -27,12 +27,12 @@ export class UserFormPageComponent extends FormPageBase<
constructor(private toast: ToastService) { constructor(private toast: ToastService) {
super(); super();
this.dataService.getAllRoles().subscribe(roles => { this.dataService.getAllRoles().subscribe((roles) => {
this.roles = roles; this.roles = roles;
}); });
if (!this.nodeId) { if (!this.nodeId) {
this.dataService.getNotExistingUsersFromKeycloak().subscribe(users => { this.dataService.getNotExistingUsersFromKeycloak().subscribe((users) => {
this.notExistingUsers = users; this.notExistingUsers = users;
this.node = this.new(); this.node = this.new();
this.setForm(this.node); this.setForm(this.node);
@ -41,7 +41,7 @@ export class UserFormPageComponent extends FormPageBase<
return; return;
} }
this.dataService.loadById(this.nodeId).subscribe(user => { this.dataService.loadById(this.nodeId).subscribe((user) => {
this.node = user; this.node = user;
this.setForm(this.node); this.setForm(this.node);
}); });
@ -59,47 +59,47 @@ export class UserFormPageComponent extends FormPageBase<
email: new FormControl<string | undefined>(undefined), email: new FormControl<string | undefined>(undefined),
roles: new FormControl<Role[]>([]), roles: new FormControl<Role[]>([]),
}); });
this.form.controls['id'].disable(); this.form.controls["id"].disable();
this.form.controls['username'].disable(); this.form.controls["username"].disable();
this.form.controls['email'].disable(); this.form.controls["email"].disable();
} }
setForm(node?: User) { setForm(node?: User) {
this.form.controls['id'].setValue(node?.id); this.form.controls["id"].setValue(node?.id);
this.form.controls['username'].setValue(node?.username); this.form.controls["username"].setValue(node?.username);
this.form.controls['email'].setValue(node?.email); this.form.controls["email"].setValue(node?.email);
this.form.controls['roles'].setValue(node?.roles ?? []); this.form.controls["roles"].setValue(node?.roles ?? []);
if (this.notExistingUsers.length > 0) { if (this.notExistingUsers.length > 0) {
this.form.controls['id'].enable(); this.form.controls["id"].enable();
this.form.controls['keycloakId'].reset(undefined, { required: true }); this.form.controls["keycloakId"].reset(undefined, { required: true });
} }
} }
getCreateInput(): UserCreateInput { getCreateInput(): UserCreateInput {
return { return {
keycloakId: this.form.controls['keycloakId'].pristine keycloakId: this.form.controls["keycloakId"].pristine
? undefined ? undefined
: this.form.controls['keycloakId'].value, : this.form.controls["keycloakId"].value,
roles: this.form.controls['roles'].pristine roles: this.form.controls["roles"].pristine
? undefined ? undefined
: this.form.controls['roles'].value, : this.form.controls["roles"].value,
}; };
} }
getUpdateInput(): UserUpdateInput { getUpdateInput(): UserUpdateInput {
return { return {
id: this.form.controls['id'].value, id: this.form.controls["id"].value,
roles: this.form.controls['roles'].pristine roles: this.form.controls["roles"].pristine
? undefined ? undefined
: this.form.controls['roles'].value, : this.form.controls["roles"].value,
}; };
} }
create(user: UserCreateInput): void { create(user: UserCreateInput): void {
this.dataService.create(user).subscribe(() => { this.dataService.create(user).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }
@ -107,7 +107,7 @@ export class UserFormPageComponent extends FormPageBase<
update(user: UserUpdateInput): void { update(user: UserUpdateInput): void {
this.dataService.update(user).subscribe(() => { this.dataService.update(user).subscribe(() => {
this.spinner.hide(); this.spinner.hide();
this.toast.success('action.created'); this.toast.success("action.created");
this.close(); this.close();
}); });
} }

View File

@ -1,11 +1,11 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { import {
DB_MODEL_COLUMNS, DB_MODEL_COLUMNS,
ID_COLUMN, ID_COLUMN,
PageColumns, PageColumns,
} from 'src/app/core/base/page.columns'; } from "src/app/core/base/page.columns";
import { TableColumn } from 'src/app/modules/shared/components/table/table.model'; import { TableColumn } from "src/app/modules/shared/components/table/table.model";
import { User } from 'src/app/model/auth/user'; import { User } from "src/app/model/auth/user";
@Injectable() @Injectable()
export class UsersColumns extends PageColumns<User> { export class UsersColumns extends PageColumns<User> {
@ -13,15 +13,15 @@ export class UsersColumns extends PageColumns<User> {
return [ return [
ID_COLUMN, ID_COLUMN,
{ {
name: 'username', name: "username",
label: 'user.username', label: "user.username",
type: 'text', type: "text",
value: (row: User) => row.username, value: (row: User) => row.username,
}, },
{ {
name: 'email', name: "email",
label: 'user.email', label: "user.email",
type: 'text', type: "text",
value: (row: User) => row.email, value: (row: User) => row.email,
}, },
...DB_MODEL_COLUMNS, ...DB_MODEL_COLUMNS,

View File

@ -1,26 +1,26 @@
import { Injectable, Provider } from '@angular/core'; import { Injectable, Provider } from "@angular/core";
import { Observable } from 'rxjs'; import { Observable } from "rxjs";
import { import {
Create, Create,
Delete, Delete,
PageDataService, PageDataService,
Restore, Restore,
Update, Update,
} from 'src/app/core/base/page.data.service'; } from "src/app/core/base/page.data.service";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort } from 'src/app/model/graphql/filter/sort.model'; import { Sort } from "src/app/model/graphql/filter/sort.model";
import { Apollo, gql } from 'apollo-angular'; import { Apollo, gql } from "apollo-angular";
import { QueryResult } from 'src/app/model/entities/query-result'; import { QueryResult } from "src/app/model/entities/query-result";
import { DB_MODEL_FRAGMENT } from 'src/app/model/graphql/db-model.query'; import { DB_MODEL_FRAGMENT } from "src/app/model/graphql/db-model.query";
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from "rxjs/operators";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
import { import {
NotExistingUser, NotExistingUser,
User, User,
UserCreateInput, UserCreateInput,
UserUpdateInput, UserUpdateInput,
} from 'src/app/model/auth/user'; } from "src/app/model/auth/user";
import { Role } from 'src/app/model/entities/role'; import { Role } from "src/app/model/entities/role";
@Injectable() @Injectable()
export class UsersDataService export class UsersDataService
@ -33,7 +33,7 @@ export class UsersDataService
{ {
constructor( constructor(
private spinner: SpinnerService, private spinner: SpinnerService,
private apollo: Apollo private apollo: Apollo,
) { ) {
super(); super();
} }
@ -42,7 +42,7 @@ export class UsersDataService
filter?: Filter[] | undefined, filter?: Filter[] | undefined,
sort?: Sort[] | undefined, sort?: Sort[] | undefined,
skip?: number | undefined, skip?: number | undefined,
take?: number | undefined take?: number | undefined,
): Observable<QueryResult<User>> { ): Observable<QueryResult<User>> {
return this.apollo return this.apollo
.query<{ users: QueryResult<User> }>({ .query<{ users: QueryResult<User> }>({
@ -77,12 +77,12 @@ export class UsersDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.users)); .pipe(map((result) => result.data.users));
} }
loadById(id: number): Observable<User> { loadById(id: number): Observable<User> {
@ -115,12 +115,12 @@ export class UsersDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.users.nodes[0])); .pipe(map((result) => result.data.users.nodes[0]));
} }
create(object: UserCreateInput): Observable<User | undefined> { create(object: UserCreateInput): Observable<User | undefined> {
@ -145,17 +145,17 @@ export class UsersDataService
variables: { variables: {
input: { input: {
keycloakId: object.keycloakId, keycloakId: object.keycloakId,
roles: object.roles?.map(x => x.id), roles: object.roles?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.user.create)); .pipe(map((result) => result.data?.user.create));
} }
update(object: UserUpdateInput): Observable<User | undefined> { update(object: UserUpdateInput): Observable<User | undefined> {
@ -180,17 +180,17 @@ export class UsersDataService
variables: { variables: {
input: { input: {
id: object.id, id: object.id,
roles: object.roles?.map(x => x.id), roles: object.roles?.map((x) => x.id),
}, },
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.user.update)); .pipe(map((result) => result.data?.user.update));
} }
delete(object: User): Observable<boolean> { delete(object: User): Observable<boolean> {
@ -208,12 +208,12 @@ export class UsersDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.user.delete ?? false)); .pipe(map((result) => result.data?.user.delete ?? false));
} }
restore(object: User): Observable<boolean> { restore(object: User): Observable<boolean> {
@ -231,12 +231,12 @@ export class UsersDataService
}, },
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data?.user.restore ?? false)); .pipe(map((result) => result.data?.user.restore ?? false));
} }
getAllRoles(): Observable<Role[]> { getAllRoles(): Observable<Role[]> {
@ -254,12 +254,12 @@ export class UsersDataService
`, `,
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.roles.nodes)); .pipe(map((result) => result.data.roles.nodes));
} }
getNotExistingUsersFromKeycloak(): Observable<NotExistingUser[]> { getNotExistingUsersFromKeycloak(): Observable<NotExistingUser[]> {
@ -277,12 +277,12 @@ export class UsersDataService
`, `,
}) })
.pipe( .pipe(
catchError(err => { catchError((err) => {
this.spinner.hide(); this.spinner.hide();
throw err; throw err;
}) }),
) )
.pipe(map(result => result.data.notExistingUsersFromKeycloak.nodes)); .pipe(map((result) => result.data.notExistingUsersFromKeycloak.nodes));
} }
static provide(): Provider[] { static provide(): Provider[] {

View File

@ -1,22 +1,22 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common'; import { CommonModule } from "@angular/common";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { PermissionGuard } from 'src/app/core/guard/permission.guard'; import { PermissionGuard } from "src/app/core/guard/permission.guard";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { UserFormPageComponent } from 'src/app/modules/admin/administration/users/form-page/user-form-page.component'; import { UserFormPageComponent } from "src/app/modules/admin/administration/users/form-page/user-form-page.component";
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page'; import { UsersPage } from "src/app/modules/admin/administration/users/users.page";
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service'; import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns'; import { UsersColumns } from "src/app/modules/admin/administration/users/users.columns";
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: "",
title: 'Admin - Users | Maxlan', title: "Admin - Users | Maxlan",
component: UsersPage, component: UsersPage,
children: [ children: [
{ {
path: 'create', path: "create",
component: UserFormPageComponent, component: UserFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {
@ -24,7 +24,7 @@ const routes: Routes = [
}, },
}, },
{ {
path: 'edit/:id', path: "edit/:id",
component: UserFormPageComponent, component: UserFormPageComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { data: {

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { UsersPage } from 'src/app/modules/admin/administration/users/users.page'; import { UsersPage } from "src/app/modules/admin/administration/users/users.page";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service'; import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
describe('UsersComponent', () => { describe("UsersComponent", () => {
let component: UsersPage; let component: UsersPage;
let fixture: ComponentFixture<UsersPage>; let fixture: ComponentFixture<UsersPage>;
@ -45,7 +45,7 @@ describe('UsersComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,21 +1,21 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { PageBase } from 'src/app/core/base/page-base'; import { PageBase } from "src/app/core/base/page-base";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from 'src/app/service/confirmation-dialog.service'; import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { User } from 'src/app/model/auth/user'; import { User } from "src/app/model/auth/user";
import { UsersColumns } from 'src/app/modules/admin/administration/users/users.columns'; import { UsersColumns } from "src/app/modules/admin/administration/users/users.columns";
import { UsersDataService } from 'src/app/modules/admin/administration/users/users.data.service'; import { UsersDataService } from "src/app/modules/admin/administration/users/users.data.service";
@Component({ @Component({
selector: 'app-users', selector: "app-users",
templateUrl: './users.page.html', templateUrl: "./users.page.html",
styleUrl: './users.page.scss', styleUrl: "./users.page.scss",
}) })
export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> { export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
constructor( constructor(
private toast: ToastService, private toast: ToastService,
private confirmation: ConfirmationDialogService private confirmation: ConfirmationDialogService,
) { ) {
super(true, { super(true, {
read: [PermissionsEnum.users], read: [PermissionsEnum.users],
@ -30,7 +30,7 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
this.loading = true; this.loading = true;
this.dataService this.dataService
.load(this.filter, this.sort, this.skip, this.take) .load(this.filter, this.sort, this.skip, this.take)
.subscribe(result => { .subscribe((result) => {
this.result = result; this.result = result;
this.loading = false; this.loading = false;
}); });
@ -38,12 +38,12 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
delete(user: User): void { delete(user: User): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.delete.header', header: "dialog.delete.header",
message: 'dialog.delete.message', message: "dialog.delete.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.delete(user).subscribe(() => { this.dataService.delete(user).subscribe(() => {
this.toast.success('action.deleted'); this.toast.success("action.deleted");
this.load(); this.load();
}); });
}, },
@ -53,12 +53,12 @@ export class UsersPage extends PageBase<User, UsersDataService, UsersColumns> {
restore(user: User): void { restore(user: User): void {
this.confirmation.confirmDialog({ this.confirmation.confirmDialog({
header: 'dialog.restore.header', header: "dialog.restore.header",
message: 'dialog.restore.message', message: "dialog.restore.message",
accept: () => { accept: () => {
this.loading = true; this.loading = true;
this.dataService.restore(user).subscribe(() => { this.dataService.restore(user).subscribe(() => {
this.toast.success('action.restored'); this.toast.success("action.restored");
this.load(); this.load();
}); });
}, },

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('ApiKeyFormpageComponent', () => { describe("ApiKeyFormpageComponent", () => {
let component: RoleFormPageComponent; let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>; let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('ApiKeyFormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page'; import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
describe('ApiKeysComponent', () => { describe("ApiKeysComponent", () => {
let component: ApiKeysPage; let component: ApiKeysPage;
let fixture: ComponentFixture<ApiKeysPage>; let fixture: ComponentFixture<ApiKeysPage>;
@ -45,7 +45,7 @@ describe('ApiKeysComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,10 +1,9 @@
import {NgModule} from '@angular/core'; import { NgModule } from "@angular/core";
import {CommonModule} from '@angular/common'; import { CommonModule } from "@angular/common";
import {RouterModule, Routes} from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import {SharedModule} from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
const routes: Routes = [ const routes: Routes = [];
];
@NgModule({ @NgModule({
declarations: [], declarations: [],

View File

@ -20,7 +20,7 @@
<input pInputText class="value" type="number" formControlName="id"/> <input pInputText class="value" type="number" formControlName="id"/>
</div> </div>
<div class="form-page-input"> <div class="form-page-input">
<p class="label">{{ 'common.short_url' | translate }}</p> <p class="label">{{ 'short_url.short_url' | translate }}</p>
<input <input
pInputText pInputText
class="value" class="value"
@ -28,7 +28,7 @@
formControlName="shortUrl"/> formControlName="shortUrl"/>
</div> </div>
<div class="form-page-input"> <div class="form-page-input">
<p class="label">{{ 'common.target_url' | translate }}</p> <p class="label">{{ 'short_url.target_url' | translate }}</p>
<input <input
pInputText pInputText
class="value" class="value"

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RoleFormPageComponent } from 'src/app/modules/admin/administration/roles/form-page/role-form-page.component'; import { RoleFormPageComponent } from "src/app/modules/admin/administration/roles/form-page/role-form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('ApiKeyFormpageComponent', () => { describe("ApiKeyFormpageComponent", () => {
let component: RoleFormPageComponent; let component: RoleFormPageComponent;
let fixture: ComponentFixture<RoleFormPageComponent>; let fixture: ComponentFixture<RoleFormPageComponent>;
@ -44,7 +44,7 @@ describe('ApiKeyFormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -65,9 +65,22 @@ export class ShortUrlFormPageComponent extends FormPageBase<
this.form.controls["id"].disable(); this.form.controls["id"].disable();
} }
private generateRandomString(length: number): string {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
setForm(node?: ShortUrl) { setForm(node?: ShortUrl) {
this.form.controls["id"].setValue(node?.id); this.form.controls["id"].setValue(node?.id);
this.form.controls["shortUrl"].setValue(node?.shortUrl); this.form.controls["shortUrl"].setValue(
node?.shortUrl ?? this.generateRandomString(8),
);
this.form.controls["targetUrl"].setValue(node?.targetUrl); this.form.controls["targetUrl"].setValue(node?.targetUrl);
this.form.controls["description"].setValue(node?.description); this.form.controls["description"].setValue(node?.description);
this.form.controls["groupId"].setValue(node?.group?.id); this.form.controls["groupId"].setValue(node?.group?.id);
@ -75,9 +88,7 @@ export class ShortUrlFormPageComponent extends FormPageBase<
getCreateInput(): ShortUrlCreateInput { getCreateInput(): ShortUrlCreateInput {
return { return {
shortUrl: this.form.controls["shortUrl"].pristine shortUrl: this.form.controls["shortUrl"].value ?? undefined,
? undefined
: (this.form.controls["shortUrl"].value ?? undefined),
targetUrl: this.form.controls["targetUrl"].pristine targetUrl: this.form.controls["targetUrl"].pristine
? undefined ? undefined
: (this.form.controls["targetUrl"].value ?? undefined), : (this.form.controls["targetUrl"].value ?? undefined),

View File

@ -14,14 +14,14 @@ export class ShortUrlsColumns extends PageColumns<ShortUrl> {
ID_COLUMN, ID_COLUMN,
{ {
name: "short_url", name: "short_url",
label: "common.short_url", label: "short_url.short_url",
type: "text", type: "text",
filterable: true, filterable: true,
value: (row: ShortUrl) => row.shortUrl, value: (row: ShortUrl) => row.shortUrl,
}, },
{ {
name: "target_url", name: "target_url",
label: "common.target_url", label: "short_url.target_url",
type: "text", type: "text",
filterable: true, filterable: true,
value: (row: ShortUrl) => row.targetUrl, value: (row: ShortUrl) => row.targetUrl,
@ -33,13 +33,6 @@ export class ShortUrlsColumns extends PageColumns<ShortUrl> {
filterable: true, filterable: true,
value: (row: ShortUrl) => row.description, value: (row: ShortUrl) => row.description,
}, },
{
name: "group",
label: "common.group",
type: "text",
filterable: true,
value: (row: ShortUrl) => row.group?.name,
},
...DB_MODEL_COLUMNS, ...DB_MODEL_COLUMNS,
]; ];
} }

View File

@ -1,19 +1,107 @@
<app-table
[rows]="result.nodes"
[columns]="columns"
[rowsPerPageOptions]="rowsPerPageOptions"
[totalCount]="result.totalCount"
[requireAnyPermissions]="requiredPermissions"
countHeaderTranslation="group.count_header"
[loading]="loading"
[(filter)]="filter"
[(sort)]="sort"
[(skip)]="skip"
[(take)]="take"
(load)="load()"
[create]="true"
[update]="true"
(delete)="delete($event)"
(restore)="restore($event)"></app-table>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<ng-template #shortUrl let-url>
<div class="flex flex-col gap-2 bg rounded-l p-3">
<div class="flex flex-col gap-2">
<h3>{{ url.shortUrl }}</h3>
<div class="grid-container">
<span class="grid-label font-bold">{{ 'short_url.target_url' | translate }}:</span>
<a class="grid-value" [href]="url.targetUrl" target="_blank">{{ url.targetUrl }}</a>
</div>
<div class="grid-container">
<span class="grid-label font-bold">{{ 'common.description' | translate }}:</span>
<span class="grid-value">{{ url.description }}</span>
</div>
<div class="grid-container">
<span class="grid-label font-bold">{{ 'short_url.visits' | translate }}:</span>
<span class="grid-value">{{ url.visits }}</span>
</div>
</div>
<div class="flex justify-end">
<p-button
class="icon-btn btn"
icon="pi pi-clone"
(onClick)="copy(url.shortUrl)"></p-button>
<p-button
class="icon-btn btn"
icon="pi pi-external-link"
(onClick)="open(url.shortUrl)"></p-button>
<p-button
*ngIf="hasPermissions.update"
class="icon-btn btn"
icon="pi pi-pencil"
tooltipPosition="left"
pTooltip="{{ 'table.update' | translate }}"
[disabled]="url?.deleted"
routerLink="edit/{{ url.id }}"></p-button>
<p-button
*ngIf="hasPermissions.delete"
class="icon-btn btn danger-icon-btn"
icon="pi pi-trash"
tooltipPosition="left"
pTooltip="{{ 'table.delete' | translate }}"
[disabled]="url?.deleted"
(click)="delete(url)"></p-button>
<p-button
*ngIf="url?.deleted && hasPermissions.restore"
class="icon-btn btn"
icon="pi pi-undo"
tooltipPosition="left"
pTooltip="{{ 'table.restore' | translate }}"
(click)="restore(url)"></p-button>
</div>
</div>
</ng-template>
<div class="flex flex-col gap-2">
<div class="flex justify-between gap-1">
<div><h1>{{ 'short_url.short_url' | translate }}</h1></div>
<div class="flex space-x-2">
<p-button
*ngIf="hasPermissions.create"
class="icon-btn btn"
icon="pi pi-plus"
tooltipPosition="left"
pTooltip="{{ 'table.create' | translate }}"
routerLink="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>
</div>
</div>
<div class="divider"></div>
<div class="bg2 rounded-xl p-3">
<div *ngFor="let url of shortUrlsWithoutGroup">
<ng-template [ngTemplateOutlet]="shortUrl" [ngTemplateOutletContext]="{ $implicit: url }"></ng-template>
</div>
</div>
<div *ngFor="let group of groupsFromGroupedShortUrls" class="bg2 rounded-xl p-3">
<div><h2>{{ group }}</h2></div>
<div class="flex flex-col gap-1.5">
<ng-template *ngFor="let url of groupedShortUrls[group]" [ngTemplateOutlet]="shortUrl"
[ngTemplateOutletContext]="{ $implicit: url }"></ng-template>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
.grid-label {
grid-column: 1;
}
.grid-value {
grid-column: 2;
word-break: break-all;
}
}

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ApiKeysPage } from 'src/app/modules/admin/administration/api-keys/api-keys.page'; import { ApiKeysPage } from "src/app/modules/admin/administration/api-keys/api-keys.page";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { PageDataService } from 'src/app/core/base/page.data.service'; import { PageDataService } from "src/app/core/base/page.data.service";
import { ApiKeysDataService } from 'src/app/modules/admin/administration/api-keys/api-keys.data.service'; import { ApiKeysDataService } from "src/app/modules/admin/administration/api-keys/api-keys.data.service";
describe('ApiKeysComponent', () => { describe("ApiKeysComponent", () => {
let component: ApiKeysPage; let component: ApiKeysPage;
let fixture: ComponentFixture<ApiKeysPage>; let fixture: ComponentFixture<ApiKeysPage>;
@ -45,7 +45,7 @@ describe('ApiKeysComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,4 +1,4 @@
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { PageBase } from "src/app/core/base/page-base"; import { PageBase } from "src/app/core/base/page-base";
import { ToastService } from "src/app/service/toast.service"; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service"; import { ConfirmationDialogService } from "src/app/service/confirmation-dialog.service";
@ -6,20 +6,44 @@ import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { ShortUrl } from "src/app/model/entities/short-url"; import { ShortUrl } from "src/app/model/entities/short-url";
import { ShortUrlsDataService } from "src/app/modules/admin/short-urls/short-urls.data.service"; import { ShortUrlsDataService } from "src/app/modules/admin/short-urls/short-urls.data.service";
import { ShortUrlsColumns } from "src/app/modules/admin/short-urls/short-urls.columns"; import { ShortUrlsColumns } from "src/app/modules/admin/short-urls/short-urls.columns";
import { AuthService } from "src/app/service/auth.service";
import { Filter } from "src/app/model/graphql/filter/filter.model";
@Component({ @Component({
selector: "app-short-urls", selector: "app-short-urls",
templateUrl: "./short-urls.page.html", templateUrl: "./short-urls.page.html",
styleUrl: "./short-urls.page.scss", styleUrl: "./short-urls.page.scss",
}) })
export class ShortUrlsPage extends PageBase< export class ShortUrlsPage
ShortUrl, extends PageBase<ShortUrl, ShortUrlsDataService, ShortUrlsColumns>
ShortUrlsDataService, implements OnInit
ShortUrlsColumns {
> { shortUrlsWithoutGroup: ShortUrl[] = [];
groupedShortUrls: { [key: string]: ShortUrl[] } = {};
get groupsFromGroupedShortUrls() {
return Object.keys(this.groupedShortUrls);
}
private hide_deleted_filter: Filter = { deleted: { equal: false } };
get showDeleted() {
return !this.filter.some(
(f) => JSON.stringify(f) === JSON.stringify(this.hide_deleted_filter),
);
}
protected hasPermissions = {
read: false,
create: false,
update: false,
delete: false,
restore: false,
};
constructor( constructor(
private toast: ToastService, private toast: ToastService,
private confirmation: ConfirmationDialogService, private confirmation: ConfirmationDialogService,
private auth: AuthService,
) { ) {
super(true, { super(true, {
read: [PermissionsEnum.shortUrls], read: [PermissionsEnum.shortUrls],
@ -30,12 +54,41 @@ export class ShortUrlsPage extends PageBase<
}); });
} }
async ngOnInit() {
this.hasPermissions = {
read: await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.read ?? [],
),
create: await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.create ?? [],
),
update: await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.update ?? [],
),
delete: await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.delete ?? [],
),
restore: await this.auth.hasAnyPermissionLazy(
this.requiredPermissions.restore ?? [],
),
};
}
load(): void { load(): void {
this.loading = true; this.loading = true;
this.dataService this.dataService
.load(this.filter, this.sort, this.skip, this.take) .load(this.filter, this.sort, this.skip, this.take)
.subscribe((result) => { .subscribe((result) => {
this.result = result; this.result = result;
this.shortUrlsWithoutGroup = this.getShortUrlsWithoutGroup();
this.groupedShortUrls = this.getShortUrlsWithGroup();
console.warn(
"result",
result,
this.shortUrlsWithoutGroup,
this.groupedShortUrls,
);
this.loading = false; this.loading = false;
}); });
} }
@ -69,4 +122,54 @@ export class ShortUrlsPage extends PageBase<
messageParams: { entity: group.shortUrl }, messageParams: { entity: group.shortUrl },
}); });
} }
toggleShowDeleted() {
if (!this.showDeleted) {
this.filter.splice(this.filter.indexOf(this.hide_deleted_filter), 1);
} else {
this.filter.push(this.hide_deleted_filter);
}
this.load();
}
resetSort() {
this.sort = [];
this.load();
}
resetFilters() {
this.filter = [];
this.load();
}
open(url: string) {
window.open(url, "_blank");
}
copy(val: string) {
navigator.clipboard.writeText(`${window.origin}/${val}`).then(() => {
this.toast.info("common.copied", "common.copied_to_clipboard");
});
}
getShortUrlsWithoutGroup(): ShortUrl[] {
return this.result.nodes.filter((shortUrl) => !shortUrl.group);
}
getShortUrlsWithGroup() {
const groupedShortUrls: { [key: string]: ShortUrl[] } = {};
this.result.nodes.forEach((shortUrl) => {
if (!shortUrl.group) return;
const groupName = shortUrl.group.name;
if (!groupedShortUrls[groupName]) {
groupedShortUrls[groupName] = [];
}
groupedShortUrls[groupName].push(shortUrl);
});
return groupedShortUrls;
}
} }

View File

@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { MenuBarComponent } from 'src/app/modules/shared/components/menu-bar/menu-bar.component'; import { MenuBarComponent } from "src/app/modules/shared/components/menu-bar/menu-bar.component";
describe('MenuBarComponent', () => { describe("MenuBarComponent", () => {
let component: MenuBarComponent; let component: MenuBarComponent;
let fixture: ComponentFixture<MenuBarComponent>; let fixture: ComponentFixture<MenuBarComponent>;
@ -16,7 +16,7 @@ describe('MenuBarComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from "@angular/core";
import { MenuElement } from 'src/app/model/view/menu-element'; import { MenuElement } from "src/app/model/view/menu-element";
@Component({ @Component({
selector: 'app-menu-bar', selector: "app-menu-bar",
templateUrl: './menu-bar.component.html', templateUrl: "./menu-bar.component.html",
styleUrl: './menu-bar.component.scss', styleUrl: "./menu-bar.component.scss",
}) })
export class MenuBarComponent { export class MenuBarComponent {
@Input() elements: MenuElement[] = []; @Input() elements: MenuElement[] = [];

View File

@ -1,8 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { SideMenuComponent } from './side-menu.component'; import { SideMenuComponent } from "./side-menu.component";
describe('SideMenuComponent', () => { describe("SideMenuComponent", () => {
let component: SideMenuComponent; let component: SideMenuComponent;
let fixture: ComponentFixture<SideMenuComponent>; let fixture: ComponentFixture<SideMenuComponent>;
@ -16,7 +16,7 @@ describe('SideMenuComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,10 +1,10 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from "@angular/core";
import { MenuElement } from 'src/app/model/view/menu-element'; import { MenuElement } from "src/app/model/view/menu-element";
@Component({ @Component({
selector: 'app-side-menu', selector: "app-side-menu",
templateUrl: './side-menu.component.html', templateUrl: "./side-menu.component.html",
styleUrl: './side-menu.component.scss', styleUrl: "./side-menu.component.scss",
}) })
export class SideMenuComponent { export class SideMenuComponent {
@Input() elements: MenuElement[] = []; @Input() elements: MenuElement[] = [];

View File

@ -1,7 +1,7 @@
<p-sidebar <p-sidebar
[visible]="true" [visible]="true"
position="right" position="right"
[style]="{ width: 'min-content', minWidth: '500px' }" [style]="{ width: 'min-content', minWidth: '350px' }"
(onHide)="onClose.emit()" (onHide)="onClose.emit()"
(visibleChange)="!$event ? onClose.emit() : undefined"> (visibleChange)="!$event ? onClose.emit() : undefined">
<ng-template pTemplate="headless"> <ng-template pTemplate="headless">

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormPageComponent } from 'src/app/modules/shared/components/slidein/form-page.component'; import { FormPageComponent } from "src/app/modules/shared/components/slidein/form-page.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
import { FormGroup } from '@angular/forms'; import { FormGroup } from "@angular/forms";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
describe('FormpageComponent', () => { describe("FormpageComponent", () => {
let component: FormPageComponent<string>; let component: FormPageComponent<string>;
let fixture: ComponentFixture<FormPageComponent<string>>; let fixture: ComponentFixture<FormPageComponent<string>>;
@ -46,7 +46,7 @@ describe('FormpageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -5,18 +5,18 @@ import {
Input, Input,
Output, Output,
TemplateRef, TemplateRef,
} from '@angular/core'; } from "@angular/core";
import { FormGroup } from '@angular/forms'; import { FormGroup } from "@angular/forms";
import { SpinnerService } from 'src/app/service/spinner.service'; import { SpinnerService } from "src/app/service/spinner.service";
import { import {
FormPageContentDirective, FormPageContentDirective,
FormPageHeaderDirective, FormPageHeaderDirective,
} from 'src/app/modules/shared/form'; } from "src/app/modules/shared/form";
@Component({ @Component({
selector: 'app-form-page', selector: "app-form-page",
templateUrl: './form-page.component.html', templateUrl: "./form-page.component.html",
styleUrl: './form-page.component.scss', styleUrl: "./form-page.component.scss",
}) })
export class FormPageComponent<T> { export class FormPageComponent<T> {
@Input({ required: true }) formGroup!: FormGroup; @Input({ required: true }) formGroup!: FormGroup;

View File

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

View File

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { TableComponent } from './table.component'; import { TableComponent } from "./table.component";
import { SharedModule } from 'src/app/modules/shared/shared.module'; import { SharedModule } from "src/app/modules/shared/shared.module";
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from "@ngx-translate/core";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from "keycloak-angular";
import { ErrorHandlingService } from 'src/app/service/error-handling.service'; import { ErrorHandlingService } from "src/app/service/error-handling.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
import { ConfirmationService, MessageService } from 'primeng/api'; import { ConfirmationService, MessageService } from "primeng/api";
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from "@angular/router";
import { of } from 'rxjs'; import { of } from "rxjs";
describe('TableComponent', () => { describe("TableComponent", () => {
let component: TableComponent<string>; let component: TableComponent<string>;
let fixture: ComponentFixture<TableComponent<string>>; let fixture: ComponentFixture<TableComponent<string>>;
@ -40,7 +40,7 @@ describe('TableComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -1,26 +1,44 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import {
Component,
ContentChild,
Directive,
EventEmitter,
Input,
OnInit,
Output,
TemplateRef,
} from "@angular/core";
import {
FilterMode,
ResolvedTableColumn, ResolvedTableColumn,
TableColumn, TableColumn,
TableRequireAnyPermissions, TableRequireAnyPermissions,
} from 'src/app/modules/shared/components/table/table.model'; } from "src/app/modules/shared/components/table/table.model";
import { TableLazyLoadEvent } from 'primeng/table'; import { TableLazyLoadEvent } from "primeng/table";
import { RowsPerPageOption } from 'src/app/service/filter.service'; import { RowsPerPageOption } from "src/app/service/filter.service";
import { Filter } from 'src/app/model/graphql/filter/filter.model'; import { Filter } from "src/app/model/graphql/filter/filter.model";
import { Sort, SortOrder } from 'src/app/model/graphql/filter/sort.model'; import { Sort, SortOrder } from "src/app/model/graphql/filter/sort.model";
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from "@angular/forms";
import { debounceTime, Subject } from 'rxjs'; import { debounceTime, Subject } from "rxjs";
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from "rxjs/operators";
import { Logger } from 'src/app/service/logger.service'; import { Logger } from "src/app/service/logger.service";
import { AuthService } from 'src/app/service/auth.service'; import { AuthService } from "src/app/service/auth.service";
import { ToastService } from 'src/app/service/toast.service'; import { ToastService } from "src/app/service/toast.service";
const logger = new Logger('TableComponent'); const logger = new Logger("TableComponent");
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: "[customAction]",
})
export class CustomActionDirective {
constructor(public templateRef: TemplateRef<unknown>) {}
}
@Component({ @Component({
selector: 'app-table', selector: "app-table",
templateUrl: './table.component.html', templateUrl: "./table.component.html",
styleUrl: './table.component.scss', styleUrl: "./table.component.scss",
}) })
export class TableComponent<T> implements OnInit { export class TableComponent<T> implements OnInit {
private _rows: T[] = []; private _rows: T[] = [];
@ -57,17 +75,20 @@ export class TableComponent<T> implements OnInit {
@Output() load = new EventEmitter<void>(); @Output() load = new EventEmitter<void>();
@Input() loading = true; @Input() loading = true;
@Input() dataKey = 'id'; @Input() dataKey = "id";
@Input() responsiveLayout: 'stack' | 'scroll' = 'stack'; @Input() responsiveLayout: "stack" | "scroll" = "stack";
@Input() create = false; @Input() create = false;
@Input() update = false; @Input() update = false;
@Input() createBaseUrl = ''; @Input() createBaseUrl = "";
@Input() updateBaseUrl = ''; @Input() updateBaseUrl = "";
@Output() delete: EventEmitter<T> = new EventEmitter<T>(); @Output() delete: EventEmitter<T> = new EventEmitter<T>();
@Output() restore: EventEmitter<T> = new EventEmitter<T>(); @Output() restore: EventEmitter<T> = new EventEmitter<T>();
@ContentChild(CustomActionDirective, { read: TemplateRef })
customActions!: TemplateRef<never>;
protected resolvedColumns: ResolvedTableColumn<T>[][] = []; protected resolvedColumns: ResolvedTableColumn<T>[][] = [];
protected filterForm!: FormGroup; protected filterForm!: FormGroup;
protected defaultFilterForm!: FormGroup; protected defaultFilterForm!: FormGroup;
@ -76,12 +97,12 @@ export class TableComponent<T> implements OnInit {
get showDeleted() { get showDeleted() {
return !this.filter.some( return !this.filter.some(
f => JSON.stringify(f) === JSON.stringify(this.hide_deleted_filter) (f) => JSON.stringify(f) === JSON.stringify(this.hide_deleted_filter),
); );
} }
get showFilters() { get showFilters() {
return this.columns.some(x => x.filterable); return this.columns.some((x) => x.filterable);
} }
protected unsubscriber$ = new Subject<void>(); protected unsubscriber$ = new Subject<void>();
@ -96,25 +117,25 @@ export class TableComponent<T> implements OnInit {
constructor( constructor(
private auth: AuthService, private auth: AuthService,
private toast: ToastService private toast: ToastService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.hasPermissions = { this.hasPermissions = {
read: await this.auth.hasAnyPermissionLazy( read: await this.auth.hasAnyPermissionLazy(
this.requireAnyPermissions.read ?? [] this.requireAnyPermissions.read ?? [],
), ),
create: await this.auth.hasAnyPermissionLazy( create: await this.auth.hasAnyPermissionLazy(
this.requireAnyPermissions.create ?? [] this.requireAnyPermissions.create ?? [],
), ),
update: await this.auth.hasAnyPermissionLazy( update: await this.auth.hasAnyPermissionLazy(
this.requireAnyPermissions.update ?? [] this.requireAnyPermissions.update ?? [],
), ),
delete: await this.auth.hasAnyPermissionLazy( delete: await this.auth.hasAnyPermissionLazy(
this.requireAnyPermissions.delete ?? [] this.requireAnyPermissions.delete ?? [],
), ),
restore: await this.auth.hasAnyPermissionLazy( restore: await this.auth.hasAnyPermissionLazy(
this.requireAnyPermissions.restore ?? [] this.requireAnyPermissions.restore ?? [],
), ),
}; };
@ -131,9 +152,9 @@ export class TableComponent<T> implements OnInit {
} }
const resolvedColumns: ResolvedTableColumn<T>[][] = []; const resolvedColumns: ResolvedTableColumn<T>[][] = [];
this.rows.forEach(row => { this.rows.forEach((row) => {
const resolvedRow: ResolvedTableColumn<T>[] = []; const resolvedRow: ResolvedTableColumn<T>[] = [];
this.columns.forEach(column => { this.columns.forEach((column) => {
resolvedRow.push({ resolvedRow.push({
value: column.value(row), value: column.value(row),
data: row, data: row,
@ -147,7 +168,7 @@ export class TableComponent<T> implements OnInit {
} }
loadData(event: TableLazyLoadEvent) { loadData(event: TableLazyLoadEvent) {
logger.trace('lazy load event', event); logger.trace("lazy load event", event);
const { rows, first, sortField, sortOrder } = event; const { rows, first, sortField, sortOrder } = event;
const reload = const reload =
@ -172,9 +193,9 @@ export class TableComponent<T> implements OnInit {
) { ) {
if (sortField instanceof Array) { if (sortField instanceof Array) {
this.sortChange.emit( this.sortChange.emit(
sortField.map(x => ({ sortField.map((x) => ({
[x]: sortOrder === 1 ? SortOrder.ASC : SortOrder.DESC, [x]: sortOrder === 1 ? SortOrder.ASC : SortOrder.DESC,
})) })),
); );
} else { } else {
this.sortChange.emit([ this.sortChange.emit([
@ -211,17 +232,17 @@ export class TableComponent<T> implements OnInit {
buildDefaultFilterForm() { buildDefaultFilterForm() {
this.defaultFilterForm = new FormGroup({}); this.defaultFilterForm = new FormGroup({});
this.columns this.columns
.filter(x => x.filterable) .filter((x) => x.filterable)
.forEach(x => { .forEach((x) => {
let control!: FormControl; let control!: FormControl;
if (x.type === 'text') { if (x.type === "text") {
control = new FormControl<string | undefined>(undefined); control = new FormControl<string | undefined>(undefined);
} else if (x.type === 'number') { } else if (x.type === "number") {
control = new FormControl<number | undefined>(undefined); control = new FormControl<number | undefined>(undefined);
} else if (x.type === 'bool') { } else if (x.type === "bool") {
control = new FormControl<boolean | undefined>(undefined); control = new FormControl<boolean | undefined>(undefined);
} else if (x.type === 'date') { } else if (x.type === "date") {
control = new FormControl<Date | undefined>(undefined); control = new FormControl<Date | undefined>(undefined);
} else { } else {
control = new FormControl<unknown | undefined>(undefined); control = new FormControl<unknown | undefined>(undefined);
@ -231,14 +252,14 @@ export class TableComponent<T> implements OnInit {
} }
setDefaultFilterForm() { setDefaultFilterForm() {
this.defaultFilter.forEach(x => { this.defaultFilter.forEach((x) => {
Object.keys(x).forEach(key => { Object.keys(x).forEach((key) => {
const value = x[key]; const value = x[key];
if (!(key in this.defaultFilterForm.controls)) { if (!(key in this.defaultFilterForm.controls)) {
return; return;
} }
if (typeof value === 'object' && value !== null) { if (typeof value === "object" && value !== null) {
Object.keys(value).forEach(subKey => { Object.keys(value).forEach((subKey) => {
this.defaultFilterForm.get([key])?.setValue(value[subKey]); this.defaultFilterForm.get([key])?.setValue(value[subKey]);
}); });
} else { } else {
@ -253,38 +274,41 @@ export class TableComponent<T> implements OnInit {
this.filterForm.valueChanges this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscriber$), debounceTime(200)) .pipe(takeUntil(this.unsubscriber$), debounceTime(200))
.subscribe(changes => { .subscribe((changes) => {
logger.trace('Filter input', changes); logger.trace("Filter input", changes);
if (this.filterForm.disabled) { if (this.filterForm.disabled) {
return; return;
} }
const filter = Object.keys(changes) const filter = Object.keys(changes)
.filter( .filter(
key => (key) =>
changes[key] !== undefined && changes[key] !== undefined &&
changes[key] !== null && changes[key] !== null &&
changes[key] !== '' changes[key] !== "",
) )
.map(key => { .map((key) => {
const column = this.columns.find(x => x.name === key); const column = this.columns.find((x) => x.name === key);
if (!column || !column.filterable) { if (!column || !column.filterable) {
return {}; return {};
} }
let value = changes[key]; let value = changes[key];
let defaultFilterMode = 'contains'; let defaultFilterMode: FilterMode = "contains";
switch (column.type) { switch (column.type) {
case 'number': case "number":
defaultFilterMode = 'equal'; defaultFilterMode = "equal";
value = +value; value = +value;
break; break;
case 'bool': case "bool":
defaultFilterMode = 'equal'; defaultFilterMode = "equal";
} }
const filterMode = column.filterMode || defaultFilterMode; const filterMode = column.filterMode || defaultFilterMode;
if (column.filterFactory) {
return column.filterFactory(key, filterMode, value);
}
return { [key]: { [filterMode]: value } }; return { [key]: { [filterMode]: value } };
}); });
@ -300,7 +324,7 @@ export class TableComponent<T> implements OnInit {
copy(val: T | T[keyof T] | string) { copy(val: T | T[keyof T] | string) {
navigator.clipboard.writeText(val as string).then(() => { navigator.clipboard.writeText(val as string).then(() => {
this.toast.info('common.copied', 'common.copied_to_clipboard'); this.toast.info("common.copied", "common.copied_to_clipboard");
}); });
} }
} }

View File

@ -1,6 +1,8 @@
import { PermissionsEnum } from 'src/app/model/auth/permissionsEnum'; import { PermissionsEnum } from "src/app/model/auth/permissionsEnum";
import { Filter } from "src/app/model/graphql/filter/filter.model";
export type TableColumnValue<T> = T | T[keyof T] | string; export type TableColumnValue<T> = T | T[keyof T] | string;
export type FilterMode = "contains" | "startsWith" | "endsWith" | "equal";
export interface TableColumn<T> { export interface TableColumn<T> {
name: string; name: string;
@ -12,7 +14,8 @@ export interface TableColumn<T> {
hidden?: boolean; hidden?: boolean;
sortable?: boolean; sortable?: boolean;
filterable?: boolean; filterable?: boolean;
filterMode?: 'contains' | 'startsWith' | 'endsWith' | 'equals'; filterMode?: FilterMode;
filterFactory?: (key: string, mode: FilterMode, value: T) => Filter;
sort?: (a: T, b: T) => number; sort?: (a: T, b: T) => number;
filter?: (row: T, filter: string) => boolean; filter?: (row: T, filter: string) => boolean;
width?: string; width?: string;

View File

@ -1,17 +1,17 @@
import { addMinutes, format, parseISO } from 'date-fns'; import { addMinutes, format, parseISO } from "date-fns";
export function formatUTCDateToClientTimezone(date: string) { export function formatUTCDateToClientTimezone(date: string) {
const dateTZ = addMinutes( const dateTZ = addMinutes(
parseISO(date), parseISO(date),
new Date().getTimezoneOffset() * -1 new Date().getTimezoneOffset() * -1,
); );
return format(dateTZ, 'yyyy-MM-dd HH:mm:ss'); return format(dateTZ, "yyyy-MM-dd HH:mm:ss");
} }
export function formatUTCDateToGermanLocaleAndTimezone(date: string) { export function formatUTCDateToGermanLocaleAndTimezone(date: string) {
const dateTZ = addMinutes( const dateTZ = addMinutes(
parseISO(date), parseISO(date),
new Date().getTimezoneOffset() * -1 new Date().getTimezoneOffset() * -1,
); );
return format(dateTZ, 'dd.MM.yy HH:mm:ss'); return format(dateTZ, "dd.MM.yy HH:mm:ss");
} }

View File

@ -1,8 +1,8 @@
import { Directive, TemplateRef } from '@angular/core'; import { Directive, TemplateRef } from "@angular/core";
@Directive({ @Directive({
// eslint-disable-next-line @angular-eslint/directive-selector // eslint-disable-next-line @angular-eslint/directive-selector
selector: '[formPageHeader]', selector: "[formPageHeader]",
}) })
export class FormPageHeaderDirective { export class FormPageHeaderDirective {
constructor(public templateRef: TemplateRef<unknown>) {} constructor(public templateRef: TemplateRef<unknown>) {}
@ -10,7 +10,7 @@ export class FormPageHeaderDirective {
@Directive({ @Directive({
// eslint-disable-next-line @angular-eslint/directive-selector // eslint-disable-next-line @angular-eslint/directive-selector
selector: '[formPageContent]', selector: "[formPageContent]",
}) })
export class FormPageContentDirective { export class FormPageContentDirective {
constructor(public templateRef: TemplateRef<unknown>) {} constructor(public templateRef: TemplateRef<unknown>) {}

View File

@ -1,15 +1,15 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from "@angular/core";
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from "@ngx-translate/core";
@Pipe({ @Pipe({
name: 'bool', name: "bool",
}) })
export class BoolPipe implements PipeTransform { export class BoolPipe implements PipeTransform {
constructor(private translate: TranslateService) {} constructor(private translate: TranslateService) {}
transform(value: unknown): string { transform(value: unknown): string {
return this.translate.instant( return this.translate.instant(
value === true || value === 'true' ? 'bool.true' : 'bool.false' value === true || value === "true" ? "bool.true" : "bool.false",
); );
} }
} }

View File

@ -1,23 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from "@angular/core";
import { format } from 'date-fns'; import { format } from "date-fns";
@Pipe({ @Pipe({
name: 'customDate', name: "customDate",
}) })
export class CustomDatePipe implements PipeTransform { export class CustomDatePipe implements PipeTransform {
transform(value: unknown, dateFormat: string = 'yyyy-MM-dd'): string { transform(value: unknown, dateFormat: string = "yyyy-MM-dd"): string {
if (!value) { if (!value) {
return ''; return "";
} }
try { try {
const date = const date =
typeof value === 'string' || typeof value === 'number' typeof value === "string" || typeof value === "number"
? new Date(value) ? new Date(value)
: value; : value;
return format(date as Date, dateFormat); return format(date as Date, dateFormat);
} catch (error) { } catch (error) {
console.error('Error formatting date:', error); console.error("Error formatting date:", error);
return ''; return "";
} }
} }
} }

View File

@ -1,14 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from "@angular/core";
@Pipe({ @Pipe({
name: 'protect', name: "protect",
}) })
export class ProtectPipe implements PipeTransform { export class ProtectPipe implements PipeTransform {
transform(text?: unknown): string { transform(text?: unknown): string {
const len = 8; const len = 8;
if (text && typeof text === 'string' && text.length) { if (text && typeof text === "string" && text.length) {
return '*'.repeat(text.length); return "*".repeat(text.length);
} }
return '*'.repeat(len); return "*".repeat(len);
} }
} }

Some files were not shown because too many files have changed in this diff Show More