convert all tables into p-table

This commit is contained in:
Anas Qureshi
2026-08-11 19:28:10 +05:30
parent fd28920554
commit 1512d3c98b
5 changed files with 178 additions and 206 deletions
@@ -1,14 +1,9 @@
import { Component, DestroyRef, OnInit, inject, signal, viewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
<<<<<<< HEAD
import { ToastrService } from 'ngx-toastr';
import { finalize, map } from 'rxjs/operators';
=======
import { of } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { NotificationService } from '../../../../../core/services/common/notification.service';
>>>>>>> dev
import { CountryLookupDto, CountryService } from '../../../countries/public-api';
import { StateDto } from '../../models/state.model';
@@ -29,12 +24,7 @@ import {
AutocompleteValueFn
} from '../../../../../shared/components/form/autocomplete/autocomplete.types';
import { ConfirmDialog } from '../../../../../shared/components/confirm-dialog/confirm-dialog';
<<<<<<< HEAD
import { DataTableToolbarDirective } from '../../../../../shared/directives/data-table-toolbar/data-table-toolbar.directive';
import { DataTableFilterActionsDirective } from '../../../../../shared/directives/data-table-filter-actions/data-table-filter-actions.directive';
=======
import { Button as AppButton } from '../../../../../shared/components/button/button';
>>>>>>> dev
import { StateFormModalComponent } from '../../components/state-form-modal/state-form-modal';
interface StateTableRow extends DataTableRecord {
@@ -57,12 +47,7 @@ interface StateTableRow extends DataTableRecord {
ReactiveFormsModule,
Autocomplete,
ConfirmDialog,
<<<<<<< HEAD
DataTableToolbarDirective,
DataTableFilterActionsDirective,
=======
AppButton,
>>>>>>> dev
StateFormModalComponent
],
providers: [DataTableStore],
@@ -84,8 +69,6 @@ export class StateList implements OnInit {
readonly showFilters = signal(false);
readonly deleteConfirmDialog = viewChild(ConfirmDialog);
readonly showFilters = signal(false);
readonly countryFilterForm = this.formBuilder.nonNullable.group({
countryId: ['']
});
@@ -229,8 +212,4 @@ export class StateList implements OnInit {
}
});
}
onToggleFilters(): void {
this.showFilters.update(value => !value);
}
}
@@ -8,12 +8,6 @@
</h5>
<div class="flex flex-wrap items-center gap-3">
@if(showFilterButton()){
<<<<<<< HEAD
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave">
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
[variant]="filterActive() ? 'primary-full' : 'light'" className="!rounded-full shadow-sm !whitespace-nowrap"
(buttonClicked)="onFilterClick($event)" />
=======
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave" class="flex items-center">
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
[variant]="filterActive() ? 'primary-full' : 'outline-primary'"
@@ -26,7 +20,6 @@
className="!rounded-full shadow-sm !mb-0" (buttonClicked)="onImportClick($event)" />
<app-button action="custom" icon="ri-download-2-line" label="Export" variant="secondary" size="sm"
className="!rounded-full shadow-sm !mb-0" (buttonClicked)="onExportClick($event)" />
>>>>>>> dev
</div>
}
@if(showAddButton()){
@@ -38,9 +31,11 @@
}
@if (showSearch()) {
<div class="search-wrapper flex items-center">
<i class="ri-search-line search-icon"></i>
<input #searchInput class="form-control form-control-sm brdr-clr" type="text"
[placeholder]="searchPlaceholder()" aria-label="table search" (input)="onSearch(searchInput.value)">
<p-iconfield>
<p-inputicon class="ri-search-line search-icon" />
<input #searchInput pInputText class="form-control form-control-sm brdr-clr" type="text"
[placeholder]="searchPlaceholder()" aria-label="table search" (input)="onSearch(searchInput.value)">
</p-iconfield>
</div>
}
@@ -58,12 +53,8 @@
</div>
}
</div>
<<<<<<< HEAD
<button type="button" class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
=======
<button type="button"
class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
>>>>>>> dev
aria-label="Close filter" (click)="onFilterClick($event)">
<i class="ti ti-x"></i>
</button>
@@ -73,8 +64,8 @@
<div class="box-body !p-0 modern-table-body">
<div class="table-responsive overflow-x-auto overflow-y-visible modern-table-wrapper">
<table [class]="tableClass()">
<thead>
<p-table [value]="rows()" [loading]="loading()" [tableStyleClass]="tableClass()" dataKey="id">
<ng-template pTemplate="header">
<tr [class]="trHeadClass()">
@for (column of columns(); track column.key) {
<th scope="col" [class]="getHeaderClass(column)" [style.width]="column.width || null"
@@ -93,43 +84,10 @@
<th scope="col" [class]="composeClass(actionHeaderClass(), 'relative overflow-visible')">Action</th>
}
</tr>
</thead>
</ng-template>
<tbody>
@if (loading()) {
<tr class="border-b border-defaultborder">
<td [attr.colspan]="totalVisibleColumns()" class="py-10">
<div class="flex flex-col items-center justify-center gap-3 text-center">
<i class="ti ti-loader-2 animate-spin text-[1.5rem]"></i>
<span class="text-[0.875rem]">Loading data...</span>
</div>
</td>
</tr>
} @else if (rows().length === 0) {
<tr class="border-b border-defaultborder bg-white dark:bg-black/10">
<td [attr.colspan]="totalVisibleColumns()" class="p-0">
<div class="flex min-h-[140px] flex-col items-center justify-center px-4 py-8 text-center">
<div
class="mb-3 inline-flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary dark:bg-primary/15">
<i class="ti ti-database-off text-[1.25rem]" aria-hidden="true"></i>
</div>
<p class="text-sm font-semibold text-defaulttextcolor dark:text-white/80">
{{ emptyMessage() }}
</p>
@if (emptyDescription()) {
<p class="mt-1 max-w-md text-sm text-textmuted">
{{ emptyDescription() }}
</p>
}
</div>
</td>
</tr>
} @else
{
@for (row of rows(); track row['id'] ?? $index; let rowIndex = $index) {
<tr [class]="getRowClass(row, $index)" (click)="onRowClick(row)">
<ng-template pTemplate="body" let-row let-rowIndex="rowIndex">
<tr [class]="getRowClass(row, rowIndex)" (click)="onRowClick(row)">
@for (column of columns(); track column.key) {
<td [class]="getCellClass(column)">
@if (getCellTemplate(column); as customTemplate) {
@@ -203,10 +161,42 @@
</td>
}
</tr>
}
}
</tbody>
</table>
</ng-template>
<ng-template pTemplate="loadingbody">
<tr class="border-b border-defaultborder">
<td [attr.colspan]="totalVisibleColumns()" class="py-10">
<div class="flex flex-col items-center justify-center gap-3 text-center">
<i class="ti ti-loader-2 animate-spin text-[1.5rem]"></i>
<span class="text-[0.875rem]">Loading data...</span>
</div>
</td>
</tr>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr class="border-b border-defaultborder bg-white dark:bg-black/10">
<td [attr.colspan]="totalVisibleColumns()" class="p-0">
<div class="flex min-h-[140px] flex-col items-center justify-center px-4 py-8 text-center">
<div
class="mb-3 inline-flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary dark:bg-primary/15">
<i class="ti ti-database-off text-[1.25rem]" aria-hidden="true"></i>
</div>
<p class="text-sm font-semibold text-defaulttextcolor dark:text-white/80">
{{ emptyMessage() }}
</p>
@if (emptyDescription()) {
<p class="mt-1 max-w-md text-sm text-textmuted">
{{ emptyDescription() }}
</p>
}
</div>
</td>
</tr>
</ng-template>
</p-table>
</div>
</div>
@if (showPaginator()) {
@@ -233,7 +223,7 @@
</label>
<select id="dataTablePageSize"
class="form-select form-select-sm !w-[80px] !py-1 !ps-2.5 !pe-6 text-defaulttextcolor bg-white dark:bg-bodybg border border-defaultborder rounded-md"
class="form-select form-select-sm page-size-select !w-[80px] !py-1 !ps-2.5 !pe-6 text-defaulttextcolor bg-white dark:bg-bodybg border rounded-md"
[value]="pageSize()"
(change)="onPageSizeChange($event)">
@for (size of pageSizeOptions(); track size) {
@@ -253,37 +243,11 @@
<!-- Pagination -->
<nav aria-label="Table page navigation">
<ul class="ti-pagination mb-0">
<!-- Previous -->
<li class="page-item" [class.disabled]="pageIndex() <= 1">
<button type="button" class="page-link px-3 py-[0.375rem]" [disabled]="pageIndex() <= 1"
(click)="goToPage(pageIndex() - 1)">
Previous
</button>
</li>
<!-- Page numbers -->
@for (page of visiblePages(); track page) {
<li class="page-item" [class.active]="page === pageIndex()">
<button type="button" class="page-link px-3 py-[0.375rem]" [class.active]="page === pageIndex()"
[attr.aria-current]="
page === pageIndex() ? 'page' : null
" (click)="goToPage(page)">
{{ page }}
</button>
</li>
}
<!-- Next -->
<li class="page-item" [class.disabled]="pageIndex() >= totalPages()">
<button type="button" class="page-link px-3 py-[0.375rem]" [disabled]="pageIndex() >= totalPages()"
(click)="goToPage(pageIndex() + 1)">
Next
</button>
</li>
</ul>
<p-paginator [first]="(pageIndex() - 1) * pageSize()" [rows]="pageSize()" [totalRecords]="totalRecords()"
[showFirstLastIcon]="false" [showPageLinks]="true" (onPageChange)="onPaginatorPageChange($event)">
<ng-template pTemplate="previouspagelinkicon">Previous</ng-template>
<ng-template pTemplate="nextpagelinkicon">Next</ng-template>
</p-paginator>
</nav>
</div>
@@ -233,17 +233,39 @@
color: var(--color-defaulttextcolor);
}
/* p-iconfield wrapper (search input) — PrimeNG renders its own internal DOM,
which lives outside this component's emulated style scope, so ::ng-deep
is required to reach it. */
.search-wrapper ::ng-deep .p-iconfield {
position: relative;
display: inline-flex;
align-items: center;
width: 100%;
}
.search-wrapper ::ng-deep .p-inputicon.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-textmuted, #8c9097);
font-size: 0.8125rem;
pointer-events: none;
}
/* Table body / wrapper */
.modern-table-body {
padding: 0 !important;
}
.modern-table-wrapper {
border-radius: 0 0 0.75rem 0.75rem;
border-radius: 0 0 0.40rem 0.40rem;
}
/* Table */
.data-table-modern .table {
/* Table — PrimeNG's p-table renders <table>/<thead>/<tbody> inside its own
component view (outside this component's emulated style scope), so
::ng-deep is required to reach it. */
.data-table-modern ::ng-deep .table {
width: 100%;
margin-bottom: 0;
border-collapse: separate;
@@ -251,26 +273,26 @@
font-size: 0.8125rem;
}
.data-table-modern .table.table-bordered,
.data-table-modern .table.table-bordered thead th,
.data-table-modern .table.table-bordered tbody td {
.data-table-modern ::ng-deep .table.table-bordered,
.data-table-modern ::ng-deep .table.table-bordered thead th,
.data-table-modern ::ng-deep .table.table-bordered tbody td {
border: none;
}
.data-table-modern .table thead {
.data-table-modern ::ng-deep .table thead {
background: #7C3AED;
color: var(--color-white);
}
.data-table-modern .table thead tr:first-child th:first-child {
.data-table-modern ::ng-deep .table thead tr:first-child th:first-child {
border-top-left-radius: 0.75rem;
}
.data-table-modern .table thead tr:first-child th:last-child {
.data-table-modern ::ng-deep .table thead tr:first-child th:last-child {
border-top-right-radius: 0.75rem;
}
.data-table-modern .table thead th {
.data-table-modern ::ng-deep .table thead th {
padding: 0.5rem 0.75rem;
font-weight: 700;
font-size: 0.75rem;
@@ -283,7 +305,7 @@
border: none;
}
.data-table-modern .table tbody td {
.data-table-modern ::ng-deep .table tbody td {
padding: 0.5rem 1rem;
vertical-align: middle;
color: var(--color-defaulttextcolor);
@@ -291,13 +313,11 @@
font-size: 0.8125rem;
}
.data-table-modern .table tbody tr:hover {
// background-color: var(--color-light);
.data-table-modern ::ng-deep .table tbody tr:hover {
background-color: #e1d0ff;
}
:host-context(.dark) .data-table-modern .table tbody tr:hover {
:host-context(.dark) .data-table-modern ::ng-deep .table tbody tr:hover {
background-color: color-mix(in srgb, #fff 6%, transparent);
}
@@ -318,32 +338,63 @@
background-color: color-mix(in srgb, var(--color-primary) 6%, transparent);
}
/* Pagination */
.data-table-modern .ti-pagination {
/* Page-size select box border */
.data-table-modern .page-size-select {
border-color: #845adf !important;
}
/* Pagination (PrimeNG p-paginator) — its internal DOM lives inside its own
component view (outside this component's emulated style scope), so
::ng-deep is required to reach it. */
.data-table-modern ::ng-deep .p-paginator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0;
margin: 0;
background: transparent;
border: none;
}
.data-table-modern ::ng-deep .p-paginator .p-paginator-pages {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0;
margin: 0;
}
.data-table-modern .ti-pagination li.page-item {
display: inline-flex;
border: 1px solid var(--color-primary);
border-radius: 9999px;
}
.data-table-modern .ti-pagination li.page-item.disabled {
opacity: 0.4;
}
.data-table-modern .ti-pagination li .page-link {
/* Previous / Next — pill buttons with a visible border, like the rest of the app */
.data-table-modern ::ng-deep .p-paginator .p-paginator-prev,
.data-table-modern ::ng-deep .p-paginator .p-paginator-next {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.5rem;
height: 1.75rem;
padding: 0 0.75rem;
border-radius: 0.375rem;
border: 1px solid var(--color-primary);
background-color: transparent;
color: var(--color-defaulttextcolor);
font-size: 0.75rem;
font-weight: 500;
line-height: 1;
transition: all 0.15s ease;
cursor: pointer;
}
.data-table-modern ::ng-deep .p-paginator .p-paginator-prev:hover:not(.p-disabled),
.data-table-modern ::ng-deep .p-paginator .p-paginator-next:hover:not(.p-disabled) {
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
color: var(--color-primary);
}
/* Page number — plain circle, filled solid when selected */
.data-table-modern ::ng-deep .p-paginator .p-paginator-page {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.75rem;
height: 1.75rem;
padding: 0;
border-radius: 9999px;
border: none;
background-color: transparent;
@@ -352,40 +403,44 @@
font-weight: 500;
line-height: 1;
transition: all 0.15s ease;
cursor: pointer;
}
.data-table-modern .ti-pagination li .page-link:hover:not(:disabled) {
.data-table-modern ::ng-deep .p-paginator .p-paginator-page:hover:not(.p-disabled):not(.p-paginator-page-selected) {
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
color: var(--color-primary);
}
.data-table-modern .ti-pagination li.active .page-link,
.data-table-modern .ti-pagination li .page-link.active {
background-color: var(--color-primary) !important;
color: var(--color-white);
box-shadow: 0 2px 6px color-mix(in srgb, var(--color-primary) 30%, transparent);
}
.data-table-modern ::ng-deep .p-paginator .p-paginator-page-selected {
background-color: var(--color-primary) !important;
color: var(--color-white);
box-shadow: 0 2px 6px color-mix(in srgb, var(--color-primary) 30%, transparent);
}
.data-table-modern .ti-pagination li.disabled .page-link {
.data-table-modern ::ng-deep .p-paginator .p-disabled {
opacity: 0.4;
cursor: not-allowed;
background-color: transparent;
}
:host-context(.dark) .data-table-modern .ti-pagination li .page-link {
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page,
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-prev,
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-next {
background-color: transparent;
color: color-mix(in srgb, var(--color-defaulttextcolor) 80%, #fff);
}
:host-context(.dark) .data-table-modern .ti-pagination li .page-link:hover:not(:disabled) {
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page:hover:not(.p-disabled):not(.p-paginator-page-selected),
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-prev:hover:not(.p-disabled),
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-next:hover:not(.p-disabled) {
background-color: color-mix(in srgb, var(--color-primary) 15%, transparent);
color: var(--color-primary);
}
:host-context(.dark) .data-table-modern .ti-pagination li.active .page-link,
:host-context(.dark) .data-table-modern .ti-pagination li .page-link.active {
color: var(--color-white);
background-color: var(--color-primary) !important;
}
:host-context(.dark) .data-table-modern ::ng-deep .p-paginator .p-paginator-page-selected {
color: var(--color-white);
background-color: var(--color-primary) !important;
}
/* Responsive scroll */
.table-responsive {
@@ -394,8 +449,8 @@
overflow-y: visible;
}
.table th,
.table td {
::ng-deep .table th,
::ng-deep .table td {
vertical-align: middle;
}
@@ -6,6 +6,11 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DataTableCellDirective } from '../../directives/data-table-cell.directive';
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';
import { TableModule } from 'primeng/table';
import { Paginator, PaginatorState } from 'primeng/paginator';
import { IconField } from 'primeng/iconfield';
import { InputIcon } from 'primeng/inputicon';
import { InputText } from 'primeng/inputtext';
import { DataTableAction, DataTableActionEvent, DataTableCellContext, DataTableColumn, DataTablePageEvent, DataTableRecord, DataTableSortEvent } from './data-table.types';
@@ -20,6 +25,11 @@ import { DataTableFilterActionsDirective } from '../../directives/data-table-fil
MatPaginatorModule,
CdkOverlayOrigin,
CdkConnectedOverlay,
TableModule,
Paginator,
IconField,
InputIcon,
InputText,
Button],
templateUrl: './data-table.html',
styleUrl: './data-table.scss',
@@ -39,34 +49,6 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
readonly filterRowOverflowing = signal(false);
private filterRowResizeObserver?: ResizeObserver;
private readonly filterRowContentEffect = effect(onCleanup => {
const element = this.filterRowContent()?.nativeElement;
this.filterRowResizeObserver?.disconnect();
if (!element) {
this.filterRowOverflowing.set(false);
return;
}
const checkOverflow = () => {
this.filterRowOverflowing.set(element.scrollWidth > element.clientWidth + 1);
};
checkOverflow();
this.filterRowResizeObserver = new ResizeObserver(checkOverflow);
this.filterRowResizeObserver.observe(element);
onCleanup(() => this.filterRowResizeObserver?.disconnect());
});
readonly filterActionsTemplate = contentChild(DataTableFilterActionsDirective);
readonly filterRowContent = viewChild<ElementRef<HTMLElement>>('filterRowContent');
readonly filterRowOverflowing = signal(false);
private filterRowResizeObserver?: ResizeObserver;
private readonly filterRowContentEffect = effect(onCleanup => {
const element = this.filterRowContent()?.nativeElement;
@@ -167,8 +149,6 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
searchDebounceTime = input(300);
emptyMessage = input('No records found');
emptyDescription = input('There is currently no data to display.');
showFilterButton = input<boolean>(false);
filterActive = input<boolean>(false);
showImportExportButtons = input<boolean>(false);
/*---------------------------*/
@@ -342,6 +322,17 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
this.closeActionMenu();
}
onPaginatorPageChange(event: PaginatorState): void {
const rows = event.rows ?? this.pageSize();
const page = event.page ?? Math.floor((event.first ?? 0) / rows);
this.onPageChange({
pageIndex: page,
pageSize: rows,
length: this.totalRecords()
});
}
onActionClick(event: MouseEvent, action: DataTableAction<T>, row: T): void {
event.stopPropagation();
-17
View File
@@ -72,22 +72,6 @@ body {
line-height: 1.25;
}
<<<<<<< HEAD
/* Center currency modal floating label on empty invalid fields */
currency-list .custom-floating-field.has-error.is-empty .custom-floating-input:not(:focus) ~ .custom-floating-label {
top: 35% !important;
transform: translateY(-50%) !important;
}
/* Remove unwanted bottom margin from primary-full buttons */
.ti-btn-primary-full {
margin-bottom: 0 !important;
}
.ti-form-select {
border: 1px solid #7c3deb !important;
}
=======
/* Remove unwanted bottom margin from buttons */
.ti-btn {
margin-bottom: 0 !important;
@@ -258,4 +242,3 @@ currency-list .custom-floating-field.has-error.is-empty .custom-floating-input:n
}
}
}
>>>>>>> dev