mirror of
https://github.com/valitydev/ng-libs.git
synced 2024-11-06 00:35:21 +00:00
EMP-55: Add table2 drag drop (#79)
Some checks are pending
Main / Publish (push) Waiting to run
Some checks are pending
Main / Publish (push) Waiting to run
This commit is contained in:
parent
784928f164
commit
3a051b855b
@ -2,31 +2,6 @@
|
||||
@import '../../styles/utils/get-color';
|
||||
|
||||
@mixin theme($theme) {
|
||||
.pinned-right {
|
||||
border-left-color: get-color($theme, neutral, 300) !important;
|
||||
}
|
||||
|
||||
.pinned-left {
|
||||
border-right-color: get-color($theme, neutral, 300) !important;
|
||||
}
|
||||
|
||||
.v-table-cell {
|
||||
.link {
|
||||
.value {
|
||||
color: get-color($theme, primary);
|
||||
}
|
||||
}
|
||||
.value-link {
|
||||
color: get-color($theme, primary);
|
||||
}
|
||||
}
|
||||
|
||||
.v-table-filter {
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
|
||||
// Table 2
|
||||
|
||||
.column {
|
||||
&,
|
||||
&__sticky-start,
|
||||
|
@ -1,26 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { MatSortHeader } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
import { BaseColumnComponent } from './base-column.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'v-score-column',
|
||||
template: `
|
||||
<ng-container [matColumnDef]="name()">
|
||||
<th *matHeaderCellDef [mat-sort-header]="name()" mat-header-cell style="display: none">
|
||||
Score
|
||||
</th>
|
||||
<td *matCellDef="let row; let index = index" mat-cell style="display: none">
|
||||
{{ scores().get(row)?.score | number: '1.2-2' : 'en' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, MatTableModule, MatSortHeader],
|
||||
})
|
||||
export class ScoreColumnComponent<T extends object> extends BaseColumnComponent {
|
||||
scores = input<Map<T, { score: number }>>(new Map());
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<button
|
||||
[disabled]="progress"
|
||||
class="show-more"
|
||||
mat-button
|
||||
matTooltip="Show {{ pageSize }} more (currently shown {{ shown }})"
|
||||
(click)="more.emit()"
|
||||
>
|
||||
{{ progress ? 'Loading...' : 'Show more' }}
|
||||
</button>
|
@ -1,3 +0,0 @@
|
||||
.show-more {
|
||||
width: 100%;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Component, EventEmitter, Input, Output, booleanAttribute } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'v-show-more-button',
|
||||
templateUrl: './show-more-button.component.html',
|
||||
styleUrls: ['./show-more-button.component.scss'],
|
||||
imports: [MatTooltip, MatButton],
|
||||
})
|
||||
export class ShowMoreButtonComponent {
|
||||
@Input({ transform: booleanAttribute }) progress = false;
|
||||
@Input() displayedPages = 0;
|
||||
@Input() pageSize = 0;
|
||||
@Input() length = 0;
|
||||
|
||||
@Output() more = new EventEmitter<void>();
|
||||
|
||||
get shown() {
|
||||
return Math.min(this.length, this.displayedPages * this.pageSize);
|
||||
}
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
import { ActionsModule } from '../../actions';
|
||||
|
||||
@Component({
|
||||
selector: 'v-table-actions',
|
||||
standalone: true,
|
||||
template: `
|
||||
<v-actions>
|
||||
<ng-content></ng-content>
|
||||
</v-actions>
|
||||
`,
|
||||
imports: [ActionsModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TableActionsComponent {}
|
||||
|
@ -1,121 +0,0 @@
|
||||
@if (colDef.lazy && !lazyVisible && !preloadLazy) {
|
||||
<button
|
||||
class="button"
|
||||
mat-icon-button
|
||||
(click)="lazyVisible = true; preloadedLazyChange.emit(true)"
|
||||
>
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<ng-container
|
||||
*ngTemplateOutlet="preloadLazy || !colDef.lazy || lazyVisible ? content : null"
|
||||
></ng-container>
|
||||
|
||||
<ng-template #content>
|
||||
<div class="wrapper">
|
||||
@if (rowData | vSelect: colDef.tooltip : '' : [colDef]; as tooltip) {
|
||||
<div [mtxTooltip]="tooltipTpl" class="tooltip" mtxTooltipPosition="right">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngTemplateOutlet="descriptionTemplate"></ng-container>
|
||||
<ng-template #tooltipTpl>
|
||||
<pre>{{ tooltip | json }}</pre>
|
||||
</ng-template>
|
||||
} @else {
|
||||
@if (colDef.link?.(rowData, index); as link) {
|
||||
<a [routerLink]="link" [target]="colDef?.linkParameters?.target" class="link">
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
</a>
|
||||
<ng-container *ngTemplateOutlet="descriptionTemplate"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="valueTemplate"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="descriptionTemplate"></ng-container>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template #valueTemplate>
|
||||
<ng-template
|
||||
#value
|
||||
[ngTemplateOutlet]="value"
|
||||
[ngTemplateOutletContext]="{
|
||||
value: rowData | vSelect: colDef.formatter ?? colDef.field : '' : [index, colDef],
|
||||
}"
|
||||
let-value="value"
|
||||
>
|
||||
<div
|
||||
[ngClass]="{ 'value-link': !!colDef.click }"
|
||||
[title]="value"
|
||||
class="value"
|
||||
(click)="colDef.click?.(rowData, index)"
|
||||
>
|
||||
@if (colDef.type === 'datetime') {
|
||||
{{ value | date: 'dd.MM.yyyy HH:mm:ss' : '+0000' }}
|
||||
}
|
||||
@if (colDef.type === 'tag') {
|
||||
@if (colDef.typeParameters.tags[value] ?? {}; as tag) {
|
||||
@if (
|
||||
tag.label ??
|
||||
(rowData | vSelect: colDef.typeParameters.label : value : [colDef]);
|
||||
as tagLabel
|
||||
) {
|
||||
<v-tag [color]="tag.color">
|
||||
{{ tagLabel }}
|
||||
</v-tag>
|
||||
}
|
||||
}
|
||||
}
|
||||
@if (colDef.type === 'boolean') {
|
||||
@if (value === true || value === false) {
|
||||
<v-tag [color]="value ? 'success' : 'warn'">
|
||||
{{ value ? 'Yes' : 'No' }}
|
||||
</v-tag>
|
||||
}
|
||||
}
|
||||
@if (colDef.type === 'currency') {
|
||||
{{
|
||||
value
|
||||
| amountCurrency
|
||||
: (rowData
|
||||
| vSelect
|
||||
: colDef?.typeParameters?.currencyCode
|
||||
: ''
|
||||
: [colDef])
|
||||
: 'long'
|
||||
: (rowData
|
||||
| vSelect: colDef?.typeParameters?.exponent : 2 : [colDef])
|
||||
: !colDef?.typeParameters?.isMinor
|
||||
}}
|
||||
}
|
||||
@if (colDef.type === 'menu') {
|
||||
<button [matMenuTriggerFor]="menu" class="button" mat-icon-button>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
@for (item of colDef.typeParameters.items; track item; let index = $index) {
|
||||
<button
|
||||
[disabled]="!!item?.disabled?.(rowData, index)"
|
||||
mat-menu-item
|
||||
(click)="item.click(rowData, index)"
|
||||
>
|
||||
{{ getLabel(item.label, index) }}
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
}
|
||||
@if (!colDef.type) {
|
||||
{{ value }}
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #descriptionTemplate>
|
||||
<div
|
||||
*ngIf="rowData | vSelect: colDef.description : '' : [colDef] as description"
|
||||
[title]="description"
|
||||
class="description mat-caption mat-secondary-text"
|
||||
>
|
||||
{{ description }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
@ -1,41 +0,0 @@
|
||||
.wrapper {
|
||||
& > * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value,
|
||||
.description {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value-link {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
|
||||
.value {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.value:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: -2.5px 0;
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
Input,
|
||||
booleanAttribute,
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ColumnFn, ColumnObject } from '../../types';
|
||||
|
||||
@Component({
|
||||
selector: 'v-table-cell',
|
||||
templateUrl: './table-cell.component.html',
|
||||
styleUrls: ['./table-cell.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TableCellComponent<T extends object> {
|
||||
@HostBinding('class.v-table-cell') hostClass: boolean = true;
|
||||
|
||||
@Input() rowData!: T;
|
||||
@Input() colDef!: ColumnObject<T>;
|
||||
@Input() index!: number;
|
||||
|
||||
@Input({ transform: booleanAttribute }) preloadLazy = false;
|
||||
|
||||
@Output() preloadedLazyChange = new EventEmitter<boolean>();
|
||||
|
||||
lazyVisible = false;
|
||||
|
||||
getLabel(label: string | ColumnFn<T, string>, index: number) {
|
||||
return typeof label === 'string' ? label : label(this.rowData, index);
|
||||
}
|
||||
}
|
@ -26,28 +26,36 @@
|
||||
</v-tag>
|
||||
</div>
|
||||
<div class="details">
|
||||
<button
|
||||
[disabled]="progress()"
|
||||
mat-icon-button
|
||||
matTooltip="Reload {{ size() }} elements"
|
||||
(click)="load.emit()"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
[disabled]="progress() || !hasMore()"
|
||||
mat-icon-button
|
||||
matTooltip="Preload {{ preloadSize() }}{{
|
||||
isPreload() && hasMore() ? ' more' : ''
|
||||
}} elements"
|
||||
(click)="preload.emit()"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
hasMore() ? (isPreload() ? 'downloading' : 'download') : 'download_done'
|
||||
}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
@if (hasLoad()) {
|
||||
<button
|
||||
[disabled]="progress()"
|
||||
mat-icon-button
|
||||
matTooltip="Reload {{ size() }} elements"
|
||||
(click)="load.emit()"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (hasLoad() || hasMore()) {
|
||||
<button
|
||||
[disabled]="progress() || !hasMore()"
|
||||
mat-icon-button
|
||||
matTooltip="Preload {{ preloadSize() }}{{
|
||||
isPreload() && hasMore() ? ' more' : ''
|
||||
}} elements"
|
||||
(click)="preload.emit()"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
hasMore()
|
||||
? isPreload()
|
||||
? 'downloading'
|
||||
: 'download'
|
||||
: 'download_done'
|
||||
}}
|
||||
</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!noDownload()) {
|
||||
<button
|
||||
[disabled]="progress() || !count()"
|
||||
|
@ -39,6 +39,7 @@ import { TagModule } from '../../../tag';
|
||||
export class TableInfoBarComponent implements OnInit {
|
||||
progress = input(false, { transform: booleanAttribute });
|
||||
hasMore = input(false, { transform: booleanAttribute });
|
||||
hasLoad = input(false, { transform: booleanAttribute });
|
||||
isPreload = input(false, { transform: booleanAttribute });
|
||||
noDownload = input(false, { transform: booleanAttribute });
|
||||
dataProgress = input(false, { transform: booleanAttribute });
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { createUniqueColumnDef } from '../../utils/create-unique-column-def';
|
||||
|
||||
export const COLUMN_DEFS = {
|
||||
select: createUniqueColumnDef('select'),
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
export * from './table2.component';
|
||||
export * from './tree-data';
|
@ -1,111 +0,0 @@
|
||||
@let columnsData = columnsData$$ | async;
|
||||
|
||||
<div class="wrapper">
|
||||
<v-table-info-bar
|
||||
[(filter)]="filter"
|
||||
[count]="(dataSource.data$ | async)?.length"
|
||||
[dataProgress]="!!(columnsDataProgress$ | async) && !hasMore()"
|
||||
[filteredCount]="displayedCount$ | async"
|
||||
[hasInputs]="!!tableInputsContent?.nativeElement"
|
||||
[hasMore]="hasMore()"
|
||||
[isPreload]="isPreload()"
|
||||
[noDownload]="noDownload()"
|
||||
[preloadSize]="maxSize()"
|
||||
[progress]="progress()"
|
||||
[selectedCount]="rowSelected()?.length"
|
||||
[size]="size()"
|
||||
[standaloneFilter]="standaloneFilter()"
|
||||
(downloadCsv)="downloadCsv()"
|
||||
(load)="load()"
|
||||
(preload)="preload()"
|
||||
>
|
||||
<ng-content select="v-table-actions"></ng-content>
|
||||
<v-table-inputs><ng-content select="v-table-inputs"></ng-content></v-table-inputs>
|
||||
</v-table-info-bar>
|
||||
<mat-card #scrollViewport class="card">
|
||||
<v-no-records
|
||||
[noRecords]="!(displayedCount$ | async)"
|
||||
[progress]="progress()"
|
||||
></v-no-records>
|
||||
<table
|
||||
#matTable
|
||||
[dataSource]="dataSource"
|
||||
[matSortActive]="(columnsDataProgress$ | async) || progress() ? '' : sort().active"
|
||||
[matSortDirection]="sort().direction"
|
||||
mat-table
|
||||
matSort
|
||||
(matSortChange)="sort.set($event)"
|
||||
>
|
||||
@if (rowSelectable()) {
|
||||
<v-select-column
|
||||
[(selected)]="rowSelected"
|
||||
[data]="displayedData$ | async"
|
||||
[name]="columnDefs.select"
|
||||
[progress]="progress()"
|
||||
></v-select-column>
|
||||
}
|
||||
@for (col of displayedNormColumns$ | async; track col; let colIndex = $index) {
|
||||
@let stickyStart = col.params.sticky === 'start';
|
||||
@let stickyEnd = col.params.sticky === 'end';
|
||||
@let columnClasses =
|
||||
{
|
||||
column: true,
|
||||
'column__sticky-start': stickyStart,
|
||||
'column__sticky-end': stickyEnd,
|
||||
};
|
||||
<ng-container
|
||||
[matColumnDef]="col.field"
|
||||
[sticky]="stickyStart"
|
||||
[stickyEnd]="stickyEnd"
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
[disabled]="(col.sort | async) === false || hasMore() || progress()"
|
||||
[mat-sort-header]="col.field"
|
||||
[ngClass]="columnClasses"
|
||||
mat-header-cell
|
||||
>
|
||||
<v-value [value]="col?.header | async" emptySymbol="" inline></v-value>
|
||||
</th>
|
||||
|
||||
<ng-template let-element let-rowIndex="index" matCellDef>
|
||||
@let cell = columnsData?.get?.(element)?.get(col);
|
||||
<td
|
||||
[ngClass]="columnClasses"
|
||||
[ngStyle]="col?.params?.style"
|
||||
[style.border-bottom]="cell?.isNextChild && !col.child ? 'none' : ''"
|
||||
mat-cell
|
||||
>
|
||||
<v-value
|
||||
[emptySymbol]="!(cell?.isChild && !col?.child)"
|
||||
[highlight]="filter$ | async"
|
||||
[lazyValue]="cell?.lazyValue"
|
||||
[lazyVisible]="loadedLazyItems.has(element)"
|
||||
[value]="cell?.value | async"
|
||||
inline
|
||||
(lazyVisibleChange)="loadedLazyItems.set(element, true)"
|
||||
></v-value>
|
||||
</td>
|
||||
</ng-template>
|
||||
<td *matFooterCellDef [ngClass]="columnClasses" mat-footer-cell>
|
||||
<v-content-loading></v-content-loading>
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns$ | async; sticky: true" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns$ | async" mat-row></tr>
|
||||
<tr
|
||||
*matFooterRowDef="(hasAutoShowMore$ | async) ? (displayedColumns$ | async) : []"
|
||||
[ngClass]="{ row__hidden: !(hasAutoShowMore$ | async) }"
|
||||
[vInfinityScroll]="
|
||||
!progress() &&
|
||||
!(columnsDataProgress$ | async) &&
|
||||
(filteredSortData$ | async)?.length
|
||||
"
|
||||
mat-row
|
||||
(vInfinityScrollMore)="showMore()"
|
||||
></tr>
|
||||
</table>
|
||||
</mat-card>
|
||||
</div>
|
@ -1,56 +0,0 @@
|
||||
:host {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
transform: translateZ(0);
|
||||
|
||||
::ng-deep .mdc-data-table__row:last-child .mat-mdc-cell {
|
||||
border-bottom-color: var(
|
||||
--mat-table-row-item-outline-color,
|
||||
rgba(0, 0, 0, 0.12)
|
||||
) !important;
|
||||
border-bottom-width: var(--mat-table-row-item-outline-width, 1px) !important;
|
||||
border-bottom-style: solid !important;
|
||||
}
|
||||
}
|
||||
|
||||
.show-more-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
max-width: max(20px, 30vw);
|
||||
|
||||
&__sticky-start,
|
||||
&__sticky-end {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&__sticky-start {
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
&__sticky-end {
|
||||
border-left: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-footer-cell {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.row__hidden {
|
||||
display: none;
|
||||
}
|
@ -1,382 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
computed,
|
||||
DestroyRef,
|
||||
booleanAttribute,
|
||||
numberAttribute,
|
||||
signal,
|
||||
output,
|
||||
Injector,
|
||||
ElementRef,
|
||||
runInInjectionContext,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ContentChild,
|
||||
model,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatSort, MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule, MatTable } from '@angular/material/table';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
switchMap,
|
||||
take,
|
||||
forkJoin,
|
||||
BehaviorSubject,
|
||||
debounceTime,
|
||||
first,
|
||||
merge,
|
||||
tap,
|
||||
defer,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
shareReplay,
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
delay,
|
||||
filter,
|
||||
startWith,
|
||||
share,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
downloadFile,
|
||||
createCsv,
|
||||
arrayAttribute,
|
||||
ArrayAttributeTransform,
|
||||
} from '../../../../utils';
|
||||
import { ContentLoadingComponent } from '../../../content-loading';
|
||||
import { ProgressModule } from '../../../progress';
|
||||
import { ValueComponent, ValueListComponent } from '../../../value';
|
||||
import { sortDataByDefault, DEFAULT_SORT } from '../../consts';
|
||||
import { Column2, UpdateOptions, NormColumn } from '../../types';
|
||||
import { TableDataSource } from '../../utils/table-data-source';
|
||||
import { tableToCsvObject } from '../../utils/table-to-csv-object';
|
||||
import { InfinityScrollDirective } from '../infinity-scroll.directive';
|
||||
import { NoRecordsComponent } from '../no-records.component';
|
||||
import { SelectColumnComponent } from '../select-column.component';
|
||||
import { ShowMoreButtonComponent } from '../show-more-button/show-more-button.component';
|
||||
import { TableInfoBarComponent } from '../table-info-bar/table-info-bar.component';
|
||||
import { TableInputsComponent } from '../table-inputs.component';
|
||||
import { TableProgressBarComponent } from '../table-progress-bar.component';
|
||||
|
||||
import { COLUMN_DEFS } from './consts';
|
||||
import { TreeData } from './tree-data';
|
||||
import { columnsDataToFilterSearchData, filterData, sortData } from './utils/filter-sort';
|
||||
import {
|
||||
toObservableColumnsData,
|
||||
toColumnsData,
|
||||
DisplayedDataItem,
|
||||
DisplayedData,
|
||||
} from './utils/to-columns-data';
|
||||
|
||||
const SHORT_DEBOUNCE_TIME_MS = 300;
|
||||
const DEBOUNCE_TIME_MS = 500;
|
||||
const DEFAULT_LOADED_LAZY_ROWS_COUNT = 3;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'v-table2',
|
||||
templateUrl: './table2.component.html',
|
||||
styleUrls: ['./table2.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatTableModule,
|
||||
MatCardModule,
|
||||
ValueComponent,
|
||||
TableProgressBarComponent,
|
||||
NoRecordsComponent,
|
||||
TableInfoBarComponent,
|
||||
ShowMoreButtonComponent,
|
||||
ContentLoadingComponent,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
InfinityScrollDirective,
|
||||
ValueListComponent,
|
||||
SelectColumnComponent,
|
||||
ProgressModule,
|
||||
MatSortModule,
|
||||
TableInputsComponent,
|
||||
MatButton,
|
||||
],
|
||||
})
|
||||
export class Table2Component<T extends object, C extends object> implements OnInit {
|
||||
data = input<T[]>();
|
||||
treeData = input<TreeData<T, C>>();
|
||||
columns = input<Column2<T, C>[], ArrayAttributeTransform<Column2<T, C>>>([], {
|
||||
transform: arrayAttribute,
|
||||
});
|
||||
progress = input(false, { transform: Boolean });
|
||||
hasMore = input(false, { transform: booleanAttribute });
|
||||
size = input(25, { transform: numberAttribute });
|
||||
maxSize = input(1000, { transform: numberAttribute });
|
||||
noDownload = input(false, { transform: booleanAttribute });
|
||||
|
||||
// Filter
|
||||
filter = model('');
|
||||
filter$ = toObservable(this.filter).pipe(
|
||||
map((v) => (v || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(DEBOUNCE_TIME_MS),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
standaloneFilter = input(false, { transform: booleanAttribute });
|
||||
externalFilter = input(false, { transform: booleanAttribute });
|
||||
filteredSortData$ = new BehaviorSubject<DisplayedData<T, C> | null>(null);
|
||||
displayedData$ = combineLatest([
|
||||
defer(() => this.dataSource.data$),
|
||||
this.filteredSortData$.pipe(distinctUntilChanged()),
|
||||
defer(() => this.columnsDataProgress$),
|
||||
]).pipe(
|
||||
map(
|
||||
([data, filteredSortData, columnsDataProgress]) =>
|
||||
(filteredSortData && !columnsDataProgress ? filteredSortData : data) || [],
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
displayedCount$ = this.displayedData$.pipe(
|
||||
map((data) => data.length),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
// Select
|
||||
rowSelectable = input(false, { transform: booleanAttribute });
|
||||
rowSelected = model<DisplayedData<T, C>>([]);
|
||||
|
||||
// Sort
|
||||
sort = model<Sort>(DEFAULT_SORT);
|
||||
@ViewChild(MatSort) sortComponent!: MatSort;
|
||||
|
||||
update = output<UpdateOptions>();
|
||||
more = output<UpdateOptions>();
|
||||
|
||||
loadedLazyItems = new WeakMap<DisplayedDataItem<T, C>, boolean>();
|
||||
|
||||
dataSource = new TableDataSource<T, C>();
|
||||
normColumns = computed<NormColumn<T, C>[]>(() => this.columns().map((c) => new NormColumn(c)));
|
||||
displayedNormColumns$ = toObservable(this.normColumns).pipe(
|
||||
switchMap((cols) =>
|
||||
combineLatest(cols.map((c) => c.hidden)).pipe(
|
||||
map((c) => cols.filter((_, idx) => !c[idx])),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
columnsData$$ = combineLatest({
|
||||
isTree: this.dataSource.isTreeData$,
|
||||
data: this.dataSource.data$,
|
||||
cols: toObservable(this.normColumns),
|
||||
}).pipe(toObservableColumnsData, shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
columnsDataProgress$ = new BehaviorSubject(false);
|
||||
columnsData$ = this.columnsData$$.pipe(
|
||||
tap(() => {
|
||||
this.columnsDataProgress$.next(true);
|
||||
}),
|
||||
toColumnsData,
|
||||
tap(() => {
|
||||
this.columnsDataProgress$.next(false);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
isPreload = signal(false);
|
||||
loadSize = computed(() => (this.isPreload() ? this.maxSize() : this.size()));
|
||||
hasAutoShowMore$ = combineLatest([
|
||||
toObservable(this.hasMore),
|
||||
this.dataSource.data$.pipe(map((d) => d?.length)),
|
||||
this.displayedCount$,
|
||||
this.dataSource.paginator.page.pipe(
|
||||
startWith(null),
|
||||
map(() => this.dataSource.paginator.pageSize),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([hasMore, dataCount, displayedDataCount, size]) =>
|
||||
(hasMore && displayedDataCount !== 0 && displayedDataCount >= dataCount) ||
|
||||
displayedDataCount > size,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
displayedColumns$ = combineLatest([
|
||||
this.displayedNormColumns$,
|
||||
toObservable(this.rowSelectable),
|
||||
]).pipe(
|
||||
map(([normColumns, rowSelectable]) => [
|
||||
...(rowSelectable ? [this.columnDefs.select] : []),
|
||||
...normColumns.map((c) => c.field),
|
||||
]),
|
||||
);
|
||||
columnDefs = COLUMN_DEFS;
|
||||
|
||||
@ViewChild('scrollViewport', { read: ElementRef }) scrollViewport!: ElementRef;
|
||||
@ViewChild('matTable', { static: false }) table!: MatTable<T>;
|
||||
@ContentChild(TableInputsComponent, { read: ElementRef }) tableInputsContent!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private dr: DestroyRef,
|
||||
private injector: Injector,
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const sort$ = toObservable(this.sort, { injector: this.injector }).pipe(
|
||||
distinctUntilChanged(),
|
||||
share(),
|
||||
);
|
||||
|
||||
toObservable(this.data, { injector: this.injector })
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.dr))
|
||||
.subscribe((data) => {
|
||||
this.dataSource.setData(data);
|
||||
});
|
||||
toObservable(this.treeData, { injector: this.injector })
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.dr))
|
||||
.subscribe((data) => {
|
||||
this.dataSource.setTreeData(data);
|
||||
});
|
||||
combineLatest([
|
||||
this.filter$,
|
||||
sort$,
|
||||
this.dataSource.data$,
|
||||
runInInjectionContext(this.injector, () =>
|
||||
this.columnsData$.pipe(columnsDataToFilterSearchData),
|
||||
),
|
||||
this.dataSource.isTreeData$,
|
||||
toObservable(this.normColumns, { injector: this.injector }),
|
||||
toObservable(this.externalFilter, { injector: this.injector }),
|
||||
])
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.filteredSortData$.next(null);
|
||||
}),
|
||||
debounceTime(SHORT_DEBOUNCE_TIME_MS),
|
||||
map(([search, sort, source, data, isTreeData, columns, isExternalFilter]) => {
|
||||
if (isTreeData) {
|
||||
return source;
|
||||
}
|
||||
const filteredData =
|
||||
!isExternalFilter && search ? filterData(data, search) : source;
|
||||
return sortData(filteredData, data, columns, sort);
|
||||
}),
|
||||
// distinctUntilChanged(isEqual),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe((filtered) => {
|
||||
this.updateSortFilter(filtered);
|
||||
this.updateLoadedLazyItems(filtered);
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
merge(
|
||||
this.filter$.pipe(filter(Boolean)),
|
||||
toObservable(this.hasMore, { injector: this.injector }).pipe(filter(Boolean)),
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.sort.set(DEFAULT_SORT);
|
||||
});
|
||||
merge(this.filter$, sort$)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.reset();
|
||||
});
|
||||
// TODO: 2, 3 column is torn away from the previous one, fixed by calling update
|
||||
this.dataSource.data$
|
||||
.pipe(
|
||||
filter((v) => !!v?.length),
|
||||
first(),
|
||||
delay(100),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.table.updateStickyColumnStyles();
|
||||
});
|
||||
}
|
||||
|
||||
load() {
|
||||
if (this.isPreload()) {
|
||||
this.isPreload.set(false);
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
|
||||
preload() {
|
||||
if (!this.isPreload()) {
|
||||
this.isPreload.set(true);
|
||||
this.reload();
|
||||
} else if (this.hasMore()) {
|
||||
this.more.emit({ size: this.loadSize() });
|
||||
}
|
||||
}
|
||||
|
||||
showMore() {
|
||||
this.dataSource.paginator.more();
|
||||
if (this.hasMore() && this.dataSource.paginator.pageSize > this.dataSource.data.length) {
|
||||
this.more.emit({ size: this.loadSize() });
|
||||
}
|
||||
this.refreshTable();
|
||||
}
|
||||
|
||||
downloadCsv() {
|
||||
this.generateCsvData()
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe((csvData) => {
|
||||
downloadFile(csvData, 'csv');
|
||||
});
|
||||
}
|
||||
|
||||
private generateCsvData(): Observable<string> {
|
||||
return combineLatest([
|
||||
this.displayedNormColumns$.pipe(
|
||||
switchMap((cols) => forkJoin(cols.map((c) => c.header.pipe(take(1))))),
|
||||
),
|
||||
this.columnsData$.pipe(take(1)),
|
||||
]).pipe(
|
||||
map(([cols, data]) =>
|
||||
createCsv(runInInjectionContext(this.injector, () => tableToCsvObject(cols, data))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private reload() {
|
||||
this.update.emit({ size: this.loadSize() });
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private updateSortFilter(filtered: DisplayedData<T, C>) {
|
||||
this.filteredSortData$.next(filtered);
|
||||
this.dataSource.sortData = filtered ? () => filtered : sortDataByDefault;
|
||||
this.dataSource.sort = this.sortComponent;
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.scrollViewport?.nativeElement?.scrollTo?.(0, 0);
|
||||
this.dataSource.paginator.reload();
|
||||
this.refreshTable();
|
||||
}
|
||||
|
||||
// TODO: Refresh table when pagination is updated
|
||||
private refreshTable() {
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.dataSource.data = this.dataSource.data;
|
||||
}
|
||||
|
||||
private updateLoadedLazyItems(items: DisplayedData<T, C>) {
|
||||
const lazyLoadedItems = items.slice(0, DEFAULT_LOADED_LAZY_ROWS_COUNT);
|
||||
for (const item of lazyLoadedItems) {
|
||||
this.loadedLazyItems.set(item, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,11 @@
|
||||
import { Sort, MatSort } from '@angular/material/sort';
|
||||
import { Sort } from '@angular/material/sort';
|
||||
|
||||
import { createUniqueColumnDef } from './utils/create-unique-column-def';
|
||||
|
||||
export const COMPLETE_MISMATCH_SCORE = 1;
|
||||
export const DEFAULT_SORT: Sort = { active: '', direction: '' };
|
||||
export const DEFAULT_DEBOUNCE_TIME_MS = 250;
|
||||
export const sortDataByDefault: <T>(data: T[], sort: MatSort) => T[] = (data) => data;
|
||||
export const DEBOUNCE_TIME_MS = 500;
|
||||
export const DEFAULT_LOADED_LAZY_ROWS_COUNT = 3;
|
||||
export const COLUMN_DEFS = {
|
||||
select: createUniqueColumnDef('select'),
|
||||
drag: createUniqueColumnDef('drag'),
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
export * from './table.component';
|
||||
export * from './table.module';
|
||||
export * from './utils/create-columns-objects';
|
||||
export * from './utils/correct-priorities';
|
||||
export * from './utils/create-column';
|
||||
export * from './utils/cached-head-map';
|
||||
export * from './components/table2';
|
||||
export * from './table.component';
|
||||
export * from './components/table-actions.component';
|
||||
export * from './components/table-inputs.component';
|
||||
export * from './types';
|
||||
export * from './presets';
|
||||
export * from './tree-data';
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { MenuValue } from '../../value/components/menu-value.component';
|
||||
// TODO: hack
|
||||
// eslint-disable-next-line
|
||||
import { Column } from '../types/column2';
|
||||
import { createColumn } from '../utils/create-column';
|
||||
|
||||
export const createMenuColumn = createColumn(
|
||||
(params: MenuValue['params']) => {
|
||||
return {
|
||||
type: 'menu',
|
||||
params,
|
||||
};
|
||||
},
|
||||
{
|
||||
field: 'menu',
|
||||
header: '',
|
||||
sticky: 'end',
|
||||
style: {
|
||||
padding: 0,
|
||||
width: '0',
|
||||
},
|
||||
},
|
||||
);
|
@ -1,38 +0,0 @@
|
||||
import { MenuValue } from '../../value/components/menu-value.component';
|
||||
import { MenuColumn } from '../types';
|
||||
import { createColumn } from '../utils/create-column';
|
||||
import { createUniqueColumnDef } from '../utils/create-unique-column-def';
|
||||
|
||||
export function createOperationColumn<T extends object>(
|
||||
items: MenuColumn<T>['typeParameters']['items'],
|
||||
other?: Partial<Omit<MenuColumn<T>, 'items'>>,
|
||||
): MenuColumn<T> {
|
||||
return {
|
||||
typeParameters: { ...(other?.typeParameters ?? {}), items },
|
||||
field: createUniqueColumnDef('operation'),
|
||||
header: '',
|
||||
pinned: 'right',
|
||||
width: '0',
|
||||
type: 'menu',
|
||||
...other,
|
||||
};
|
||||
}
|
||||
|
||||
// Table 2 Menu Column
|
||||
export const createMenuColumn = createColumn(
|
||||
(params: MenuValue['params']) => {
|
||||
return {
|
||||
type: 'menu',
|
||||
params,
|
||||
};
|
||||
},
|
||||
{
|
||||
field: 'menu',
|
||||
header: '',
|
||||
sticky: 'end',
|
||||
style: {
|
||||
padding: 0,
|
||||
width: '0',
|
||||
},
|
||||
},
|
||||
);
|
@ -1 +1 @@
|
||||
export * from './create-operation-column';
|
||||
export * from './create-menu-column';
|
||||
|
@ -1,301 +1,128 @@
|
||||
<div class="table">
|
||||
@if (!noFilter && (standaloneFilter || noActions)) {
|
||||
<div class="toolbar">
|
||||
<ng-content select="v-table-inputs"></ng-content>
|
||||
<mat-form-field class="v-table-filter-standalone">
|
||||
<mat-label>{{ externalFilter() ? 'Search' : 'Filter' }}</mat-label>
|
||||
<input [formControl]="filterControl" matInput />
|
||||
<div matSuffix style="white-space: nowrap">
|
||||
<ng-container *ngTemplateOutlet="exactButtonTpl"></ng-container>
|
||||
<button
|
||||
[disabled]="!filterControl.value"
|
||||
mat-icon-button
|
||||
(click)="filterControl.setValue('')"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-form-field>
|
||||
@if (noActions && update.observed) {
|
||||
<div style="flex: 0; white-space: nowrap; padding-top: 12px">
|
||||
<ng-container *ngTemplateOutlet="updateButtonsTpl"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!noActions) {
|
||||
<v-actions>
|
||||
<v-actions>
|
||||
<div style="display: flex">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="update.observed ? updateButtonsTpl : null"
|
||||
></ng-container>
|
||||
@if (!noFilter && !standaloneFilter) {
|
||||
<div style="margin: -6px 0">
|
||||
<ng-container *ngTemplateOutlet="exactButtonTpl"></ng-container>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (!noFilter && !standaloneFilter) {
|
||||
<div style="display: flex">
|
||||
<v-input-field
|
||||
[formControl]="filterControl"
|
||||
appearance="outline"
|
||||
class="v-table-filter"
|
||||
label="Filter"
|
||||
></v-input-field>
|
||||
@if (filterControl.value) {
|
||||
<button
|
||||
mat-icon-button
|
||||
style="margin: -6px 0"
|
||||
(click)="filterControl.setValue('')"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</v-actions>
|
||||
<div>
|
||||
<ng-content select="v-table-actions"></ng-content>
|
||||
</div>
|
||||
</v-actions>
|
||||
}
|
||||
<div class="table-card-details">
|
||||
<div class="details">
|
||||
<div>
|
||||
Quantity:
|
||||
{{
|
||||
progress
|
||||
? '...'
|
||||
: data()?.length
|
||||
? data().length + (hasMore ? ' (more available)' : ' (all)')
|
||||
: '0'
|
||||
}}
|
||||
@if (filterControl.value && !externalFilter()) {
|
||||
| Filtered: {{ filteredDataLength ?? '...' }}
|
||||
}
|
||||
@if (selected?.length) {
|
||||
|
|
||||
<b>Selected: {{ selected.length }}</b>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<mat-card class="table-card">
|
||||
@if (progress || (!externalFilter() && (filterProgress$ | async))) {
|
||||
<mat-progress-bar class="progress-bar" mode="indeterminate"></mat-progress-bar>
|
||||
}
|
||||
<table
|
||||
#table
|
||||
[cdkDropListDisabled]="dragDisabled"
|
||||
[dataSource]="dataSource"
|
||||
[matSortActive]="sort.active"
|
||||
[matSortDirection]="sort.direction"
|
||||
cdkDropList
|
||||
cdkDropListData="dataSource"
|
||||
mat-table
|
||||
matSort
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
(matSortChange)="sortChanged($event)"
|
||||
>
|
||||
<ng-container [matColumnDef]="dragColumnDef" [sticky]="true">
|
||||
<th *matHeaderCellDef mat-header-cell></th>
|
||||
<td *matCellDef="let row; let index = index" mat-cell>
|
||||
<div class="position">
|
||||
<mat-icon class="dragCursor" (mousedown)="dragDisabled = false"
|
||||
>reorder</mat-icon
|
||||
>
|
||||
<!-- <span>{{ index + 1 }}</span>-->
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<v-score-column [name]="scoreColumnDef" [scores]="scores"></v-score-column>
|
||||
@if (rowSelectable) {
|
||||
<v-select-column
|
||||
[data]="data()"
|
||||
[filtered]="!!filterControl.value"
|
||||
[name]="selectColumnDef"
|
||||
[progress]="progress"
|
||||
[selected]="rowSelected"
|
||||
(selectedChange)="rowSelectedChange.emit($event); selected = $event"
|
||||
></v-select-column>
|
||||
}
|
||||
@for (col of columnsObjects.values(); track col) {
|
||||
<ng-container
|
||||
[matColumnDef]="col.field"
|
||||
[sticky]="col.pinned === 'left'"
|
||||
[stickyEnd]="col.pinned === 'right'"
|
||||
>
|
||||
@if (col.sortable) {
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
[mat-sort-header]="col.field"
|
||||
[ngClass]="{
|
||||
'pinned-left': col.pinned === 'left',
|
||||
'pinned-right': col.pinned === 'right',
|
||||
}"
|
||||
[ngStyle]="{
|
||||
width: col.width,
|
||||
'min-width': col.minWidth,
|
||||
'max-width': col.maxWidth,
|
||||
}"
|
||||
mat-header-cell
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
} @else {
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
[ngClass]="{
|
||||
'pinned-left': col.pinned === 'left',
|
||||
'pinned-right': col.pinned === 'right',
|
||||
}"
|
||||
[ngStyle]="{
|
||||
width: col.width,
|
||||
'min-width': col.minWidth,
|
||||
'max-width': col.maxWidth,
|
||||
}"
|
||||
mat-header-cell
|
||||
>
|
||||
{{ col.header }}
|
||||
</th>
|
||||
}
|
||||
<td
|
||||
*matCellDef="let row; let index = index"
|
||||
[ngClass]="{
|
||||
'pinned-left': col.pinned === 'left',
|
||||
'pinned-right': col.pinned === 'right',
|
||||
td: true,
|
||||
}"
|
||||
[ngStyle]="{
|
||||
width: col.width,
|
||||
'min-width': col.minWidth,
|
||||
'max-width': col.maxWidth,
|
||||
}"
|
||||
mat-cell
|
||||
style="vertical-align: top; padding: 6px 16px"
|
||||
@let columnsData = columnsData$$ | async;
|
||||
|
||||
<div class="wrapper">
|
||||
<v-table-info-bar
|
||||
[(filter)]="filter"
|
||||
[count]="(dataSource.data$ | async)?.length"
|
||||
[dataProgress]="!!(columnsDataProgress$ | async) && !hasMore()"
|
||||
[filteredCount]="displayedCount$ | async"
|
||||
[hasInputs]="!!tableInputsContent?.nativeElement"
|
||||
[hasLoad]="update$.observed"
|
||||
[hasMore]="hasMore()"
|
||||
[isPreload]="isPreload()"
|
||||
[noDownload]="noDownload()"
|
||||
[preloadSize]="maxSize()"
|
||||
[progress]="progress()"
|
||||
[selectedCount]="rowSelected()?.length"
|
||||
[size]="size()"
|
||||
[standaloneFilter]="standaloneFilter()"
|
||||
(downloadCsv)="downloadCsv()"
|
||||
(load)="load()"
|
||||
(preload)="preload()"
|
||||
>
|
||||
<ng-content select="v-table-actions"></ng-content>
|
||||
<v-table-inputs><ng-content select="v-table-inputs"></ng-content></v-table-inputs>
|
||||
</v-table-info-bar>
|
||||
<mat-card #scrollViewport class="card">
|
||||
<v-no-records
|
||||
[noRecords]="!(displayedCount$ | async)"
|
||||
[progress]="progress()"
|
||||
></v-no-records>
|
||||
<table
|
||||
#matTable
|
||||
[cdkDropListDisabled]="dragDisabled"
|
||||
[dataSource]="dataSource"
|
||||
[matSortActive]="(columnsDataProgress$ | async) || progress() ? '' : sort().active"
|
||||
[matSortDirection]="sort().direction"
|
||||
cdkDropList
|
||||
cdkDropListData="dataSource"
|
||||
mat-table
|
||||
matSort
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
(matSortChange)="sort.set($event)"
|
||||
>
|
||||
<ng-container [matColumnDef]="columnDefs.drag" [sticky]="true">
|
||||
<th *matHeaderCellDef mat-header-cell></th>
|
||||
<td *matCellDef="let row; let index = index" mat-cell>
|
||||
<div class="position">
|
||||
<mat-icon class="dragCursor" (mousedown)="dragDisabled = false"
|
||||
>reorder</mat-icon
|
||||
>
|
||||
<div style="min-height: 40px; display: flex; align-items: center">
|
||||
<div>
|
||||
@if (!col.cellTemplate && !cellTemplate[col.field]) {
|
||||
<v-table-cell
|
||||
[colDef]="col"
|
||||
[index]="index"
|
||||
[preloadLazy]="
|
||||
index < preloadedLazyRowsCount ||
|
||||
!!preloadedLazyCells.get(row)
|
||||
"
|
||||
[rowData]="row"
|
||||
(preloadedLazyChange)="
|
||||
preloadedLazyCells.set(row, $event)
|
||||
"
|
||||
></v-table-cell>
|
||||
} @else {
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
col.cellTemplate ?? cellTemplate[col.field] ?? null;
|
||||
context: {
|
||||
colDef: col,
|
||||
index: index,
|
||||
rowData: row,
|
||||
value:
|
||||
row
|
||||
| vSelect
|
||||
: col.formatter ?? col.field
|
||||
: ''
|
||||
: [index, col],
|
||||
}
|
||||
"
|
||||
></ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[cdkDragData]="row"
|
||||
cdkDrag
|
||||
mat-row
|
||||
></tr>
|
||||
|
||||
<ng-container [matColumnDef]="noRecordsColumnDef">
|
||||
<td
|
||||
*matFooterCellDef
|
||||
[colSpan]="displayedColumns.length"
|
||||
class="no-records mat-body-1"
|
||||
mat-footer-cell
|
||||
<!-- <span>{{ index + 1 }}</span>-->
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@if (rowSelectable()) {
|
||||
<v-select-column
|
||||
[(selected)]="rowSelected"
|
||||
[data]="displayedData$ | async"
|
||||
[name]="columnDefs.select"
|
||||
[progress]="progress()"
|
||||
></v-select-column>
|
||||
}
|
||||
@for (col of displayedNormColumns$ | async; track col; let colIndex = $index) {
|
||||
@let stickyStart = col.params.sticky === 'start';
|
||||
@let stickyEnd = col.params.sticky === 'end';
|
||||
@let columnClasses =
|
||||
{
|
||||
column: true,
|
||||
'column__sticky-start': stickyStart,
|
||||
'column__sticky-end': stickyEnd,
|
||||
};
|
||||
<ng-container
|
||||
[matColumnDef]="col.field"
|
||||
[sticky]="stickyStart"
|
||||
[stickyEnd]="stickyEnd"
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
[disabled]="(col.sort | async) === false || hasMore() || progress()"
|
||||
[mat-sort-header]="col.field"
|
||||
[ngClass]="columnClasses"
|
||||
mat-header-cell
|
||||
>
|
||||
{{
|
||||
progress
|
||||
? 'Loading...'
|
||||
: !externalFilter() && (filterProgress$ | async)
|
||||
? 'Filtering...'
|
||||
: filteredDataLength === 0
|
||||
? 'No records found'
|
||||
: 'No records'
|
||||
}}
|
||||
<v-value [value]="col?.header | async" emptySymbol="" inline></v-value>
|
||||
</th>
|
||||
|
||||
<ng-template let-element let-rowIndex="index" matCellDef>
|
||||
@let cell = columnsData?.get?.(element)?.get(col);
|
||||
<td
|
||||
[ngClass]="columnClasses"
|
||||
[ngStyle]="col?.params?.style"
|
||||
[style.border-bottom]="cell?.isNextChild && !col.child ? 'none' : ''"
|
||||
mat-cell
|
||||
>
|
||||
<v-value
|
||||
[emptySymbol]="!(cell?.isChild && !col?.child)"
|
||||
[highlight]="filter$ | async"
|
||||
[lazyValue]="cell?.lazyValue"
|
||||
[lazyVisible]="loadedLazyItems.has(element)"
|
||||
[value]="cell?.value | async"
|
||||
inline
|
||||
(lazyVisibleChange)="loadedLazyItems.set(element, true)"
|
||||
></v-value>
|
||||
</td>
|
||||
</ng-template>
|
||||
<td *matFooterCellDef [ngClass]="columnClasses" mat-footer-cell>
|
||||
<v-content-loading></v-content-loading>
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<tr
|
||||
*matFooterRowDef="isNoRecords ? [noRecordsColumnDef] : []"
|
||||
[style.display]="isNoRecords ? 'table-row' : 'none'"
|
||||
mat-footer-row
|
||||
></tr>
|
||||
</table>
|
||||
</mat-card>
|
||||
</div>
|
||||
@if (hasShowMore) {
|
||||
<v-show-more-button
|
||||
[displayedPages]="displayedPages"
|
||||
[length]="data()?.length ?? 0"
|
||||
[pageSize]="size"
|
||||
[progress]="!!progress"
|
||||
(more)="showMore()"
|
||||
></v-show-more-button>
|
||||
}
|
||||
<tr *matHeaderRowDef="displayedColumns$ | async; sticky: true" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns$ | async"
|
||||
[cdkDragData]="row"
|
||||
cdkDrag
|
||||
mat-row
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns$ | async"
|
||||
[ngClass]="{ row__hidden: !(hasAutoShowMore$ | async) || !tableRow }"
|
||||
[vInfinityScroll]="!progress()"
|
||||
mat-footer-row
|
||||
(vInfinityScrollMore)="showMore()"
|
||||
></tr>
|
||||
</table>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<ng-template #updateButtonsTpl>
|
||||
<button
|
||||
[disabled]="!!progress"
|
||||
mat-icon-button
|
||||
matTooltip="Reload {{ size }} elements"
|
||||
style="margin: -6px 0"
|
||||
(click)="load()"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
[color]="isPreload && hasMore ? 'accent' : undefined"
|
||||
[disabled]="!!progress || !hasMore"
|
||||
mat-icon-button
|
||||
matTooltip="Preload {{ preloadSize }}{{ isPreload && hasMore ? ' more' : '' }} elements"
|
||||
style="margin: -6px 0"
|
||||
(click)="preload()"
|
||||
>
|
||||
<mat-icon>{{
|
||||
hasMore || progress
|
||||
? isPreload && hasMore
|
||||
? 'downloading'
|
||||
: 'download'
|
||||
: 'download_done'
|
||||
}}</mat-icon>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #exactButtonTpl>
|
||||
@if (!externalFilter()) {
|
||||
<v-switch-button
|
||||
[formControl]="exactFilterControl"
|
||||
[states]="[
|
||||
{ label: 'partial match search', icon: 'remove_moderator' },
|
||||
{ label: 'full match search', icon: 'policy' },
|
||||
]"
|
||||
></v-switch-button>
|
||||
}
|
||||
</ng-template>
|
||||
|
@ -1,3 +1,72 @@
|
||||
:host {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: max-content;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
transform: translateZ(0);
|
||||
|
||||
::ng-deep .mdc-data-table__row:last-child .mat-mdc-cell {
|
||||
border-bottom-color: var(
|
||||
--mat-table-row-item-outline-color,
|
||||
rgba(0, 0, 0, 0.12)
|
||||
) !important;
|
||||
border-bottom-width: var(--mat-table-row-item-outline-width, 1px) !important;
|
||||
border-bottom-style: solid !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table,
|
||||
::ng-deep .cdk-drag-preview {
|
||||
::ng-deep .cdk-drag-placeholder {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.dragCursor {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
max-width: max(20px, 30vw);
|
||||
|
||||
&__sticky-start,
|
||||
&__sticky-end {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&__sticky-start {
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
&__sticky-end {
|
||||
border-left: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
.row__hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Drag
|
||||
|
||||
::ng-deep .cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
@ -18,94 +87,8 @@
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.table,
|
||||
::ng-deep .cdk-drag-preview {
|
||||
::ng-deep .cdk-drag-placeholder {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.dragCursor {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
::ng-deep & > * {
|
||||
flex: 1;
|
||||
flex-basis: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-table-filter,
|
||||
.v-table-filter-standalone {
|
||||
::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > *,
|
||||
mat-icon {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
.action {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.pinned-right,
|
||||
.pinned-left {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.pinned-right {
|
||||
border-left: 1px solid;
|
||||
}
|
||||
|
||||
.pinned-left {
|
||||
border-right: 1px solid;
|
||||
}
|
||||
|
||||
.no-records {
|
||||
text-align: center;
|
||||
height: 52px * 3;
|
||||
}
|
||||
}
|
||||
::ng-deep td {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
@ -1,506 +1,428 @@
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ContentChild,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
numberAttribute,
|
||||
booleanAttribute,
|
||||
OnDestroy,
|
||||
input,
|
||||
computed,
|
||||
DestroyRef,
|
||||
booleanAttribute,
|
||||
numberAttribute,
|
||||
signal,
|
||||
output,
|
||||
Injector,
|
||||
ElementRef,
|
||||
runInInjectionContext,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ContentChild,
|
||||
model,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTable } from '@angular/material/table';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
import Fuse from 'fuse.js';
|
||||
import { toObservable, takeUntilDestroyed, outputFromObservable } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton, MatButton } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatSort, MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { MatTableModule, MatTable, MatRow } from '@angular/material/table';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import {
|
||||
combineLatest,
|
||||
map,
|
||||
of,
|
||||
take,
|
||||
debounceTime,
|
||||
Observable,
|
||||
switchMap,
|
||||
take,
|
||||
forkJoin,
|
||||
BehaviorSubject,
|
||||
debounceTime,
|
||||
first,
|
||||
merge,
|
||||
tap,
|
||||
Observable,
|
||||
catchError,
|
||||
defer,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { distinctUntilChanged, startWith } from 'rxjs/operators';
|
||||
|
||||
import { QueryParamsService, QueryParamsNamespace } from '../../services';
|
||||
import { Progressable } from '../../types/progressable';
|
||||
import {
|
||||
compareDifferentTypes,
|
||||
ComponentChanges,
|
||||
select,
|
||||
getPossiblyAsyncObservable,
|
||||
} from '../../utils';
|
||||
shareReplay,
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
delay,
|
||||
filter,
|
||||
startWith,
|
||||
share,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { TableActionsComponent } from './components/table-actions.component';
|
||||
import { downloadFile, createCsv, arrayAttribute, ArrayAttributeTransform } from '../../utils';
|
||||
import { ContentLoadingComponent } from '../content-loading';
|
||||
import { ProgressModule } from '../progress';
|
||||
import { ValueComponent, ValueListComponent } from '../value';
|
||||
|
||||
import { InfinityScrollDirective } from './components/infinity-scroll.directive';
|
||||
import { NoRecordsComponent } from './components/no-records.component';
|
||||
import { SelectColumnComponent } from './components/select-column.component';
|
||||
import { TableInfoBarComponent } from './components/table-info-bar/table-info-bar.component';
|
||||
import { TableInputsComponent } from './components/table-inputs.component';
|
||||
import { TableProgressBarComponent } from './components/table-progress-bar.component';
|
||||
import {
|
||||
DEFAULT_DEBOUNCE_TIME_MS,
|
||||
DEBOUNCE_TIME_MS,
|
||||
DEFAULT_LOADED_LAZY_ROWS_COUNT,
|
||||
DEFAULT_SORT,
|
||||
COMPLETE_MISMATCH_SCORE,
|
||||
sortDataByDefault,
|
||||
COLUMN_DEFS,
|
||||
} from './consts';
|
||||
import { Column, ColumnObject, UpdateOptions, DragDrop } from './types';
|
||||
import { createColumnsObjects } from './utils/create-columns-objects';
|
||||
import { createUniqueColumnDef } from './utils/create-unique-column-def';
|
||||
import { OnePageTableDataSourcePaginator } from './utils/one-page-table-data-source-paginator';
|
||||
import { TreeData } from './tree-data';
|
||||
import { Column, UpdateOptions, NormColumn, DragDrop } from './types';
|
||||
import { columnsDataToFilterSearchData, filterData, sortData } from './utils/filter-sort';
|
||||
import { TableDataSource } from './utils/table-data-source';
|
||||
import { tableToCsvObject } from './utils/table-to-csv-object';
|
||||
import {
|
||||
toObservableColumnsData,
|
||||
toColumnsData,
|
||||
DisplayedDataItem,
|
||||
DisplayedData,
|
||||
} from './utils/to-columns-data';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'v-table',
|
||||
templateUrl: './table.component.html',
|
||||
styleUrls: ['./table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatTableModule,
|
||||
MatCardModule,
|
||||
ValueComponent,
|
||||
TableProgressBarComponent,
|
||||
NoRecordsComponent,
|
||||
TableInfoBarComponent,
|
||||
ContentLoadingComponent,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
MatIconButton,
|
||||
InfinityScrollDirective,
|
||||
ValueListComponent,
|
||||
SelectColumnComponent,
|
||||
ProgressModule,
|
||||
MatSortModule,
|
||||
TableInputsComponent,
|
||||
MatButton,
|
||||
CdkDrag,
|
||||
CdkDropList,
|
||||
],
|
||||
})
|
||||
export class TableComponent<T extends object>
|
||||
implements OnInit, Progressable, OnChanges, AfterViewInit, OnDestroy
|
||||
{
|
||||
data = input<T[]>([]);
|
||||
|
||||
@Input() columns!: Column<T>[];
|
||||
@Input() cellTemplate: Record<ColumnObject<T>['field'], ColumnObject<T>['cellTemplate']> = {};
|
||||
@Input() progress?: boolean | number | null = false;
|
||||
@Input() preloadedLazyRowsCount = 3;
|
||||
|
||||
@Input({ transform: numberAttribute }) size: number = 25;
|
||||
@Input() preloadSize: number = 1000;
|
||||
@Input() name?: string;
|
||||
|
||||
@Input({ transform: booleanAttribute }) hasMore: boolean = false;
|
||||
@Output() update = new EventEmitter<UpdateOptions>();
|
||||
@Output() more = new EventEmitter<UpdateOptions>();
|
||||
|
||||
// Actions
|
||||
@Input({ transform: booleanAttribute }) noActions: boolean = false;
|
||||
@ContentChild(TableActionsComponent) actions!: TableActionsComponent;
|
||||
@ContentChild(TableInputsComponent) inputs!: TableInputsComponent;
|
||||
|
||||
// Sort
|
||||
@Input() sort: Sort = DEFAULT_SORT;
|
||||
@Output() sortChange = new EventEmitter<Sort>();
|
||||
@Input({ transform: booleanAttribute }) sortOnFront: boolean = false;
|
||||
@ViewChild(MatSort) sortComponent!: MatSort;
|
||||
|
||||
// Select
|
||||
@Input({ transform: booleanAttribute }) rowSelectable: boolean = false;
|
||||
@Input() rowSelected!: T[];
|
||||
@Output() rowSelectedChange = new EventEmitter<T[]>();
|
||||
selectColumnDef = createUniqueColumnDef('select');
|
||||
selected: T[] = [];
|
||||
export class TableComponent<T extends object, C extends object> implements OnInit {
|
||||
data = input<T[]>();
|
||||
treeData = input<TreeData<T, C>>();
|
||||
columns = input<Column<T, C>[], ArrayAttributeTransform<Column<T, C>>>([], {
|
||||
transform: arrayAttribute,
|
||||
});
|
||||
progress = input(false, { transform: Boolean });
|
||||
hasMore = input(false, { transform: booleanAttribute });
|
||||
size = input(25, { transform: numberAttribute });
|
||||
maxSize = input(1000, { transform: numberAttribute });
|
||||
noDownload = input(false, { transform: booleanAttribute });
|
||||
|
||||
// Filter
|
||||
@Input({ transform: booleanAttribute }) noFilter: boolean = false;
|
||||
@Input({ transform: booleanAttribute }) standaloneFilter: boolean = false;
|
||||
filter = model('');
|
||||
filter$ = toObservable(this.filter).pipe(
|
||||
map((v) => (v || '').trim()),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(DEBOUNCE_TIME_MS),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
standaloneFilter = input(false, { transform: booleanAttribute });
|
||||
externalFilter = input(false, { transform: booleanAttribute });
|
||||
@Input() filter = '';
|
||||
// TODO: filter by rendered column fields, it will be useful if you save the render in memory
|
||||
@Input() filterByColumns?: string[];
|
||||
@Output() filterChange = new EventEmitter<string>();
|
||||
filterControl = new FormControl('');
|
||||
exactFilterControl = new FormControl(1);
|
||||
scores = new Map<T, { score: number }>();
|
||||
filteredDataLength?: number;
|
||||
filterProgress$ = new BehaviorSubject(false);
|
||||
filteredSortData$ = new BehaviorSubject<DisplayedData<T, C> | null>(null);
|
||||
displayedData$ = combineLatest([
|
||||
defer(() => this.dataSource.data$),
|
||||
this.filteredSortData$.pipe(distinctUntilChanged()),
|
||||
defer(() => this.columnsDataProgress$),
|
||||
]).pipe(
|
||||
map(
|
||||
([data, filteredSortData, columnsDataProgress]) =>
|
||||
(filteredSortData && !columnsDataProgress ? filteredSortData : data) || [],
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
displayedCount$ = this.displayedData$.pipe(
|
||||
map((data) => data.length),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@Input({
|
||||
// Select
|
||||
rowSelectable = input(false, { transform: booleanAttribute });
|
||||
rowSelected = model<DisplayedData<T, C>>([]);
|
||||
|
||||
// Sort
|
||||
sort = model<Sort>(DEFAULT_SORT);
|
||||
@ViewChild(MatSort) sortComponent!: MatSort;
|
||||
|
||||
// Drag & drop
|
||||
rowDragDrop = input(false, {
|
||||
transform: (v: boolean | string[]) => {
|
||||
if (Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
return booleanAttribute(v);
|
||||
},
|
||||
})
|
||||
rowDragDrop: boolean | string[] = false;
|
||||
@Output() rowDropped = new EventEmitter<DragDrop<T>>();
|
||||
});
|
||||
rowDropped = output<DragDrop<DisplayedDataItem<T, C>>>();
|
||||
dragDisabled = true;
|
||||
|
||||
columnsObjects = new Map<ColumnObject<T>['field'], ColumnObject<T>>([]);
|
||||
update$ = new Subject<UpdateOptions>();
|
||||
update = outputFromObservable(this.update$);
|
||||
more = output<UpdateOptions>();
|
||||
|
||||
isPreload = false;
|
||||
loadedLazyItems = new WeakMap<DisplayedDataItem<T, C>, boolean>();
|
||||
|
||||
dataSource = new MatTableDataSource<T>();
|
||||
dataSource = new TableDataSource<T, C>();
|
||||
normColumns = computed<NormColumn<T, C>[]>(() => this.columns().map((c) => new NormColumn(c)));
|
||||
displayedNormColumns$ = toObservable(this.normColumns).pipe(
|
||||
switchMap((cols) =>
|
||||
combineLatest(cols.map((c) => c.hidden)).pipe(
|
||||
map((c) => cols.filter((_, idx) => !c[idx])),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
columnsData$$ = combineLatest({
|
||||
isTree: this.dataSource.isTreeData$,
|
||||
data: this.dataSource.data$,
|
||||
cols: toObservable(this.normColumns),
|
||||
}).pipe(toObservableColumnsData, shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
columnsDataProgress$ = new BehaviorSubject(false);
|
||||
columnsData$ = this.columnsData$$.pipe(
|
||||
tap(() => {
|
||||
this.columnsDataProgress$.next(true);
|
||||
}),
|
||||
toColumnsData,
|
||||
tap(() => {
|
||||
this.columnsDataProgress$.next(false);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
isPreload = signal(false);
|
||||
loadSize = computed(() => (this.isPreload() ? this.maxSize() : this.size()));
|
||||
hasAutoShowMore$ = combineLatest([
|
||||
toObservable(this.hasMore),
|
||||
this.dataSource.data$.pipe(map((d) => d?.length)),
|
||||
this.displayedCount$,
|
||||
this.dataSource.paginator.page.pipe(
|
||||
startWith(null),
|
||||
map(() => this.dataSource.paginator.pageSize),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([hasMore, dataCount, displayedDataCount, size]) =>
|
||||
(hasMore && displayedDataCount !== 0 && displayedDataCount >= dataCount) ||
|
||||
displayedDataCount > size,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
displayedColumns: string[] = [];
|
||||
displayedColumns$ = combineLatest([
|
||||
this.displayedNormColumns$,
|
||||
toObservable(this.rowSelectable),
|
||||
toObservable(this.sort),
|
||||
toObservable(this.rowDragDrop),
|
||||
]).pipe(
|
||||
map(([normColumns, rowSelectable, sort, rowDragDrop]) => [
|
||||
...((
|
||||
Array.isArray(rowDragDrop)
|
||||
? sort?.direction && rowDragDrop.includes(sort?.active)
|
||||
: rowDragDrop
|
||||
)
|
||||
? [this.columnDefs.drag]
|
||||
: []),
|
||||
...(rowSelectable ? [this.columnDefs.select] : []),
|
||||
...normColumns.map((c) => c.field),
|
||||
]),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
columnDefs = COLUMN_DEFS;
|
||||
|
||||
scoreColumnDef = createUniqueColumnDef('score');
|
||||
noRecordsColumnDef = createUniqueColumnDef('no-records');
|
||||
dragColumnDef = createUniqueColumnDef('drag');
|
||||
|
||||
preloadedLazyCells = new Map<T, boolean>();
|
||||
|
||||
@ViewChild('table', { static: true }) table!: MatTable<T>;
|
||||
|
||||
get displayedPages() {
|
||||
return this.paginator.displayedPages;
|
||||
}
|
||||
|
||||
get currentSize() {
|
||||
return this.isPreload ? this.preloadSize : this.size;
|
||||
}
|
||||
|
||||
get hasShowMore() {
|
||||
return (
|
||||
this.hasMore ||
|
||||
(this.filteredDataLength ?? this.data()?.length) > this.size * this.displayedPages
|
||||
);
|
||||
}
|
||||
|
||||
get isNoRecords() {
|
||||
return !this.data()?.length || this.filteredDataLength === 0;
|
||||
}
|
||||
|
||||
private paginator!: OnePageTableDataSourcePaginator;
|
||||
private qp?: QueryParamsNamespace<{ filter: string; exact?: boolean }>;
|
||||
@ViewChild('scrollViewport', { read: ElementRef }) scrollViewport!: ElementRef;
|
||||
@ViewChild('matTable', { static: false }) table!: MatTable<T>;
|
||||
@ContentChild(TableInputsComponent, { read: ElementRef }) tableInputsContent!: ElementRef;
|
||||
@ViewChild(MatRow, { static: false }) tableRow!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private destroyRef: DestroyRef,
|
||||
private queryParamsService: QueryParamsService,
|
||||
private dr: DestroyRef,
|
||||
private injector: Injector,
|
||||
) {
|
||||
this.updatePaginator();
|
||||
}
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
const startValue = this.filterControl.value;
|
||||
const filter$ = this.filterControl.valueChanges.pipe(
|
||||
...((startValue ? [startWith(startValue)] : []) as []),
|
||||
map((value) => (value || '').trim()),
|
||||
const sort$ = toObservable(this.sort, { injector: this.injector }).pipe(
|
||||
distinctUntilChanged(),
|
||||
debounceTime(DEFAULT_DEBOUNCE_TIME_MS),
|
||||
share(),
|
||||
);
|
||||
filter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((filter) => {
|
||||
this.filterChange.emit(filter);
|
||||
this.qp?.patch?.({ filter });
|
||||
});
|
||||
const exactFilter$ = this.exactFilterControl.valueChanges.pipe(
|
||||
startWith(this.exactFilterControl.value),
|
||||
map(Boolean),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
exactFilter$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((exact) => {
|
||||
this.qp?.patch?.({ exact });
|
||||
});
|
||||
|
||||
toObservable(this.data, { injector: this.injector })
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.dr))
|
||||
.subscribe((data) => {
|
||||
this.dataSource.setData(data);
|
||||
});
|
||||
toObservable(this.treeData, { injector: this.injector })
|
||||
.pipe(filter(Boolean), takeUntilDestroyed(this.dr))
|
||||
.subscribe((data) => {
|
||||
this.dataSource.setTreeData(data);
|
||||
});
|
||||
combineLatest([
|
||||
filter$,
|
||||
exactFilter$,
|
||||
toObservable(this.data, { injector: this.injector }),
|
||||
this.filter$,
|
||||
sort$,
|
||||
this.dataSource.data$,
|
||||
runInInjectionContext(this.injector, () =>
|
||||
this.columnsData$.pipe(columnsDataToFilterSearchData),
|
||||
),
|
||||
this.dataSource.isTreeData$,
|
||||
toObservable(this.normColumns, { injector: this.injector }),
|
||||
toObservable(this.externalFilter, { injector: this.injector }),
|
||||
])
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.filterProgress$.next(true);
|
||||
delete this.filteredDataLength;
|
||||
}),
|
||||
debounceTime(DEFAULT_DEBOUNCE_TIME_MS),
|
||||
switchMap(([filter, exact]): Observable<[Map<T, { score: number }>, string]> => {
|
||||
if (!filter || this.externalFilter() || !this.data()?.length) {
|
||||
return of([new Map(), filter]);
|
||||
map(([search, sort, source, data, isTreeData, columns, isExternalFilter]) => {
|
||||
if (isTreeData) {
|
||||
return source;
|
||||
}
|
||||
const cols = this.filterByColumns
|
||||
? this.filterByColumns.map(
|
||||
(c) => this.columnsObjects.get(c) as ColumnObject<T>,
|
||||
)
|
||||
: Array.from(this.columnsObjects.values());
|
||||
// TODO: Refactor
|
||||
return (
|
||||
cols.length
|
||||
? forkJoin([
|
||||
forkJoin(
|
||||
this.data().map((sourceValue, index) =>
|
||||
combineLatest(
|
||||
cols.map((colDef) =>
|
||||
colDef.lazy
|
||||
? of('')
|
||||
: getPossiblyAsyncObservable(
|
||||
select(
|
||||
sourceValue,
|
||||
colDef.formatter ?? colDef.field,
|
||||
'',
|
||||
[index, colDef] as never,
|
||||
),
|
||||
).pipe(catchError(() => of(''))),
|
||||
),
|
||||
).pipe(take(1)),
|
||||
),
|
||||
),
|
||||
forkJoin(
|
||||
this.data().map((sourceValue, index) =>
|
||||
combineLatest(
|
||||
cols.map((colDef) =>
|
||||
colDef.description && !colDef.lazy
|
||||
? getPossiblyAsyncObservable(
|
||||
select(
|
||||
sourceValue,
|
||||
colDef.description,
|
||||
'',
|
||||
[index, colDef] as never,
|
||||
),
|
||||
).pipe(catchError(() => of('')))
|
||||
: of(''),
|
||||
),
|
||||
).pipe(take(1)),
|
||||
),
|
||||
),
|
||||
])
|
||||
: of([[] as unknown[], [] as unknown[]])
|
||||
).pipe(
|
||||
map(([formattedValues, formattedDescription]) => {
|
||||
const fuseData = this.data().map((item, idx) => ({
|
||||
// TODO: add weights
|
||||
value: JSON.stringify(item),
|
||||
formattedValue: JSON.stringify(formattedValues[idx]), // TODO: split columns
|
||||
formattedDescription: JSON.stringify(formattedDescription[idx]),
|
||||
}));
|
||||
const fuse = new Fuse(fuseData, {
|
||||
keys: Object.keys(fuseData[0]),
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
findAllMatches: true,
|
||||
ignoreLocation: true,
|
||||
threshold: exact ? 0 : 0.6,
|
||||
});
|
||||
const filterResult = fuse.search(filter);
|
||||
return [
|
||||
new Map(
|
||||
filterResult.map(({ refIndex, score }) => [
|
||||
this.data()[refIndex],
|
||||
{ score: score ?? COMPLETE_MISMATCH_SCORE },
|
||||
]),
|
||||
),
|
||||
filter,
|
||||
];
|
||||
}),
|
||||
);
|
||||
const filteredData =
|
||||
!isExternalFilter && search ? filterData(data, search) : source;
|
||||
return sortData(filteredData, data, columns, sort);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe(([scores, filter]) => {
|
||||
this.scores = scores;
|
||||
this.sortChanged(
|
||||
filter && !this.externalFilter()
|
||||
? { active: this.scoreColumnDef, direction: 'asc' }
|
||||
: this.sortComponent.active === this.scoreColumnDef
|
||||
? DEFAULT_SORT
|
||||
: this.sort,
|
||||
);
|
||||
this.filterProgress$.next(false);
|
||||
.subscribe((filtered) => {
|
||||
this.updateSortFilter(filtered);
|
||||
this.updateLoadedLazyItems(filtered);
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
merge(
|
||||
this.filter$.pipe(filter(Boolean)),
|
||||
toObservable(this.hasMore, { injector: this.injector }).pipe(filter(Boolean)),
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.sort.set(DEFAULT_SORT);
|
||||
});
|
||||
merge(this.filter$, sort$)
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe(() => {
|
||||
this.reset();
|
||||
});
|
||||
// TODO: 2, 3 column is torn away from the previous one, fixed by calling update
|
||||
this.dataSource.data$
|
||||
.pipe(
|
||||
filter((v) => !!v?.length),
|
||||
first(),
|
||||
delay(100),
|
||||
takeUntilDestroyed(this.dr),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.table.updateStickyColumnStyles();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.sort = this.sortComponent;
|
||||
this.updateSort();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: ComponentChanges<TableComponent<T>>) {
|
||||
if (changes.columns) {
|
||||
this.updateColumns();
|
||||
load() {
|
||||
if (this.isPreload()) {
|
||||
this.isPreload.set(false);
|
||||
}
|
||||
if (changes.columns || changes.rowSelectable || changes.rowDragDrop) {
|
||||
this.updateDisplayedColumns();
|
||||
}
|
||||
if (changes.data) {
|
||||
this.dataSource.data = this.data();
|
||||
this.preloadedLazyCells = new Map();
|
||||
}
|
||||
if (this.dataSource.sort && changes.sort) {
|
||||
this.updateSort();
|
||||
}
|
||||
if (changes.size) {
|
||||
this.updatePaginator();
|
||||
}
|
||||
if (changes.filter) {
|
||||
this.filterControl.setValue(this.filter ?? '');
|
||||
}
|
||||
if (changes.name && this.name) {
|
||||
if (this.qp) {
|
||||
this.qp.destroy();
|
||||
}
|
||||
this.qp = this.queryParamsService.createNamespace(this.name);
|
||||
const filter = this.qp.params?.filter ?? '';
|
||||
if (filter) {
|
||||
this.filterControl.patchValue(filter);
|
||||
}
|
||||
const exact = this.qp.params?.exact ?? this.exactFilterControl.value;
|
||||
if (exact !== this.exactFilterControl.value) {
|
||||
this.exactFilterControl.setValue(1);
|
||||
}
|
||||
}
|
||||
if (changes.sortOnFront || changes.data) {
|
||||
this.tryFrontSort();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.qp?.destroy?.();
|
||||
}
|
||||
|
||||
updateColumns(columns: ColumnObject<T>[] = createColumnsObjects(this.columns)) {
|
||||
this.columnsObjects = new Map((columns || []).map((c) => [c.field, c]));
|
||||
}
|
||||
|
||||
load(isPreload = false) {
|
||||
if (this.isPreload !== isPreload) {
|
||||
this.isPreload = isPreload;
|
||||
}
|
||||
this.update.emit({ size: this.currentSize });
|
||||
this.paginator.reload();
|
||||
this.reload();
|
||||
}
|
||||
|
||||
preload() {
|
||||
if (this.isPreload && this.hasMore) {
|
||||
this.more.emit({ size: this.currentSize });
|
||||
return;
|
||||
if (!this.isPreload()) {
|
||||
this.isPreload.set(true);
|
||||
this.reload();
|
||||
} else if (this.hasMore()) {
|
||||
this.more.emit({ size: this.loadSize() });
|
||||
}
|
||||
this.load(true);
|
||||
}
|
||||
|
||||
showMore() {
|
||||
this.paginator.more();
|
||||
if (this.hasMore && this.displayedPages * this.size > this.data()?.length) {
|
||||
this.more.emit({ size: this.currentSize });
|
||||
this.dataSource.paginator.more();
|
||||
if (this.hasMore() && this.dataSource.paginator.pageSize > this.dataSource.data.length) {
|
||||
this.more.emit({ size: this.loadSize() });
|
||||
}
|
||||
this.refreshTable();
|
||||
}
|
||||
|
||||
sortChanged(sort: Sort) {
|
||||
this.sortChange.emit(sort);
|
||||
this.tryFrontSort(sort);
|
||||
this.updateDisplayedColumns();
|
||||
downloadCsv() {
|
||||
this.generateCsvData()
|
||||
.pipe(takeUntilDestroyed(this.dr))
|
||||
.subscribe((csvData) => {
|
||||
downloadFile(csvData, 'csv');
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
drop(event: CdkDragDrop<any>) {
|
||||
this.dragDisabled = true;
|
||||
const item = event.item.data;
|
||||
const { previousIndex, currentIndex } = event;
|
||||
const previousData = this.dataSource.sortData(this.data(), this.sortComponent);
|
||||
const currentData = previousData.slice();
|
||||
let currentDataIndex = 0;
|
||||
if (previousIndex > currentIndex) {
|
||||
currentData.splice(previousIndex, 1);
|
||||
currentData.splice(currentIndex, 0, event.item.data);
|
||||
currentDataIndex = currentIndex;
|
||||
} else {
|
||||
currentData.splice(currentIndex, 0, event.item.data);
|
||||
currentData.splice(previousIndex, 1);
|
||||
currentDataIndex = currentIndex - 1;
|
||||
}
|
||||
this.rowDropped.emit({
|
||||
previousIndex,
|
||||
currentIndex,
|
||||
item,
|
||||
previousData,
|
||||
currentData,
|
||||
currentDataIndex,
|
||||
sort: this?.sortComponent,
|
||||
this.filteredSortData$.pipe(filter(Boolean), first()).subscribe((data) => {
|
||||
const item = event.item.data;
|
||||
const { previousIndex, currentIndex } = event;
|
||||
const previousData = data;
|
||||
const currentData = previousData.slice();
|
||||
let currentDataIndex = 0;
|
||||
if (previousIndex > currentIndex) {
|
||||
currentData.splice(previousIndex, 1);
|
||||
currentData.splice(currentIndex, 0, event.item.data);
|
||||
currentDataIndex = currentIndex;
|
||||
} else {
|
||||
currentData.splice(currentIndex, 0, event.item.data);
|
||||
currentData.splice(previousIndex, 1);
|
||||
currentDataIndex = currentIndex - 1;
|
||||
}
|
||||
this.rowDropped.emit({
|
||||
previousIndex,
|
||||
currentIndex,
|
||||
item,
|
||||
previousData,
|
||||
currentData,
|
||||
currentDataIndex,
|
||||
sort: this?.sortComponent,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private tryFrontSort({ active, direction }: Partial<Sort> = this.sortComponent || {}) {
|
||||
const data = this.data();
|
||||
if (!data?.length || !active || !direction) {
|
||||
this.updateDataSourceSort();
|
||||
return;
|
||||
}
|
||||
if (active === this.scoreColumnDef && this.filterControl.value) {
|
||||
let sortedData = data
|
||||
.filter(
|
||||
(data) =>
|
||||
(this.scores.get(data)?.score ?? COMPLETE_MISMATCH_SCORE) <
|
||||
COMPLETE_MISMATCH_SCORE,
|
||||
)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(this.scores.get(a)?.score ?? COMPLETE_MISMATCH_SCORE) -
|
||||
(this.scores.get(b)?.score ?? COMPLETE_MISMATCH_SCORE),
|
||||
);
|
||||
if (direction === 'desc') {
|
||||
sortedData = sortedData.reverse();
|
||||
}
|
||||
this.filteredDataLength = sortedData.length;
|
||||
this.updateDataSourceSort(sortedData);
|
||||
return;
|
||||
}
|
||||
if (!this.sortOnFront) {
|
||||
this.updateDataSourceSort();
|
||||
return;
|
||||
}
|
||||
if (this.filterControl.value) {
|
||||
this.filterControl.setValue('');
|
||||
}
|
||||
const colDef = this.columnsObjects.get(active);
|
||||
if (!colDef) {
|
||||
this.updateDataSourceSort();
|
||||
return;
|
||||
}
|
||||
combineLatest(
|
||||
data.map((sourceValue, index) =>
|
||||
getPossiblyAsyncObservable(
|
||||
select(sourceValue, colDef.formatter ?? colDef.field, '', [
|
||||
index,
|
||||
colDef,
|
||||
] as never),
|
||||
).pipe(map((value) => ({ value, sourceValue }))),
|
||||
private generateCsvData(): Observable<string> {
|
||||
return combineLatest([
|
||||
this.displayedNormColumns$.pipe(
|
||||
switchMap((cols) => forkJoin(cols.map((c) => c.header.pipe(take(1))))),
|
||||
),
|
||||
)
|
||||
.pipe(take(1), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((loadedData) => {
|
||||
let sortedData = loadedData
|
||||
.sort((a, b) => compareDifferentTypes(a.value, b.value))
|
||||
.map((v) => v.sourceValue);
|
||||
if (direction === 'desc') {
|
||||
sortedData = sortedData.reverse();
|
||||
}
|
||||
this.updateDataSourceSort(sortedData);
|
||||
});
|
||||
this.columnsData$.pipe(take(1)),
|
||||
]).pipe(
|
||||
map(([cols, data]) =>
|
||||
createCsv(runInInjectionContext(this.injector, () => tableToCsvObject(cols, data))),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private updateDataSourceSort(sortedData?: T[]) {
|
||||
this.dataSource.sortData = sortedData ? () => sortedData : sortDataByDefault;
|
||||
// TODO: hack for update
|
||||
private reload() {
|
||||
this.update$.next({ size: this.loadSize() });
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private updateSortFilter(filtered: DisplayedData<T, C>) {
|
||||
this.filteredSortData$.next(filtered);
|
||||
this.dataSource.sortData = () => filtered;
|
||||
this.dataSource.sort = this.sortComponent;
|
||||
}
|
||||
|
||||
private updatePaginator() {
|
||||
this.paginator = new OnePageTableDataSourcePaginator(this.size);
|
||||
this.dataSource.paginator = this.paginator as never;
|
||||
private reset() {
|
||||
(this.scrollViewport?.nativeElement as HTMLElement)?.scrollTo?.({ top: 0 });
|
||||
this.dataSource.paginator.reload();
|
||||
this.refreshTable();
|
||||
}
|
||||
|
||||
private updateSort() {
|
||||
this.sortComponent.active = this.sort.active;
|
||||
this.sortComponent.direction = this.sort.direction;
|
||||
this.tryFrontSort();
|
||||
// TODO: Refresh table when pagination is updated
|
||||
private refreshTable() {
|
||||
// eslint-disable-next-line no-self-assign
|
||||
this.dataSource.data = this.dataSource.data;
|
||||
}
|
||||
|
||||
private updateDisplayedColumns() {
|
||||
this.displayedColumns = [
|
||||
...((
|
||||
Array.isArray(this.rowDragDrop)
|
||||
? (this.sortComponent?.direction ?? this.sort?.direction) &&
|
||||
this.rowDragDrop.includes(this.sortComponent?.active ?? this.sort?.active)
|
||||
: this.rowDragDrop
|
||||
)
|
||||
? [this.dragColumnDef]
|
||||
: []),
|
||||
this.scoreColumnDef,
|
||||
...(this.rowSelectable ? [this.selectColumnDef] : []),
|
||||
...Array.from(this.columnsObjects.values())
|
||||
.filter((c) => !c.hide)
|
||||
.map((c) => c.field),
|
||||
];
|
||||
private updateLoadedLazyItems(items: DisplayedData<T, C>) {
|
||||
const lazyLoadedItems = items.slice(0, DEFAULT_LOADED_LAZY_ROWS_COUNT);
|
||||
for (const item of lazyLoadedItems) {
|
||||
this.loadedLazyItems.set(item, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +1,11 @@
|
||||
import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MtxTooltip } from '@ng-matero/extensions/tooltip';
|
||||
|
||||
import { PipesModule } from '../../pipes';
|
||||
import { ActionsModule } from '../actions';
|
||||
import { InputFieldModule } from '../input-field';
|
||||
import { SwitchButtonModule } from '../switch-button';
|
||||
import { TagModule } from '../tag';
|
||||
|
||||
import { ScoreColumnComponent } from './components/score-column.component';
|
||||
import { SelectColumnComponent } from './components/select-column.component';
|
||||
import { ShowMoreButtonComponent } from './components/show-more-button/show-more-button.component';
|
||||
import { TableActionsComponent } from './components/table-actions.component';
|
||||
import { TableCellComponent } from './components/table-cell/table-cell.component';
|
||||
import { TableInputsComponent } from './components/table-inputs.component';
|
||||
import { Table2Component } from './components/table2';
|
||||
import { TableComponent } from './table.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatTableModule,
|
||||
MatCardModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
MatButtonModule,
|
||||
ActionsModule,
|
||||
MatTooltipModule,
|
||||
MatChipsModule,
|
||||
PipesModule,
|
||||
TagModule,
|
||||
RouterLink,
|
||||
MatProgressBarModule,
|
||||
MatCheckboxModule,
|
||||
MatSortModule,
|
||||
InputFieldModule,
|
||||
ReactiveFormsModule,
|
||||
SwitchButtonModule,
|
||||
CdkDrag,
|
||||
CdkDropList,
|
||||
SelectColumnComponent,
|
||||
ScoreColumnComponent,
|
||||
MtxTooltip,
|
||||
Table2Component,
|
||||
ShowMoreButtonComponent,
|
||||
TableInputsComponent,
|
||||
],
|
||||
declarations: [TableComponent, TableActionsComponent, TableCellComponent],
|
||||
exports: [TableComponent, TableActionsComponent, TableInputsComponent, Table2Component],
|
||||
imports: [TableActionsComponent, TableComponent, TableInputsComponent],
|
||||
exports: [TableActionsComponent, TableInputsComponent, TableComponent],
|
||||
})
|
||||
export class TableModule {}
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { TemplateRef } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { SelectFn } from '../../../utils';
|
||||
|
||||
import { ColumnObject } from './column';
|
||||
|
||||
export type ColumnPinValue = 'left' | 'right' | null;
|
||||
|
||||
export type FormatterFn<TObject extends object, TResult = unknown> = SelectFn<
|
||||
TObject,
|
||||
TResult,
|
||||
[index: number, colDef: ColumnObject<TObject>]
|
||||
>;
|
||||
export type ColumnFn<TObject extends object, TResult = unknown> = (
|
||||
rowData: TObject,
|
||||
index: number,
|
||||
) => TResult;
|
||||
|
||||
export interface BaseColumn<T extends object> {
|
||||
field: string;
|
||||
header?: string | Observable<string>;
|
||||
hide?: boolean;
|
||||
pinned?: ColumnPinValue;
|
||||
width?: string;
|
||||
minWidth?: string;
|
||||
maxWidth?: string;
|
||||
sortable?: boolean | string;
|
||||
cellTemplate?: TemplateRef<unknown>;
|
||||
formatter?: FormatterFn<T>;
|
||||
|
||||
description?: FormatterFn<T>;
|
||||
|
||||
tooltip?: FormatterFn<T>;
|
||||
|
||||
link?: ColumnFn<T, string>;
|
||||
linkParameters?: { target?: '_blank' };
|
||||
|
||||
click?: ColumnFn<T, void>;
|
||||
|
||||
lazy?: boolean;
|
||||
|
||||
// TODO: Need to delete
|
||||
type?: void;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { BaseColumn } from './base-column';
|
||||
import { TypedColumns } from './typed-column';
|
||||
|
||||
export type ColumnObject<T extends object> = BaseColumn<T> | TypedColumns<T>;
|
||||
export type Column<T extends object> = ColumnObject<T> | string;
|
@ -19,7 +19,7 @@ interface ColumnParams {
|
||||
|
||||
export type CellValue = 'string' | Value;
|
||||
|
||||
export interface Column2<T extends object, C extends object = object> extends ColumnParams {
|
||||
export interface Column<T extends object, C extends object = object> extends ColumnParams {
|
||||
field: string;
|
||||
|
||||
header?: PossiblyAsync<Partial<Value> | string>;
|
||||
@ -38,7 +38,7 @@ export function normalizePossiblyFn<R, P extends Array<unknown>>(fn: PossiblyFn<
|
||||
|
||||
export function normalizeCell<T extends object, C extends object>(
|
||||
field: string,
|
||||
cell: Column2<T, C>['cell'],
|
||||
cell: Column<T, C>['cell'],
|
||||
hasChild: boolean = false,
|
||||
): Fn<Observable<Value>, CellFnArgs<T>> {
|
||||
const cellFn = normalizePossiblyFn(cell);
|
||||
@ -65,7 +65,7 @@ export class NormColumn<T extends object, C extends object = object> {
|
||||
params!: ColumnParams;
|
||||
|
||||
constructor(
|
||||
{ field, header, cell, child, hidden, sort, lazyCell, ...params }: Column2<T, C>,
|
||||
{ field, header, cell, child, hidden, sort, lazyCell, ...params }: Column<T, C>,
|
||||
commonParams: ColumnParams = {},
|
||||
) {
|
||||
this.field = field ?? (typeof header === 'string' ? header : Math.random());
|
||||
|
@ -1,6 +1,3 @@
|
||||
export * from './base-column';
|
||||
export * from './typed-column';
|
||||
export * from './column';
|
||||
export * from './column2';
|
||||
export * from './update-options';
|
||||
export * from './drag-drop';
|
||||
|
@ -1,44 +0,0 @@
|
||||
import { OmitByValueExact } from 'utility-types';
|
||||
|
||||
import { Color } from '../../../styles';
|
||||
|
||||
import { BaseColumn, ColumnFn, FormatterFn } from './base-column';
|
||||
|
||||
export type TypedColumn<
|
||||
T extends object,
|
||||
TType extends string,
|
||||
TTypeParameters extends object = never,
|
||||
> = Omit<BaseColumn<T>, 'type'> &
|
||||
OmitByValueExact<{ type: TType; typeParameters: TTypeParameters }, never>;
|
||||
|
||||
export type MenuColumn<T extends object> = TypedColumn<
|
||||
T,
|
||||
'menu',
|
||||
{
|
||||
items: {
|
||||
label: string | ColumnFn<T, string>;
|
||||
click: ColumnFn<T, void>;
|
||||
disabled?: ColumnFn<T, boolean>;
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
export type TagColumn<T extends object, TTag extends PropertyKey = PropertyKey> = TypedColumn<
|
||||
T,
|
||||
'tag',
|
||||
{
|
||||
label?: FormatterFn<T>;
|
||||
tags: Record<TTag, { label?: string; color?: Color }>;
|
||||
}
|
||||
>;
|
||||
export type CurrencyColumn<T extends object> = TypedColumn<
|
||||
T,
|
||||
'currency',
|
||||
{ currencyCode: FormatterFn<T>; isMinor?: boolean; exponent?: FormatterFn<T> }
|
||||
>;
|
||||
|
||||
export type TypedColumns<T extends object> =
|
||||
| TypedColumn<T, 'datetime'>
|
||||
| CurrencyColumn<T>
|
||||
| TagColumn<T>
|
||||
| MenuColumn<T>
|
||||
| TypedColumn<T, 'boolean'>;
|
@ -4,18 +4,18 @@ import { map } from 'rxjs/operators';
|
||||
|
||||
import { PossiblyAsync, getPossiblyAsyncObservable } from '../../../utils';
|
||||
import { Value } from '../../value';
|
||||
import { Column2, CellFnArgs, normalizeCell } from '../types';
|
||||
import { Column, CellFnArgs, normalizeCell } from '../types';
|
||||
|
||||
import { createUniqueColumnDef } from './create-unique-column-def';
|
||||
|
||||
export function createColumn<P, A extends object>(
|
||||
createCell: (cellParams: P, ...args: CellFnArgs<A>) => PossiblyAsync<Value>,
|
||||
columnObject: Partial<Column2<A>> = {},
|
||||
columnObject: Partial<Column<A>> = {},
|
||||
) {
|
||||
return <T extends A>(
|
||||
getCellParams: (...args: CellFnArgs<T>) => PossiblyAsync<P>,
|
||||
{ isLazyCell, ...column }: Partial<Column2<T>> & { isLazyCell?: boolean } = {},
|
||||
): Column2<T> => {
|
||||
{ isLazyCell, ...column }: Partial<Column<T>> & { isLazyCell?: boolean } = {},
|
||||
): Column<T> => {
|
||||
const injector = inject(Injector);
|
||||
const field = column?.field ?? createUniqueColumnDef(column?.header);
|
||||
const cellKey = isLazyCell ? 'lazyCell' : 'cell';
|
||||
|
@ -1,17 +0,0 @@
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import isObject from 'lodash-es/isObject';
|
||||
import startCase from 'lodash-es/startCase';
|
||||
|
||||
import { Column, ColumnObject } from '../types';
|
||||
|
||||
export function createColumnObject<T extends object>(col: Column<T>): ColumnObject<T> {
|
||||
const extCol: ColumnObject<T> = isObject(col) ? col : { field: col };
|
||||
if (isNil(extCol.header)) {
|
||||
extCol.header = startCase(String(extCol.field.split('.').at(-1)));
|
||||
}
|
||||
return extCol;
|
||||
}
|
||||
|
||||
export function createColumnsObjects<T extends object>(columns: Column<T>[]): ColumnObject<T>[] {
|
||||
return columns?.map((col) => createColumnObject(col)) || [];
|
||||
}
|
@ -3,12 +3,12 @@ import { Sort } from '@angular/material/sort';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { compareDifferentTypes } from '../../../../../utils';
|
||||
import { valueToString } from '../../../../value/utils/value-to-string';
|
||||
import { NormColumn } from '../../../types';
|
||||
import { normalizeString } from '../../../utils/normalize-string';
|
||||
import { compareDifferentTypes } from '../../../utils';
|
||||
import { valueToString } from '../../value/utils/value-to-string';
|
||||
import { NormColumn } from '../types';
|
||||
import { DisplayedData, DisplayedDataItem, ColumnData } from '../utils/to-columns-data';
|
||||
|
||||
import { DisplayedData, DisplayedDataItem, ColumnData } from './to-columns-data';
|
||||
import { normalizeString } from './normalize-string';
|
||||
|
||||
export type FilterSearchData<T extends object, C extends object> = Map<
|
||||
DisplayedDataItem<T, C>,
|
@ -1,50 +0,0 @@
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { PageEvent, MatPaginator } from '@angular/material/paginator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export class OnePageTableDataSourcePaginator implements Partial<MatPaginator> {
|
||||
pageIndex = 0;
|
||||
pageSize = 0;
|
||||
page = new EventEmitter<PageEvent>();
|
||||
initialized = new BehaviorSubject(undefined);
|
||||
|
||||
get length() {
|
||||
return this.pageSize;
|
||||
}
|
||||
set length(v: number) {}
|
||||
|
||||
get displayedPages() {
|
||||
return this.pageSize / this.partSize;
|
||||
}
|
||||
|
||||
private partSize!: number;
|
||||
|
||||
constructor(partSize?: number) {
|
||||
this.setSize(partSize);
|
||||
}
|
||||
|
||||
setSize(partSize = 25) {
|
||||
if (partSize !== this.partSize) {
|
||||
this.pageSize = this.partSize = partSize;
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.pageSize = this.partSize;
|
||||
this.update();
|
||||
}
|
||||
|
||||
more() {
|
||||
this.pageSize += this.partSize;
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.page.next({
|
||||
pageIndex: this.pageIndex,
|
||||
pageSize: this.pageSize,
|
||||
length: this.length,
|
||||
});
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import {
|
||||
TreeData,
|
||||
TreeInlineDataItem,
|
||||
treeDataItemToInlineDataItem,
|
||||
} from '../components/table2/tree-data';
|
||||
} from '../tree-data';
|
||||
|
||||
import { cachedHeadMap } from './cached-head-map';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Value } from '../../value';
|
||||
import { unknownToString } from '../../value/utils/unknown-to-string';
|
||||
import { valueToString } from '../../value/utils/value-to-string';
|
||||
import { DisplayedDataItem, ColumnData } from '../components/table2/utils/to-columns-data';
|
||||
import { DisplayedDataItem, ColumnData } from '../utils/to-columns-data';
|
||||
|
||||
export function tableToCsvObject<T extends object, C extends object>(
|
||||
cols: Value[],
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Observable, scan, of, switchMap, combineLatest, timer } from 'rxjs';
|
||||
import { shareReplay, map } from 'rxjs/operators';
|
||||
import { shareReplay, map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { Value } from '../../../../value';
|
||||
import { CellFnArgs, Fn, NormColumn } from '../../../types';
|
||||
import { Value } from '../../value';
|
||||
import { TreeInlineDataItem, TreeInlineData } from '../tree-data';
|
||||
import { CellFnArgs, Fn, NormColumn } from '../types';
|
||||
|
||||
export type DisplayedDataItem<T extends object, C extends object> = TreeInlineDataItem<T, C> | T;
|
||||
export type DisplayedData<T extends object, C extends object> = TreeInlineData<T, C> | T[];
|
||||
@ -119,11 +120,15 @@ export function toColumnsData<T extends object, C extends object>(
|
||||
Array.from(columnsData.values()).map((v) =>
|
||||
combineLatest(
|
||||
Array.from(v.values()).map((cell) =>
|
||||
timer(0).pipe(switchMap(() => cell.value)),
|
||||
timer(0).pipe(
|
||||
switchMap(() => cell.value),
|
||||
distinctUntilChanged(isEqual),
|
||||
),
|
||||
),
|
||||
),
|
||||
).pipe(debounceTime(0)),
|
||||
),
|
||||
).pipe(
|
||||
debounceTime(0),
|
||||
map(
|
||||
(res) =>
|
||||
new Map(
|
Loading…
Reference in New Issue
Block a user