diff --git a/projects/ng-core/src/lib/components/table/_table-theme.scss b/projects/ng-core/src/lib/components/table/_table-theme.scss index d0a5dfd..9ee3c05 100644 --- a/projects/ng-core/src/lib/components/table/_table-theme.scss +++ b/projects/ng-core/src/lib/components/table/_table-theme.scss @@ -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, diff --git a/projects/ng-core/src/lib/components/table/components/score-column.component.ts b/projects/ng-core/src/lib/components/table/components/score-column.component.ts deleted file mode 100644 index f1f6a62..0000000 --- a/projects/ng-core/src/lib/components/table/components/score-column.component.ts +++ /dev/null @@ -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: ` - - - Score - - - {{ scores().get(row)?.score | number: '1.2-2' : 'en' }} - - - `, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [CommonModule, MatTableModule, MatSortHeader], -}) -export class ScoreColumnComponent extends BaseColumnComponent { - scores = input>(new Map()); -} diff --git a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.html b/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.html deleted file mode 100644 index 807dd0b..0000000 --- a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.scss b/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.scss deleted file mode 100644 index 077fcd7..0000000 --- a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.show-more { - width: 100%; -} diff --git a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.ts b/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.ts deleted file mode 100644 index 9d63e74..0000000 --- a/projects/ng-core/src/lib/components/table/components/show-more-button/show-more-button.component.ts +++ /dev/null @@ -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(); - - get shown() { - return Math.min(this.length, this.displayedPages * this.pageSize); - } -} diff --git a/projects/ng-core/src/lib/components/table/components/table-actions.component.ts b/projects/ng-core/src/lib/components/table/components/table-actions.component.ts index 960a4ed..404ab9f 100644 --- a/projects/ng-core/src/lib/components/table/components/table-actions.component.ts +++ b/projects/ng-core/src/lib/components/table/components/table-actions.component.ts @@ -1,12 +1,16 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ActionsModule } from '../../actions'; + @Component({ selector: 'v-table-actions', + standalone: true, template: ` `, + imports: [ActionsModule], changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableActionsComponent {} diff --git a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.html b/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.html deleted file mode 100644 index 4c9a22a..0000000 --- a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.html +++ /dev/null @@ -1,121 +0,0 @@ -@if (colDef.lazy && !lazyVisible && !preloadLazy) { - -} - - - -
- @if (rowData | vSelect: colDef.tooltip : '' : [colDef]; as tooltip) { -
- -
- - -
{{ tooltip | json }}
-
- } @else { - @if (colDef.link?.(rowData, index); as link) { - - - - - } @else { - - - } - } -
- - - -
- @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 - ) { - - {{ tagLabel }} - - } - } - } - @if (colDef.type === 'boolean') { - @if (value === true || value === false) { - - {{ value ? 'Yes' : 'No' }} - - } - } - @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') { - - - @for (item of colDef.typeParameters.items; track item; let index = $index) { - - } - - } - @if (!colDef.type) { - {{ value }} - } -
-
-
- -
- {{ description }} -
-
-
diff --git a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.scss b/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.scss deleted file mode 100644 index c67de18..0000000 --- a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.scss +++ /dev/null @@ -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; -} diff --git a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.ts b/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.ts deleted file mode 100644 index 45597c7..0000000 --- a/projects/ng-core/src/lib/components/table/components/table-cell/table-cell.component.ts +++ /dev/null @@ -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 { - @HostBinding('class.v-table-cell') hostClass: boolean = true; - - @Input() rowData!: T; - @Input() colDef!: ColumnObject; - @Input() index!: number; - - @Input({ transform: booleanAttribute }) preloadLazy = false; - - @Output() preloadedLazyChange = new EventEmitter(); - - lazyVisible = false; - - getLabel(label: string | ColumnFn, index: number) { - return typeof label === 'string' ? label : label(this.rowData, index); - } -} diff --git a/projects/ng-core/src/lib/components/table/components/table-info-bar/table-info-bar.component.html b/projects/ng-core/src/lib/components/table/components/table-info-bar/table-info-bar.component.html index 2088b10..666cad2 100644 --- a/projects/ng-core/src/lib/components/table/components/table-info-bar/table-info-bar.component.html +++ b/projects/ng-core/src/lib/components/table/components/table-info-bar/table-info-bar.component.html @@ -26,28 +26,36 @@
- - + @if (hasLoad()) { + + } + @if (hasLoad() || hasMore()) { + + } @if (!noDownload()) {
diff --git a/projects/ng-core/src/lib/components/table/components/table2/table2.component.scss b/projects/ng-core/src/lib/components/table/components/table2/table2.component.scss deleted file mode 100644 index f93ad0b..0000000 --- a/projects/ng-core/src/lib/components/table/components/table2/table2.component.scss +++ /dev/null @@ -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; -} diff --git a/projects/ng-core/src/lib/components/table/components/table2/table2.component.ts b/projects/ng-core/src/lib/components/table/components/table2/table2.component.ts deleted file mode 100644 index a9eacad..0000000 --- a/projects/ng-core/src/lib/components/table/components/table2/table2.component.ts +++ /dev/null @@ -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 implements OnInit { - data = input(); - treeData = input>(); - columns = input[], ArrayAttributeTransform>>([], { - 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 | 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>([]); - - // Sort - sort = model(DEFAULT_SORT); - @ViewChild(MatSort) sortComponent!: MatSort; - - update = output(); - more = output(); - - loadedLazyItems = new WeakMap, boolean>(); - - dataSource = new TableDataSource(); - normColumns = computed[]>(() => 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; - @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 { - 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) { - 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) { - const lazyLoadedItems = items.slice(0, DEFAULT_LOADED_LAZY_ROWS_COUNT); - for (const item of lazyLoadedItems) { - this.loadedLazyItems.set(item, true); - } - } -} diff --git a/projects/ng-core/src/lib/components/table/consts.ts b/projects/ng-core/src/lib/components/table/consts.ts index 1e0f459..a57ddd4 100644 --- a/projects/ng-core/src/lib/components/table/consts.ts +++ b/projects/ng-core/src/lib/components/table/consts.ts @@ -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: (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'), +}; diff --git a/projects/ng-core/src/lib/components/table/index.ts b/projects/ng-core/src/lib/components/table/index.ts index d3734dc..7eb8c90 100644 --- a/projects/ng-core/src/lib/components/table/index.ts +++ b/projects/ng-core/src/lib/components/table/index.ts @@ -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'; diff --git a/projects/ng-core/src/lib/components/table/presets/create-menu-column.ts b/projects/ng-core/src/lib/components/table/presets/create-menu-column.ts new file mode 100644 index 0000000..9249d55 --- /dev/null +++ b/projects/ng-core/src/lib/components/table/presets/create-menu-column.ts @@ -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', + }, + }, +); diff --git a/projects/ng-core/src/lib/components/table/presets/create-operation-column.ts b/projects/ng-core/src/lib/components/table/presets/create-operation-column.ts deleted file mode 100644 index b4f4d0e..0000000 --- a/projects/ng-core/src/lib/components/table/presets/create-operation-column.ts +++ /dev/null @@ -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( - items: MenuColumn['typeParameters']['items'], - other?: Partial, 'items'>>, -): MenuColumn { - 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', - }, - }, -); diff --git a/projects/ng-core/src/lib/components/table/presets/index.ts b/projects/ng-core/src/lib/components/table/presets/index.ts index 272ceed..b52b1f7 100644 --- a/projects/ng-core/src/lib/components/table/presets/index.ts +++ b/projects/ng-core/src/lib/components/table/presets/index.ts @@ -1 +1 @@ -export * from './create-operation-column'; +export * from './create-menu-column'; diff --git a/projects/ng-core/src/lib/components/table/table.component.html b/projects/ng-core/src/lib/components/table/table.component.html index 546bd0f..c753e87 100644 --- a/projects/ng-core/src/lib/components/table/table.component.html +++ b/projects/ng-core/src/lib/components/table/table.component.html @@ -1,301 +1,128 @@ -
- @if (!noFilter && (standaloneFilter || noActions)) { -
- - - {{ externalFilter() ? 'Search' : 'Filter' }} - -
- - -
-
- @if (noActions && update.observed) { -
- -
- } -
- } - @if (!noActions) { - - -
- - @if (!noFilter && !standaloneFilter) { -
- -
- } -
- @if (!noFilter && !standaloneFilter) { -
- - @if (filterControl.value) { - - } -
- } -
-
- -
-
- } -
-
-
- Quantity: - {{ - progress - ? '...' - : data()?.length - ? data().length + (hasMore ? ' (more available)' : ' (all)') - : '0' - }} - @if (filterControl.value && !externalFilter()) { - | Filtered: {{ filteredDataLength ?? '...' }} - } - @if (selected?.length) { - | - Selected: {{ selected.length }} - } -
-
- - @if (progress || (!externalFilter() && (filterProgress$ | async))) { - - } - - - - - - - @if (rowSelectable) { - - } - @for (col of columnsObjects.values(); track col) { - - @if (col.sortable) { - - } @else { - - } - + + +
-
- reorder - -
-
- {{ col.header }} - - {{ col.header }} - + + + + + + + + + + - - } - - - - - - + + @if (rowSelectable()) { + + } + @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, + }; + + + + + @let cell = columnsData?.get?.(element)?.get(col); + + + + } - -
+
+ reorder -
-
- @if (!col.cellTemplate && !cellTemplate[col.field]) { - - } @else { - - } -
-
-
{{ index + 1 }}--> + + - {{ - progress - ? 'Loading...' - : !externalFilter() && (filterProgress$ | async) - ? 'Filtering...' - : filteredDataLength === 0 - ? 'No records found' - : 'No records' - }} + + + + +
-
- - @if (hasShowMore) { - - } +
+
- - - - - - - - @if (!externalFilter()) { - - } - diff --git a/projects/ng-core/src/lib/components/table/table.component.scss b/projects/ng-core/src/lib/components/table/table.component.scss index 2d949cc..b620fe4 100644 --- a/projects/ng-core/src/lib/components/table/table.component.scss +++ b/projects/ng-core/src/lib/components/table/table.component.scss @@ -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; } } diff --git a/projects/ng-core/src/lib/components/table/table.component.ts b/projects/ng-core/src/lib/components/table/table.component.ts index f372c99..881d63e 100644 --- a/projects/ng-core/src/lib/components/table/table.component.ts +++ b/projects/ng-core/src/lib/components/table/table.component.ts @@ -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 - implements OnInit, Progressable, OnChanges, AfterViewInit, OnDestroy -{ - data = input([]); - - @Input() columns!: Column[]; - @Input() cellTemplate: Record['field'], ColumnObject['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(); - @Output() more = new EventEmitter(); - - // 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(); - @Input({ transform: booleanAttribute }) sortOnFront: boolean = false; - @ViewChild(MatSort) sortComponent!: MatSort; - - // Select - @Input({ transform: booleanAttribute }) rowSelectable: boolean = false; - @Input() rowSelected!: T[]; - @Output() rowSelectedChange = new EventEmitter(); - selectColumnDef = createUniqueColumnDef('select'); - selected: T[] = []; +export class TableComponent implements OnInit { + data = input(); + treeData = input>(); + columns = input[], ArrayAttributeTransform>>([], { + 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(); - filterControl = new FormControl(''); - exactFilterControl = new FormControl(1); - scores = new Map(); - filteredDataLength?: number; - filterProgress$ = new BehaviorSubject(false); + filteredSortData$ = new BehaviorSubject | 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>([]); + + // Sort + sort = model(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>(); + }); + rowDropped = output>>(); dragDisabled = true; - columnsObjects = new Map['field'], ColumnObject>([]); + update$ = new Subject(); + update = outputFromObservable(this.update$); + more = output(); - isPreload = false; + loadedLazyItems = new WeakMap, boolean>(); - dataSource = new MatTableDataSource(); + dataSource = new TableDataSource(); + normColumns = computed[]>(() => 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(); - - @ViewChild('table', { static: true }) table!: MatTable; - - 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; + @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, 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, - ) - : 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>) { - 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[] = 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) { 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 = 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 { + 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) { + 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) { + const lazyLoadedItems = items.slice(0, DEFAULT_LOADED_LAZY_ROWS_COUNT); + for (const item of lazyLoadedItems) { + this.loadedLazyItems.set(item, true); + } } } diff --git a/projects/ng-core/src/lib/components/table/table.module.ts b/projects/ng-core/src/lib/components/table/table.module.ts index 5e01777..62e5410 100644 --- a/projects/ng-core/src/lib/components/table/table.module.ts +++ b/projects/ng-core/src/lib/components/table/table.module.ts @@ -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 {} diff --git a/projects/ng-core/src/lib/components/table/components/table2/tree-data/index.ts b/projects/ng-core/src/lib/components/table/tree-data/index.ts similarity index 100% rename from projects/ng-core/src/lib/components/table/components/table2/tree-data/index.ts rename to projects/ng-core/src/lib/components/table/tree-data/index.ts diff --git a/projects/ng-core/src/lib/components/table/components/table2/tree-data/tree-data-item-to-inline-data-item.ts b/projects/ng-core/src/lib/components/table/tree-data/tree-data-item-to-inline-data-item.ts similarity index 100% rename from projects/ng-core/src/lib/components/table/components/table2/tree-data/tree-data-item-to-inline-data-item.ts rename to projects/ng-core/src/lib/components/table/tree-data/tree-data-item-to-inline-data-item.ts diff --git a/projects/ng-core/src/lib/components/table/components/table2/tree-data/tree-data.ts b/projects/ng-core/src/lib/components/table/tree-data/tree-data.ts similarity index 100% rename from projects/ng-core/src/lib/components/table/components/table2/tree-data/tree-data.ts rename to projects/ng-core/src/lib/components/table/tree-data/tree-data.ts diff --git a/projects/ng-core/src/lib/components/table/types/base-column.ts b/projects/ng-core/src/lib/components/table/types/base-column.ts deleted file mode 100644 index a541502..0000000 --- a/projects/ng-core/src/lib/components/table/types/base-column.ts +++ /dev/null @@ -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 = SelectFn< - TObject, - TResult, - [index: number, colDef: ColumnObject] ->; -export type ColumnFn = ( - rowData: TObject, - index: number, -) => TResult; - -export interface BaseColumn { - field: string; - header?: string | Observable; - hide?: boolean; - pinned?: ColumnPinValue; - width?: string; - minWidth?: string; - maxWidth?: string; - sortable?: boolean | string; - cellTemplate?: TemplateRef; - formatter?: FormatterFn; - - description?: FormatterFn; - - tooltip?: FormatterFn; - - link?: ColumnFn; - linkParameters?: { target?: '_blank' }; - - click?: ColumnFn; - - lazy?: boolean; - - // TODO: Need to delete - type?: void; -} diff --git a/projects/ng-core/src/lib/components/table/types/column.ts b/projects/ng-core/src/lib/components/table/types/column.ts deleted file mode 100644 index a9fc801..0000000 --- a/projects/ng-core/src/lib/components/table/types/column.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseColumn } from './base-column'; -import { TypedColumns } from './typed-column'; - -export type ColumnObject = BaseColumn | TypedColumns; -export type Column = ColumnObject | string; diff --git a/projects/ng-core/src/lib/components/table/types/column2.ts b/projects/ng-core/src/lib/components/table/types/column2.ts index f378607..438cf19 100644 --- a/projects/ng-core/src/lib/components/table/types/column2.ts +++ b/projects/ng-core/src/lib/components/table/types/column2.ts @@ -19,7 +19,7 @@ interface ColumnParams { export type CellValue = 'string' | Value; -export interface Column2 extends ColumnParams { +export interface Column extends ColumnParams { field: string; header?: PossiblyAsync | string>; @@ -38,7 +38,7 @@ export function normalizePossiblyFn>(fn: PossiblyFn< export function normalizeCell( field: string, - cell: Column2['cell'], + cell: Column['cell'], hasChild: boolean = false, ): Fn, CellFnArgs> { const cellFn = normalizePossiblyFn(cell); @@ -65,7 +65,7 @@ export class NormColumn { params!: ColumnParams; constructor( - { field, header, cell, child, hidden, sort, lazyCell, ...params }: Column2, + { field, header, cell, child, hidden, sort, lazyCell, ...params }: Column, commonParams: ColumnParams = {}, ) { this.field = field ?? (typeof header === 'string' ? header : Math.random()); diff --git a/projects/ng-core/src/lib/components/table/types/index.ts b/projects/ng-core/src/lib/components/table/types/index.ts index 3b90966..bf6f3db 100644 --- a/projects/ng-core/src/lib/components/table/types/index.ts +++ b/projects/ng-core/src/lib/components/table/types/index.ts @@ -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'; diff --git a/projects/ng-core/src/lib/components/table/types/typed-column.ts b/projects/ng-core/src/lib/components/table/types/typed-column.ts deleted file mode 100644 index c130d61..0000000 --- a/projects/ng-core/src/lib/components/table/types/typed-column.ts +++ /dev/null @@ -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, 'type'> & - OmitByValueExact<{ type: TType; typeParameters: TTypeParameters }, never>; - -export type MenuColumn = TypedColumn< - T, - 'menu', - { - items: { - label: string | ColumnFn; - click: ColumnFn; - disabled?: ColumnFn; - }[]; - } ->; -export type TagColumn = TypedColumn< - T, - 'tag', - { - label?: FormatterFn; - tags: Record; - } ->; -export type CurrencyColumn = TypedColumn< - T, - 'currency', - { currencyCode: FormatterFn; isMinor?: boolean; exponent?: FormatterFn } ->; - -export type TypedColumns = - | TypedColumn - | CurrencyColumn - | TagColumn - | MenuColumn - | TypedColumn; diff --git a/projects/ng-core/src/lib/components/table/utils/create-column.ts b/projects/ng-core/src/lib/components/table/utils/create-column.ts index 125a5d8..05c1a9b 100644 --- a/projects/ng-core/src/lib/components/table/utils/create-column.ts +++ b/projects/ng-core/src/lib/components/table/utils/create-column.ts @@ -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( createCell: (cellParams: P, ...args: CellFnArgs) => PossiblyAsync, - columnObject: Partial> = {}, + columnObject: Partial> = {}, ) { return ( getCellParams: (...args: CellFnArgs) => PossiblyAsync

, - { isLazyCell, ...column }: Partial> & { isLazyCell?: boolean } = {}, - ): Column2 => { + { isLazyCell, ...column }: Partial> & { isLazyCell?: boolean } = {}, + ): Column => { const injector = inject(Injector); const field = column?.field ?? createUniqueColumnDef(column?.header); const cellKey = isLazyCell ? 'lazyCell' : 'cell'; diff --git a/projects/ng-core/src/lib/components/table/utils/create-columns-objects.ts b/projects/ng-core/src/lib/components/table/utils/create-columns-objects.ts deleted file mode 100644 index 97c7ff4..0000000 --- a/projects/ng-core/src/lib/components/table/utils/create-columns-objects.ts +++ /dev/null @@ -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(col: Column): ColumnObject { - const extCol: ColumnObject = isObject(col) ? col : { field: col }; - if (isNil(extCol.header)) { - extCol.header = startCase(String(extCol.field.split('.').at(-1))); - } - return extCol; -} - -export function createColumnsObjects(columns: Column[]): ColumnObject[] { - return columns?.map((col) => createColumnObject(col)) || []; -} diff --git a/projects/ng-core/src/lib/components/table/components/table2/utils/filter-sort.ts b/projects/ng-core/src/lib/components/table/utils/filter-sort.ts similarity index 90% rename from projects/ng-core/src/lib/components/table/components/table2/utils/filter-sort.ts rename to projects/ng-core/src/lib/components/table/utils/filter-sort.ts index 23424d7..7d1df8c 100644 --- a/projects/ng-core/src/lib/components/table/components/table2/utils/filter-sort.ts +++ b/projects/ng-core/src/lib/components/table/utils/filter-sort.ts @@ -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 = Map< DisplayedDataItem, diff --git a/projects/ng-core/src/lib/components/table/utils/one-page-table-data-source-paginator.ts b/projects/ng-core/src/lib/components/table/utils/one-page-table-data-source-paginator.ts deleted file mode 100644 index ce81ec8..0000000 --- a/projects/ng-core/src/lib/components/table/utils/one-page-table-data-source-paginator.ts +++ /dev/null @@ -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 { - pageIndex = 0; - pageSize = 0; - page = new EventEmitter(); - 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, - }); - } -} diff --git a/projects/ng-core/src/lib/components/table/utils/table-data-source.ts b/projects/ng-core/src/lib/components/table/utils/table-data-source.ts index 48a1d40..cb9e558 100644 --- a/projects/ng-core/src/lib/components/table/utils/table-data-source.ts +++ b/projects/ng-core/src/lib/components/table/utils/table-data-source.ts @@ -10,7 +10,7 @@ import { TreeData, TreeInlineDataItem, treeDataItemToInlineDataItem, -} from '../components/table2/tree-data'; +} from '../tree-data'; import { cachedHeadMap } from './cached-head-map'; diff --git a/projects/ng-core/src/lib/components/table/utils/table-to-csv-object.ts b/projects/ng-core/src/lib/components/table/utils/table-to-csv-object.ts index 0285574..006e09b 100644 --- a/projects/ng-core/src/lib/components/table/utils/table-to-csv-object.ts +++ b/projects/ng-core/src/lib/components/table/utils/table-to-csv-object.ts @@ -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( cols: Value[], diff --git a/projects/ng-core/src/lib/components/table/components/table2/utils/to-columns-data.ts b/projects/ng-core/src/lib/components/table/utils/to-columns-data.ts similarity index 92% rename from projects/ng-core/src/lib/components/table/components/table2/utils/to-columns-data.ts rename to projects/ng-core/src/lib/components/table/utils/to-columns-data.ts index 2fd8fa2..4673ec4 100644 --- a/projects/ng-core/src/lib/components/table/components/table2/utils/to-columns-data.ts +++ b/projects/ng-core/src/lib/components/table/utils/to-columns-data.ts @@ -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 = TreeInlineDataItem | T; export type DisplayedData = TreeInlineData | T[]; @@ -119,11 +120,15 @@ export function toColumnsData( 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(