diff --git a/projects/components/table/public_api.ts b/projects/components/table/public_api.ts index 8dff423d..fff65f44 100644 --- a/projects/components/table/public_api.ts +++ b/projects/components/table/public_api.ts @@ -14,6 +14,7 @@ export { PsTableTopButtonSectionDirective, } from './src/directives/table.directives'; export { PsTableMemoryStateManager, PsTableStateManager, PsTableUrlStateManager } from './src/helper/state-manager'; +export { PsTableActionStoreBase, PsTableActionStore } from './src/helper/action-store'; export { IExtendedPsTableUpdateDataInfo, IPsTableAction, IPsTableSortDefinition, IPsTableUpdateDataInfo } from './src/models'; export { IPsTableSetting, PsTableSettingsService } from './src/services/table-settings.service'; export { PsTableComponent } from './src/table.component'; diff --git a/projects/components/table/src/data/table-data-source.ts b/projects/components/table/src/data/table-data-source.ts index 9baefa33..41117def 100644 --- a/projects/components/table/src/data/table-data-source.ts +++ b/projects/components/table/src/data/table-data-source.ts @@ -1,9 +1,12 @@ import { DataSource, SelectionModel } from '@angular/cdk/collections'; import { TrackByFunction } from '@angular/core'; -import { BehaviorSubject, NEVER, Observable, of, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, isObservable, NEVER, Observable, of, Subject, Subscription } from 'rxjs'; import { catchError, finalize, map, take, tap } from 'rxjs/operators'; +import { PsTableActionStore, PsTableActionStoreBase } from '../helper/action-store'; + import { _isNumberValue } from '../helper/table.helper'; -import { IExtendedPsTableUpdateDataInfo, IPsTableAction, IPsTableUpdateDataInfo, PsTableActionScope } from '../models'; +import { IExtendedPsTableUpdateDataInfo, IPsTableAction, IPsTableUpdateDataInfo, PsTableAction, PsTableActionScope } from '../models'; + /** * Corresponds to `Number.MAX_SAFE_INTEGER`. Moved out into a variable here due to * flaky browser support and the value not being defined in Closure's typings. @@ -13,7 +16,7 @@ const MAX_SAFE_INTEGER = 9007199254740991; export interface PsTableDataSourceOptions { loadTrigger$?: Observable; loadDataFn: (updateInfo: IExtendedPsTableUpdateDataInfo) => Observable>; - actions?: IPsTableAction[]; + actions?: IPsTableAction[] | Observable[]>; mode?: PsTableMode; moreMenuThreshold?: number; } @@ -86,18 +89,29 @@ export class PsTableDataSource extends DataSource { public readonly mode: PsTableMode; /** List of actions which can be executed for a single row */ - public readonly rowActions: IPsTableAction[]; + public get rowActions(): PsTableAction[] { + return this._rowActions || []; + } + private _rowActions: PsTableAction[]; /** List of actions which can be executed for a selection of rows */ - public readonly listActions: IPsTableAction[]; + public get listActions(): PsTableAction[] { + return this._listActions || []; + } + private _listActions: PsTableAction[]; public readonly moreMenuThreshold: number; + /** Stores table actions.*/ + private readonly _store: PsTableActionStoreBase = new PsTableActionStore(); + /** Stream that emits when a new data array is set on the data source. */ private readonly _updateDataTrigger$: Observable; private readonly _loadData: (updateInfo: IExtendedPsTableUpdateDataInfo) => Observable>; + private readonly _actions: IPsTableAction[] | Observable[]>; + /** Stream that emits when a new data array is set on the data source. */ private readonly _data: BehaviorSubject = new BehaviorSubject([]); @@ -109,6 +123,11 @@ export class PsTableDataSource extends DataSource { private _lastLoadTriggerData: TTrigger = null; + /** + * Subscription to the result of the _actions observable. + */ + private _loadActionsSubscription = Subscription.EMPTY; + /** * Subscription to the result of the loadData function. */ @@ -143,10 +162,7 @@ export class PsTableDataSource extends DataSource { 'loadDataFn' in optionsOrLoadDataFn ? optionsOrLoadDataFn : { loadDataFn: optionsOrLoadDataFn, actions: [], mode: mode }; this.mode = options.mode || 'client'; - // tslint:disable-next-line:no-bitwise - this.rowActions = options.actions?.filter((a) => a.scope & PsTableActionScope.row) || []; - // tslint:disable-next-line:no-bitwise - this.listActions = options.actions?.filter((a) => a.scope & PsTableActionScope.list) || []; + this._actions = options.actions; this._updateDataTrigger$ = options.loadTrigger$ || NEVER; this._loadData = options.loadDataFn; this.moreMenuThreshold = options.moreMenuThreshold ?? 3; @@ -298,6 +314,30 @@ export class PsTableDataSource extends DataSource { return data; } + public updateActions(): void { + if (!this._actions) { + return; + } + + const setActions = (actions: PsTableAction[]) => { + this._rowActions = actions?.filter((a) => a.scope & PsTableActionScope.row) || []; + this._listActions = actions?.filter((a) => a.scope & PsTableActionScope.list) || []; + }; + + if (Array.isArray(this._actions) && this._actions.length) { + setActions(this._actions.map((x) => this._store.get(x))); + } else if (isObservable(this._actions)) { + this._loadActionsSubscription?.unsubscribe(); + this._loadActionsSubscription = this._actions + .pipe( + take(1), + map((x) => x.map((y) => this._store.get(y))), + finalize(() => this._internalDetectChanges.next()) + ) + .subscribe((actions) => setActions(actions)); + } + } + /** * Reloads the data */ @@ -371,6 +411,7 @@ export class PsTableDataSource extends DataSource { this._lastLoadTriggerData = data; this.updateData(); }); + this.updateActions(); return this._renderData; } @@ -382,6 +423,7 @@ export class PsTableDataSource extends DataSource { this._renderChangesSubscription.unsubscribe(); this._updateDataTriggerSub.unsubscribe(); this._loadDataSubscription.unsubscribe(); + this._loadActionsSubscription.unsubscribe(); } /** diff --git a/projects/components/table/src/helper/action-store.spec.ts b/projects/components/table/src/helper/action-store.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/projects/components/table/src/helper/action-store.ts b/projects/components/table/src/helper/action-store.ts new file mode 100644 index 00000000..de70513d --- /dev/null +++ b/projects/components/table/src/helper/action-store.ts @@ -0,0 +1,28 @@ +import { IPsTableAction, PsTableAction } from '../../src/models'; + +export abstract class PsTableActionStoreBase { + abstract get(declaration: IPsTableAction): PsTableAction; +} + +export class PsTableActionStore extends PsTableActionStoreBase { + private readonly _store = new WeakMap, PsTableAction>(); + + /** + * Gets the existing PsTableAction object by the declaration (IPsTableAction) from the store or creates a new one. + * + * Because actions can have observables as children, we cannot create for each declaration the coresponding PsTableAction at the beginning. + * This forces us to create the PsTableAction dynamicly. But we do not want to execute the same observables multiple times, so we cache them. + * Otherwise the observable would be executed for each row. + * + * @param declaration Action declaration information + * @returns Existing or new PsTableAction + */ + public get(declaration: IPsTableAction): PsTableAction { + if (!this._store.has(declaration)) { + const action = new PsTableAction(declaration, this); + this._store.set(declaration, action); + } + + return this._store.get(declaration); + } +} diff --git a/projects/components/table/src/models.ts b/projects/components/table/src/models.ts index eceb8336..481206ca 100644 --- a/projects/components/table/src/models.ts +++ b/projects/components/table/src/models.ts @@ -1,3 +1,7 @@ +import { isObservable, Observable, of } from 'rxjs'; +import { map, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { PsTableActionStoreBase } from './helper/action-store'; + export interface IPsTableSortDefinition { prop: string; displayName: string; @@ -17,7 +21,7 @@ export interface IExtendedPsTableUpdateDataInfo extends IPsTableUpdate triggerData: TTrigger; } -export const enum PsTableActionScope { +export enum PsTableActionScope { row = 1, list = 2, // tslint:disable-next-line:no-bitwise @@ -27,10 +31,64 @@ export const enum PsTableActionScope { export interface IPsTableAction { label: string; icon: string; + isSvgIcon?: boolean; iconColor?: string; - children?: IPsTableAction[]; + children?: IPsTableAction[] | Observable[]>; scope: PsTableActionScope; isDisabledFn?: (items: T[]) => boolean; isHiddenFn?: (items: T[]) => boolean; actionFn?: (items: T[]) => void; + routerLink?: (item: T) => []; + routerLinkQueryParams?: (item: T) => { [key: string]: any }; +} + +export class PsTableAction { + public readonly label: string; + public readonly icon: string; + public readonly isSvgIcon: boolean; + public readonly iconColor?: string; + public readonly scope: PsTableActionScope; + public readonly isDisabledFn?: (items: T[]) => boolean; + public readonly isHiddenFn?: (items: T[]) => boolean; + public readonly actionFn?: (items: T[]) => void; + public readonly isObservable: boolean; + public readonly hasChildren: boolean; + public readonly routerLink?: (item: T) => string[]; + public readonly routerLinkQueryParams?: (item: T) => { [key: string]: any }; + public children$: Observable[]>; + + public get isLoading(): boolean { + return this._isLoading; + } + private _isLoading = false; + + constructor(declaration: IPsTableAction, store: PsTableActionStoreBase) { + this.label = declaration.label; + this.icon = declaration.icon; + this.isSvgIcon = declaration.isSvgIcon === true; + this.iconColor = declaration.iconColor; + this.scope = declaration.scope; + this.isDisabledFn = declaration.isDisabledFn; + this.isHiddenFn = declaration.isHiddenFn; + this.actionFn = declaration.actionFn; + this.routerLink = declaration.routerLink; + this.routerLinkQueryParams = declaration.routerLinkQueryParams; + + this.isObservable = isObservable(declaration.children); + this.hasChildren = Array.isArray(declaration.children) || this.isObservable; + + if (this.hasChildren) { + this.children$ = isObservable(declaration.children) + ? of(void 0).pipe( + tap(() => (this._isLoading = true)), + switchMap(() => declaration.children as Observable[]>), + tap(() => (this._isLoading = false)), + shareReplay({ bufferSize: 1, refCount: false }), + map((x) => x.map((y) => store.get(y) as PsTableAction) || []) + ) + : of(declaration.children.map((x) => store.get(x) as PsTableAction)); + } else { + this.children$ = of([]); + } + } } diff --git a/projects/components/table/src/pipes/table-actions-to-render.pipe.ts b/projects/components/table/src/pipes/table-actions-to-render.pipe.ts index 47d7044c..f85bdcf5 100644 --- a/projects/components/table/src/pipes/table-actions-to-render.pipe.ts +++ b/projects/components/table/src/pipes/table-actions-to-render.pipe.ts @@ -1,5 +1,6 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { IPsTableAction } from '../models'; + +import { PsTableAction } from '../models'; /** * Filters out hidden actions @@ -9,8 +10,8 @@ import { IPsTableAction } from '../models'; pure: true, }) export class PsTableActionsToRenderPipe implements PipeTransform { - transform(actions: IPsTableAction[], ...args: [T | T[]]): any { + transform(actions: PsTableAction[], ...args: [T | T[]]): any { const elements = Array.isArray(args[0]) ? args[0] : [args[0]]; - return actions.filter((a) => !a.isHiddenFn || !a.isHiddenFn(elements)); + return actions?.filter((a) => !a.isHiddenFn || !a.isHiddenFn(elements)) || []; } } diff --git a/projects/components/table/src/subcomponents/table-actions.component.html b/projects/components/table/src/subcomponents/table-actions.component.html index 540f2343..3941652b 100644 --- a/projects/components/table/src/subcomponents/table-actions.component.html +++ b/projects/components/table/src/subcomponents/table-actions.component.html @@ -5,21 +5,40 @@ - + + + + {{ action.label }} + + + + + - + @@ -34,3 +53,11 @@ + + + {{ action.icon }} + + + + + diff --git a/projects/components/table/src/subcomponents/table-actions.component.ts b/projects/components/table/src/subcomponents/table-actions.component.ts index abc36df0..47b41ad4 100644 --- a/projects/components/table/src/subcomponents/table-actions.component.ts +++ b/projects/components/table/src/subcomponents/table-actions.component.ts @@ -1,22 +1,51 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; import { MatMenu } from '@angular/material/menu'; import { IPsTableIntlTexts } from '@prosoft/components/core'; -import { IPsTableAction } from '../models'; +import { merge, Subscription } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { PsTableAction, PsTableActionScope } from '../models'; @Component({ selector: 'ps-table-actions', templateUrl: './table-actions.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PsTableActionsComponent { - @Input() public actions: IPsTableAction[]; +export class PsTableActionsComponent implements OnDestroy { + @Input() + public get actions(): PsTableAction[] { + return this._actions; + } + public set actions(value: PsTableAction[]) { + this._actions = value || []; + this._subscription?.unsubscribe(); + const observables = this.actions?.filter((x) => x.isObservable).map((x) => x.children$); + + if (observables) { + this._subscription = merge(observables) + .pipe(tap(() => this.cd.markForCheck())) + .subscribe(); + } + } + private _actions: PsTableAction[]; @Input() public items: unknown[]; @Input() public refreshable: boolean; @Input() public settingsEnabled: boolean; @Input() public intl: IPsTableIntlTexts; + @Input() public scope: PsTableActionScope; + + public psTableActionScopes = PsTableActionScope; @Output() public refreshData = new EventEmitter(); @Output() public showSettings = new EventEmitter(); @ViewChild('menu', { static: true }) menu: MatMenu; + + private _subscription: Subscription = Subscription.EMPTY; + + constructor(private cd: ChangeDetectorRef) {} + + public ngOnDestroy(): void { + this._subscription?.unsubscribe(); + } } diff --git a/projects/components/table/src/subcomponents/table-data.component.html b/projects/components/table/src/subcomponents/table-data.component.html index 18850548..4c96e8bb 100644 --- a/projects/components/table/src/subcomponents/table-data.component.html +++ b/projects/components/table/src/subcomponents/table-data.component.html @@ -5,7 +5,7 @@ - + @@ -13,7 +13,7 @@ - + @@ -57,6 +57,7 @@ [refreshable]="refreshable" [settingsEnabled]="settingsEnabled" [intl]="intl" + [scope]="psTableActionScopes.list" (refreshData)="onRefreshDataClicked()" (showSettings)="onShowSettingsClicked()" > diff --git a/projects/components/table/src/subcomponents/table-data.component.ts b/projects/components/table/src/subcomponents/table-data.component.ts index ed2c1a93..e44679a0 100644 --- a/projects/components/table/src/subcomponents/table-data.component.ts +++ b/projects/components/table/src/subcomponents/table-data.component.ts @@ -14,6 +14,7 @@ import { IPsTableIntlTexts } from '@prosoft/components/core'; import { PsTableDataSource } from '../data/table-data-source'; import { PsTableColumnDirective, PsTableRowDetailDirective } from '../directives/table.directives'; import { Subscription } from 'rxjs'; +import { PsTableActionScope } from '../models'; @Component({ selector: 'ps-table-data', @@ -42,6 +43,8 @@ export class PsTableDataComponent implements OnChanges { */ @Input() public listActions: TemplateRef | null = null; + public psTableActionScopes = PsTableActionScope; + @Output() public showSettingsClicked = new EventEmitter(); @Output() public refreshDataClicked = new EventEmitter(); diff --git a/projects/components/table/src/subcomponents/table-row-actions.component.html b/projects/components/table/src/subcomponents/table-row-actions.component.html index 7f9f136d..a3df9e3e 100644 --- a/projects/components/table/src/subcomponents/table-row-actions.component.html +++ b/projects/components/table/src/subcomponents/table-row-actions.component.html @@ -3,7 +3,7 @@ - + @@ -30,7 +30,12 @@ > {{ action.icon }} - + diff --git a/projects/components/table/src/subcomponents/table-row-actions.component.ts b/projects/components/table/src/subcomponents/table-row-actions.component.ts index c881d239..cd476105 100644 --- a/projects/components/table/src/subcomponents/table-row-actions.component.ts +++ b/projects/components/table/src/subcomponents/table-row-actions.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, TemplateRef } from '@angular/core'; -import { IPsTableAction } from '../models'; + +import { PsTableAction, PsTableActionScope } from '../models'; @Component({ selector: 'ps-table-row-actions', @@ -7,11 +8,13 @@ import { IPsTableAction } from '../models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PsTableRowActionsComponent implements OnChanges { - @Input() public actions: IPsTableAction[]; + @Input() public actions: PsTableAction[]; @Input() public actionsTemplate: TemplateRef | null = null; @Input() public moreMenuThreshold: number; @Input() public item: any; + public psTableActionScopes = PsTableActionScope; + public itemAsArray: any[]; public ngOnChanges(changes: SimpleChanges) { diff --git a/projects/components/table/src/table.component.spec.ts b/projects/components/table/src/table.component.spec.ts index 6bc23388..f0c62665 100644 --- a/projects/components/table/src/table.component.spec.ts +++ b/projects/components/table/src/table.component.spec.ts @@ -3,16 +3,21 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, Injectable, QueryList, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { IconType, MatIconHarness } from '@angular/material/icon/testing'; +import { MatMenuItemHarness } from '@angular/material/menu/testing'; +import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ActivatedRoute, convertToParamMap, ParamMap, Params, Router } from '@angular/router'; +import { ActivatedRoute, convertToParamMap, ParamMap, Params, RouterLinkWithHref } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { IPsTableIntlTexts, PsIntlService, PsIntlServiceEn } from '@prosoft/components/core'; import { filterAsync } from '@prosoft/components/utils/src/array'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; + import { PsTableDataSource } from './data/table-data-source'; import { PsTableColumnDirective } from './directives/table.directives'; import { PsTableMemoryStateManager } from './helper/state-manager'; -import { IPsTableSortDefinition, PsTableActionScope } from './models'; +import { IPsTableAction, IPsTableSortDefinition, PsTableActionScope } from './models'; import { IPsTableSetting, PsTableSettingsService } from './services/table-settings.service'; import { PsTablePaginationComponent } from './subcomponents/table-pagination.component'; import { PsTableComponent } from './table.component'; @@ -40,6 +45,7 @@ class TestSettingsService extends PsTableSettingsService { const router: any = { navigate: (_route: any, _options: any) => {}, + navigateByUrl: (_urltree: any, _extras: any) => {}, }; const queryParams$ = new BehaviorSubject(convertToParamMap({ other: 'value' })); @@ -103,20 +109,14 @@ function createColDef(data: { property?: string; header?: string; sortable?: boo -
- custom header -
+
custom header
custom settings {{ settings.pageSize }}
-
- custom button section -
+
custom button section
- + @@ -483,16 +483,12 @@ describe('PsTableComponent', () => { } beforeEach(async () => { - queryParams$.next(convertToParamMap({})); - await TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, CommonModule, PsTableModule], + imports: [NoopAnimationsModule, CommonModule, PsTableModule, RouterTestingModule], declarations: [TestComponent], providers: [ { provide: PsTableSettingsService, useClass: TestSettingsService }, { provide: PsIntlService, useClass: PsIntlServiceEn }, - { provide: ActivatedRoute, useValue: route }, - { provide: Router, useValue: router }, ], }); @@ -783,5 +779,181 @@ describe('PsTableComponent', () => { const firstRowSecondPage = (await table.getRows())[0]; expect(await (await firstRowSecondPage.getCells({ columnName: 'str' }))[0].getText()).toEqual('item 15'); }); + + describe('table actions', () => { + beforeEach(async () => { + await initTestComponent( + new PsTableDataSource({ + loadDataFn: () => + of([ + { id: 1, str: 'item 1' }, + { id: 2, str: 'item 2' }, + { id: 3, str: 'item 3' }, + ]), + mode: 'client', + actions: [ + { + label: 'custom action 1', + icon: 'list', + scope: PsTableActionScope.all, + actionFn: (selection) => component.onListActionExecute(selection), + }, + { + label: 'custom action 2', + icon: 'angular', + isSvgIcon: true, + scope: PsTableActionScope.all, + actionFn: (selection) => component.onListActionExecute(selection), + }, + { + label: 'custom list action 1', + icon: 'list', + scope: PsTableActionScope.list, + actionFn: () => {}, + }, + { + label: 'custom list action 2', + icon: 'angular', + isSvgIcon: true, + scope: PsTableActionScope.list, + actionFn: () => {}, + }, + { + label: 'custom row action 1', + icon: 'list', + scope: PsTableActionScope.row, + routerLink: (item: any) => [item.id], + routerLinkQueryParams: (item: any) => ({ + a: item.str.replace(' ', '_'), + }), + }, + { + label: 'custom row action 2', + icon: 'angular', + isSvgIcon: true, + scope: PsTableActionScope.row, + actionFn: () => {}, + }, + ] as IPsTableAction[], + }) + ); + + component.refreshable = false; + component.showSettings = false; + }); + + it('icon and svgIcon should be displayed correctly', async () => { + fixture.detectChanges(); + + // list actions + const listActionButtonHarness = await table.getListActionsButton(); + await listActionButtonHarness.open(); + const listActionHarnesses = await listActionButtonHarness.getItems(); + expect(listActionHarnesses.length).toBe(4); + await checkAction$(listActionHarnesses[0], 'list', IconType.FONT, 'custom action 1'); + await checkAction$(listActionHarnesses[1], 'angular', IconType.SVG, 'custom action 2'); + await checkAction$(listActionHarnesses[2], 'list', IconType.FONT, 'custom list action 1'); + await checkAction$(listActionHarnesses[3], 'angular', IconType.SVG, 'custom list action 2'); + + // row actions + const rowActionButtonHarness = await table.getRowActionsButton(1); + await rowActionButtonHarness.open(); + const rowActionHarnesses = await rowActionButtonHarness.getItems(); + expect(rowActionHarnesses.length).toBe(4); + + await checkAction$(rowActionHarnesses[0], 'list', IconType.FONT, 'custom action 1'); + await checkAction$(rowActionHarnesses[1], 'angular', IconType.SVG, 'custom action 2'); + await checkAction$(rowActionHarnesses[2], 'list', IconType.FONT, 'custom row action 1'); + await checkAction$(rowActionHarnesses[3], 'angular', IconType.SVG, 'custom row action 2'); + }); + + it('routerlinks should be displayed correctly', async () => { + fixture.detectChanges(); + + const rowActionButtonHarness = await table.getRowActionsButton(1); + await rowActionButtonHarness.open(); + const rowActionHarnesses = await rowActionButtonHarness.getItems(); + expect(rowActionHarnesses.length).toBe(4); + + const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); + expect(links.length).toEqual(1); + expect(links[0].attributes['href']).toEqual('/1?a=item_1'); + }); + + async function checkAction$( + menuItemHarness: MatMenuItemHarness, + expectedIcon: string, + expectedIconType: IconType, + expectedText: string + ): Promise { + expect(menuItemHarness).toBeTruthy(); + + const iconHarness = await menuItemHarness.getHarness(MatIconHarness); + expect(iconHarness).toBeTruthy(); + expect(await iconHarness.getName()).toEqual(expectedIcon); + expect(await iconHarness.getType()).toEqual(expectedIconType); + + // we use toContain here, because getText() includes the icon name as well + expect(await menuItemHarness.getText()).toContain(expectedText); + } + }); + + describe('async table actions', () => { + beforeEach(async () => { + await initTestComponent( + new PsTableDataSource({ + loadDataFn: () => + of([ + { id: 1, str: 'item 1' }, + { id: 2, str: 'item 2' }, + { id: 3, str: 'item 3' }, + ]), + mode: 'client', + actions: of([ + { + label: 'custom action', + icon: '', + scope: PsTableActionScope.all, + actionFn: (selection) => component.onListActionExecute(selection), + }, + { + label: 'custom list action', + icon: '', + scope: PsTableActionScope.list, + actionFn: () => {}, + }, + { + label: 'custom row action', + icon: '', + scope: PsTableActionScope.row, + actionFn: () => {}, + }, + ]), + }) + ); + }); + + it('async list actions should work', async () => { + component.refreshable = false; + fixture.detectChanges(); + const listActionsButtonHarness = await table.getListActionsButton(); + await listActionsButtonHarness.open(); + const listActions = await listActionsButtonHarness.getItems(); + expect(listActions.length).toEqual(2); + expect(await listActions[0].getText()).toEqual('custom action'); + expect(await listActions[1].getText()).toEqual('custom list action'); + }); + + it('async row actions should work', async () => { + component.refreshable = false; + fixture.detectChanges(); + const rowActionsButtonHarness = await table.getRowActionsButton(1); + await rowActionsButtonHarness.open(); + const rowActions = await rowActionsButtonHarness.getItems(); + expect(rowActions.length).toEqual(2); + expect(await rowActions[0].getText()).toEqual('custom action'); + expect(await rowActions[1].getText()).toEqual('custom row action'); + }); + }); }); }); diff --git a/projects/components/table/src/table.module.ts b/projects/components/table/src/table.module.ts index d6908773..67921793 100644 --- a/projects/components/table/src/table.module.ts +++ b/projects/components/table/src/table.module.ts @@ -8,10 +8,12 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatPaginatorModule } from '@angular/material/paginator'; +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 { RouterModule } from '@angular/router'; import { PsBlockUiModule } from '@prosoft/components/block-ui'; import { PsFlipContainerModule } from '@prosoft/components/flip-container'; import { PsSavebarModule } from '@prosoft/components/savebar'; @@ -66,6 +68,7 @@ import { PsTableComponent } from './table.component'; imports: [ CommonModule, FormsModule, + RouterModule, MatSortModule, MatTableModule, MatPaginatorModule, @@ -77,6 +80,7 @@ import { PsTableComponent } from './table.component'; MatInputModule, MatCardModule, MatTooltipModule, + MatProgressSpinnerModule, PsFlipContainerModule, PsSavebarModule, PsBlockUiModule, diff --git a/projects/prosoft-components-demo/src/app/app.component.ts b/projects/prosoft-components-demo/src/app/app.component.ts index 9222f6bc..eaca73f8 100644 --- a/projects/prosoft-components-demo/src/app/app.component.ts +++ b/projects/prosoft-components-demo/src/app/app.component.ts @@ -1,7 +1,13 @@ import { Component } from '@angular/core'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) -export class AppComponent {} +export class AppComponent { + constructor(matIconRegistry: MatIconRegistry, domSanitizer: DomSanitizer) { + matIconRegistry.addSvgIcon('angular', domSanitizer.bypassSecurityTrustResourceUrl('../assets/angular.svg')); + } +} diff --git a/projects/prosoft-components-demo/src/app/app.module.ts b/projects/prosoft-components-demo/src/app/app.module.ts index e5d46304..91b31661 100644 --- a/projects/prosoft-components-demo/src/app/app.module.ts +++ b/projects/prosoft-components-demo/src/app/app.module.ts @@ -1,5 +1,7 @@ +import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -14,7 +16,9 @@ import { AppComponent } from './app.component'; imports: [ BrowserModule, BrowserAnimationsModule, + HttpClientModule, + MatIconModule, MatSidenavModule, MatToolbarModule, MatListModule, diff --git a/projects/prosoft-components-demo/src/app/table-demo/table-demo.component.ts b/projects/prosoft-components-demo/src/app/table-demo/table-demo.component.ts index 6325fa85..8aecd9c1 100644 --- a/projects/prosoft-components-demo/src/app/table-demo/table-demo.component.ts +++ b/projects/prosoft-components-demo/src/app/table-demo/table-demo.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild } from '@angular/core'; import { PageEvent } from '@angular/material/paginator'; import { PsTableComponent, PsTableDataSource } from '@prosoft/components/table'; -import { PsTableActionScope } from '@prosoft/components/table/src/models'; +import { IPsTableAction, PsTableActionScope } from '@prosoft/components/table/src/models'; import { of, timer } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { delay, first, map } from 'rxjs/operators'; interface ISampleData { id: number; @@ -169,6 +169,55 @@ export class TableDemoComponent { }, ], }, + { + label: 'async table actions', + icon: 'angular', + isSvgIcon: true, + scope: PsTableActionScope.all, + children: of([ + { + label: 'async table action 1', + icon: 'cancel', + scope: PsTableActionScope.all, + }, + { + label: 'async table action 2', + icon: 'cancel', + scope: PsTableActionScope.all, + }, + { + label: 'async table action 3', + icon: 'cancel', + scope: PsTableActionScope.all, + children: of([ + { + label: 'sub async table action 1', + icon: 'cancel', + scope: PsTableActionScope.row, + }, + { + label: 'sub async table action 2', + icon: 'cancel', + scope: PsTableActionScope.all, + }, + { + label: 'sub async table action 3', + icon: 'cancel', + scope: PsTableActionScope.all, + }, + ]).pipe(delay(400)), + }, + { + label: 'router link', + icon: 'cancel', + scope: PsTableActionScope.row, + routerLink: (item: ISampleData) => ['..', item.id], + routerLinkQueryParams: (item: ISampleData) => ({ + a: item.string, + }), + }, + ] as IPsTableAction[]).pipe(delay(500)), + }, ], }); diff --git a/projects/prosoft-components-demo/src/assets/angular.svg b/projects/prosoft-components-demo/src/assets/angular.svg new file mode 100644 index 00000000..bf081acb --- /dev/null +++ b/projects/prosoft-components-demo/src/assets/angular.svg @@ -0,0 +1,16 @@ + + + + + + + + + +