Feature/stats finishoff (#1720)
* Added ability to click on genres, tags, and people to view all items in a modal. * Made it so we can click and open a filtered search from generic list * Fixed broken epub pagination area due to a typo in a query selector * Added day breakdown, wrapping up stats
This commit is contained in:
parent
dfbc8da427
commit
02daa5ed56
20 changed files with 378 additions and 66 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { UserReadStatistics } from '../statistics/_models/user-read-statistics';
|
||||
|
|
@ -13,6 +13,16 @@ import { StatCount } from '../statistics/_models/stat-count';
|
|||
import { PublicationStatus } from '../_models/metadata/publication-status';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
|
||||
export enum DayOfWeek
|
||||
{
|
||||
Sunday = 0,
|
||||
Monday = 1,
|
||||
Tuesday = 2,
|
||||
Wednesday = 3,
|
||||
Thursday = 4,
|
||||
Friday = 5,
|
||||
Saturday = 6,
|
||||
}
|
||||
|
||||
const publicationStatusPipe = new PublicationStatusPipe();
|
||||
const mangaFormatPipe = new MangaFormatPipe();
|
||||
|
|
@ -85,4 +95,8 @@ export class StatisticsService {
|
|||
getReadCountByDay(userId: number = 0, days: number = 0) {
|
||||
return this.httpClient.get<Array<any>>(this.baseUrl + 'stats/reading-count-by-day?userId=' + userId + '&days=' + days);
|
||||
}
|
||||
|
||||
getDayBreakdown() {
|
||||
return this.httpClient.get<Array<StatCount<DayOfWeek>>>(this.baseUrl + 'stats/day-breakdown');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/auth/member';
|
||||
|
|
@ -24,7 +23,7 @@ export class LibraryAccessModalComponent implements OnInit {
|
|||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
constructor(public modal: NgbActiveModal, private libraryService: LibraryService, private fb: FormBuilder) { }
|
||||
constructor(public modal: NgbActiveModal, private libraryService: LibraryService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.libraryService.getLibraries().subscribe(libs => {
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
/**
|
||||
* book-content class
|
||||
*/
|
||||
@ViewChild('bookContentElemRef', {static: false}) bookContentElemRef!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('readingHtml', {static: false}) bookContentElemRef!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('reader', {static: true}) reader!: ElementRef;
|
||||
|
|
@ -382,7 +382,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
get PageHeightForPagination() {
|
||||
if (this.layoutMode === BookPageLayoutMode.Default) {
|
||||
|
||||
// if the book content is less than the height of the container, override and return height of container for pagination area
|
||||
if (this.bookContainerElemRef?.nativeElement?.clientHeight > this.bookContentElemRef?.nativeElement?.clientHeight) {
|
||||
return (this.bookContainerElemRef?.nativeElement?.clientHeight || 0) + 'px';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DashboardRoutingModule } from './dashboard-routing.module';
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||
|
||||
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
|
||||
|
||||
filterList = (listItem: ReadingList) => {
|
||||
return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
|
||||
constructor(private modal: NgbActiveModal, private readingListService: ReadingListService, private toastr: ToastrService) { }
|
||||
|
||||
|
|
@ -128,9 +132,4 @@ export class AddToListModalComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
filterList = (listItem: ReadingList) => {
|
||||
return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form style="width: 100%" [formGroup]="listForm">
|
||||
<div class="mb-3" *ngIf="items.length >= 5">
|
||||
<label for="filter" class="form-label">Filter</label>
|
||||
<div class="input-group">
|
||||
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
|
||||
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let item of items | filter: filterList; let i = index">
|
||||
{{item}}
|
||||
<button class="btn btn-primary" [disabled]="clicked === undefined" (click)="handleClick(item)">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Open a filtered search for {{item}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="close()">Close</button>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.list-group-item.no-click {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { Component, EventEmitter, Input } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-list-modal',
|
||||
templateUrl: './generic-list-modal.component.html',
|
||||
styleUrls: ['./generic-list-modal.component.scss']
|
||||
})
|
||||
export class GenericListModalComponent {
|
||||
@Input() items: Array<string> = [];
|
||||
@Input() title: string = '';
|
||||
@Input() clicked: ((item: string) => void) | undefined = undefined;
|
||||
|
||||
listForm: FormGroup = new FormGroup({
|
||||
'filterQuery': new FormControl('', [])
|
||||
});
|
||||
|
||||
filterList = (listItem: string) => {
|
||||
return listItem.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
constructor(private modal: NgbActiveModal) {}
|
||||
|
||||
close() {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
handleClick(item: string) {
|
||||
if (this.clicked) {
|
||||
this.clicked(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<div class="row g-0 mb-2">
|
||||
<h4>Day Breakdown</h4>
|
||||
</div>
|
||||
|
||||
<ngx-charts-bar-vertical
|
||||
class="dark"
|
||||
[view]="view"
|
||||
[results]="dayBreakdown$ | async"
|
||||
[xAxis]="true"
|
||||
[yAxis]="true"
|
||||
[legend]="showLegend"
|
||||
[showXAxisLabel]="true"
|
||||
[showYAxisLabel]="true"
|
||||
xAxisLabel="Day of Week"
|
||||
yAxisLabel="Reading Events">
|
||||
</ngx-charts-bar-vertical>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
::ng-deep .dark .ngx-charts text {
|
||||
fill: #a0aabe;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { LegendPosition } from '@swimlane/ngx-charts';
|
||||
import { Subject, combineLatest, map, takeUntil, Observable } from 'rxjs';
|
||||
import { DayOfWeek, StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { compare } from 'src/app/_single-module/table/_directives/sortable-header.directive';
|
||||
import { PieDataItem } from '../../_models/pie-data-item';
|
||||
import { StatCount } from '../../_models/stat-count';
|
||||
import { DayOfWeekPipe } from '../../_pipes/day-of-week.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-day-breakdown',
|
||||
templateUrl: './day-breakdown.component.html',
|
||||
styleUrls: ['./day-breakdown.component.scss']
|
||||
})
|
||||
export class DayBreakdownComponent implements OnInit {
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
|
||||
view: [number, number] = [700, 400];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = false;
|
||||
legendPosition: LegendPosition = LegendPosition.Right;
|
||||
colorScheme = {
|
||||
domain: ['#5AA454', '#A10A28', '#C7B42C', '#AAAAAA']
|
||||
};
|
||||
|
||||
formControl: FormControl = new FormControl(true, []);
|
||||
dayBreakdown$!: Observable<Array<PieDataItem>>;
|
||||
|
||||
constructor(private statService: StatisticsService) {
|
||||
const dayOfWeekPipe = new DayOfWeekPipe();
|
||||
this.dayBreakdown$ = this.statService.getDayBreakdown().pipe(
|
||||
map((data: Array<StatCount<DayOfWeek>>) => {
|
||||
return data.map(d => {
|
||||
return {name: dayOfWeekPipe.transform(d.value), value: d.count};
|
||||
})
|
||||
}),
|
||||
takeUntil(this.onDestroy)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export class ReadByDayAndComponent implements OnInit, OnDestroy {
|
|||
shareReplay(),
|
||||
);
|
||||
|
||||
this.data$.subscribe(_ => console.log('hi'));
|
||||
this.data$.subscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="row g-0 mt-4 mb-3 d-flex justify-content-around" *ngIf="stats$ | async as stats">
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Series" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Total Series">
|
||||
<app-icon-and-title label="Total Series" [clickable]="false" fontClasses="fa-solid fa-book-open" title="Total Series">
|
||||
{{stats.seriesCount | compactNumber}} Series
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<ng-container >
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Volumes" [clickable]="false" fontClasses="fas fa-eye" title="Total Volumes">
|
||||
<app-icon-and-title label="Total Volumes" [clickable]="false" fontClasses="fas fa-book" title="Total Volumes">
|
||||
{{stats.volumeCount | compactNumber}} Volumes
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Size" [clickable]="false" fontClasses="fa-solid fa-weight-scale" title="Total Size">
|
||||
<app-icon-and-title label="Total Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="Total Size">
|
||||
{{stats.totalSize | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Genres" [clickable]="false" fontClasses="fa-solid fa-tags" title="Total Genres">
|
||||
<app-icon-and-title label="Total Genres" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Genres" (click)="openGenreList();$event.stopPropagation();">
|
||||
{{stats.totalGenres | compactNumber}} Genres
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total Tags" [clickable]="false" fontClasses="fa-solid fa-tags" title="Total Tags">
|
||||
<app-icon-and-title label="Total Tags" [clickable]="true" fontClasses="fa-solid fa-tags" title="Total Tags" (click)="openTagList();$event.stopPropagation();">
|
||||
{{stats.totalTags | compactNumber}} Tags
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
<ng-container>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Total People" [clickable]="false" fontClasses="fa-solid fa-user-tag" title="Total People">
|
||||
<app-icon-and-title label="Total People" [clickable]="true" fontClasses="fa-solid fa-user-tag" title="Total People" (click)="openPeopleList();$event.stopPropagation();">
|
||||
{{stats.totalPeople | compactNumber}} People
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
|
|
@ -74,7 +74,7 @@
|
|||
|
||||
<div class="grid row g-0 pt-2 pb-2 d-flex justify-content-around">
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="releaseYears$" title="Release Years" lable="series"></app-stat-list>
|
||||
<app-stat-list [data$]="releaseYears$" title="Release Years" label="series"></app-stat-list>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<app-stat-list [data$]="mostActiveUsers$" title="Most Active Users" label="reads"></app-stat-list>
|
||||
|
|
@ -109,4 +109,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 pt-4 pb-2 " style="height: 242px">
|
||||
<div class="col-md-12 col-sm-12 mt-4 pt-2">
|
||||
<app-day-breakdown></app-day-breakdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { map, Observable, shareReplay, Subject, takeUntil, tap } from 'rxjs';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { StatisticsService } from 'src/app/_services/statistics.service';
|
||||
import { PieDataItem } from '../../_models/pie-data-item';
|
||||
import { ServerStatistics } from '../../_models/server-statistics';
|
||||
import { GenericListModalComponent } from '../_modals/generic-list-modal/generic-list-modal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-stats',
|
||||
|
|
@ -25,8 +27,13 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
|
|||
stats$!: Observable<ServerStatistics>;
|
||||
seriesImage: (data: PieDataItem) => string;
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
openSeries = (data: PieDataItem) => {
|
||||
const series = data.extra as Series;
|
||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService) {
|
||||
constructor(private statService: StatisticsService, private router: Router, private imageService: ImageService,
|
||||
private metadataService: MetadataService, private modalService: NgbModal) {
|
||||
this.seriesImage = (data: PieDataItem) => {
|
||||
if (data.extra) return this.imageService.getSeriesCoverImage(data.extra.id);
|
||||
return '';
|
||||
|
|
@ -75,9 +82,40 @@ export class ServerStatsComponent implements OnInit, OnDestroy {
|
|||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
openSeries = (data: PieDataItem) => {
|
||||
const series = data.extra as Series;
|
||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||
openGenreList() {
|
||||
this.metadataService.getAllGenres().subscribe(genres => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = genres.map(t => t.title);
|
||||
ref.componentInstance.title = 'Genres';
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Genres] = item;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
openTagList() {
|
||||
this.metadataService.getAllTags().subscribe(tags => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = tags.map(t => t.title);
|
||||
ref.componentInstance.title = 'Tags';
|
||||
ref.componentInstance.clicked = (item: string) => {
|
||||
const params: any = {};
|
||||
params[FilterQueryParam.Tags] = item;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
this.router.navigate(['all-series'], {queryParams: params});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
openPeopleList() {
|
||||
this.metadataService.getAllPeople().subscribe(people => {
|
||||
const ref = this.modalService.open(GenericListModalComponent, { scrollable: true });
|
||||
ref.componentInstance.items = [...new Set(people.map(person => person.name))];
|
||||
ref.componentInstance.title = 'People';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
22
UI/Web/src/app/statistics/_pipes/day-of-week.pipe.ts
Normal file
22
UI/Web/src/app/statistics/_pipes/day-of-week.pipe.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DayOfWeek } from 'src/app/_services/statistics.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'dayOfWeek'
|
||||
})
|
||||
export class DayOfWeekPipe implements PipeTransform {
|
||||
|
||||
transform(value: DayOfWeek): string {
|
||||
switch(value) {
|
||||
case DayOfWeek.Monday: return 'Monday';
|
||||
case DayOfWeek.Tuesday: return 'Tuesday';
|
||||
case DayOfWeek.Wednesday: return 'Wednesday';
|
||||
case DayOfWeek.Thursday: return 'Thursday';
|
||||
case DayOfWeek.Friday: return 'Friday';
|
||||
case DayOfWeek.Saturday: return 'Saturday';
|
||||
case DayOfWeek.Sunday: return 'Sunday';
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { SharedModule } from '../shared/shared.module';
|
|||
import { ServerStatsComponent } from './_components/server-stats/server-stats.component';
|
||||
import { NgxChartsModule } from '@swimlane/ngx-charts';
|
||||
import { StatListComponent } from './_components/stat-list/stat-list.component';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbModalModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PublicationStatusStatsComponent } from './_components/publication-status-stats/publication-status-stats.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MangaFormatStatsComponent } from './_components/manga-format-stats/manga-format-stats.component';
|
||||
|
|
@ -15,6 +15,9 @@ import { FileBreakdownStatsComponent } from './_components/file-breakdown-stats/
|
|||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { TopReadersComponent } from './_components/top-readers/top-readers.component';
|
||||
import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day-and.component';
|
||||
import { GenericListModalComponent } from './_components/_modals/generic-list-modal/generic-list-modal.component';
|
||||
import { DayBreakdownComponent } from './_components/day-breakdown/day-breakdown.component';
|
||||
import { DayOfWeekPipe } from './_pipes/day-of-week.pipe';
|
||||
|
||||
|
||||
|
||||
|
|
@ -28,13 +31,17 @@ import { ReadByDayAndComponent } from './_components/read-by-day-and/read-by-day
|
|||
MangaFormatStatsComponent,
|
||||
FileBreakdownStatsComponent,
|
||||
TopReadersComponent,
|
||||
ReadByDayAndComponent
|
||||
ReadByDayAndComponent,
|
||||
GenericListModalComponent,
|
||||
DayBreakdownComponent,
|
||||
DayOfWeekPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TableModule,
|
||||
SharedModule,
|
||||
NgbTooltipModule,
|
||||
NgbModalModule,
|
||||
ReactiveFormsModule,
|
||||
PipeModule,
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue