From a6f750c87451af3180c39c3c79d31234e5fe5df4 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:56:56 -0400 Subject: [PATCH 01/54] feat(components/ag-grid): create an easy mode for data grid --- apps/playground/src/app/app.component.ts | 32 +- .../src/app/components/components.module.ts | 17 + .../data-grid/basic/grid.component.html | 183 ++++++++ .../data-grid/basic/grid.component.scss | 5 + .../data-grid/basic/grid.component.ts | 127 +++++ .../components/data-grid/data-grid.module.ts | 38 ++ .../paging/grid-paging.component.html | 27 ++ .../data-grid/paging/grid-paging.component.ts | 15 + .../grids/basic/grid.component.html | 240 ++++++++++ .../grids/basic/grid.component.scss | 5 + .../components/grids/basic/grid.component.ts | 217 +++++++++ .../src/app/components/grids/grids.module.ts | 47 ++ .../grids/paging/grid-paging.component.html | 18 + .../grids/paging/grid-paging.component.ts | 14 + .../grids/search/grid-search.component.html | 18 + .../grids/search/grid-search.component.ts | 14 + .../list-builder/list-builder.module.ts | 29 ++ .../list-view-grid.component.html | 148 ++++++ .../list-view-grid.component.ts | 110 +++++ apps/playground/src/main.ts | 12 +- libs/components/ag-grid/documentation.json | 1 + libs/components/ag-grid/project.json | 22 +- libs/components/ag-grid/src/index.ts | 3 + .../ag-grid/header/header.component.html | 11 +- .../ag-grid/header/header.component.ts | 22 +- .../fixtures/ag-grid-test.component.html | 151 ++++++ .../fixtures/ag-grid-test.component.ts | 113 +++++ .../sky-ag-grid-column.component.ts | 58 +++ .../sky-ag-grid/sky-ag-grid.component.css | 4 + .../sky-ag-grid/sky-ag-grid.component.html | 24 + .../sky-ag-grid/sky-ag-grid.component.spec.ts | 270 +++++++++++ .../sky-ag-grid/sky-ag-grid.component.ts | 432 ++++++++++++++++++ .../modules/ag-grid/types/header-params.ts | 15 +- .../ag-grid/testing/eslint.config.js | 5 + libs/components/ag-grid/testing/karma.conf.js | 19 + .../ag-grid/testing/ng-package.json | 5 + libs/components/ag-grid/testing/project.json | 47 ++ .../ag-grid-wrapper-harness.filters.ts | 7 + .../ag-grid-wrapper-harness.spec.ts | 43 ++ .../ag-grid-wrapper-harness.ts | 80 ++++ .../ag-grid/ag-grid-harness.filters.ts | 7 + .../modules/ag-grid/ag-grid-harness.spec.ts | 99 ++++ .../src/modules/ag-grid/ag-grid-harness.ts | 59 +++ .../fixtures/ag-grid-test.component.html | 151 ++++++ .../fixtures/ag-grid-test.component.ts | 114 +++++ .../ag-grid/testing/src/public-api.ts | 1 + .../ag-grid/testing/tsconfig.spec.json | 8 + libs/components/code-examples/src/index.ts | 1 + .../grid/basic/context-menu.component.html | 31 ++ .../grid/basic/context-menu.component.ts | 36 ++ .../lib/modules/ag-grid/grid/basic/data.ts | 157 +++++++ .../ag-grid/grid/basic/example.component.html | 15 + .../ag-grid/grid/basic/example.component.ts | 18 + tsconfig.base.json | 3 + 54 files changed, 3308 insertions(+), 40 deletions(-) create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.html create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.scss create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.ts create mode 100644 apps/playground/src/app/components/data-grid/data-grid.module.ts create mode 100644 apps/playground/src/app/components/data-grid/paging/grid-paging.component.html create mode 100644 apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.html create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.scss create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.ts create mode 100644 apps/playground/src/app/components/grids/grids.module.ts create mode 100644 apps/playground/src/app/components/grids/paging/grid-paging.component.html create mode 100644 apps/playground/src/app/components/grids/paging/grid-paging.component.ts create mode 100644 apps/playground/src/app/components/grids/search/grid-search.component.html create mode 100644 apps/playground/src/app/components/grids/search/grid-search.component.ts create mode 100644 apps/playground/src/app/components/list-builder/list-builder.module.ts create mode 100644 apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html create mode 100644 apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts create mode 100644 libs/components/ag-grid/testing/eslint.config.js create mode 100644 libs/components/ag-grid/testing/karma.conf.js create mode 100644 libs/components/ag-grid/testing/ng-package.json create mode 100644 libs/components/ag-grid/testing/project.json create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts create mode 100644 libs/components/ag-grid/testing/src/public-api.ts create mode 100644 libs/components/ag-grid/testing/tsconfig.spec.json create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts diff --git a/apps/playground/src/app/app.component.ts b/apps/playground/src/app/app.component.ts index e9e7487c56..5457348d7f 100644 --- a/apps/playground/src/app/app.component.ts +++ b/apps/playground/src/app/app.component.ts @@ -1,12 +1,6 @@ -import { Component, Renderer2 } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { Router, RouterLink, RouterOutlet } from '@angular/router'; -import { - SkyAppViewportService, - SkyTheme, - SkyThemeMode, - SkyThemeService, - SkyThemeSettings, -} from '@skyux/theme'; +import { SkyAppViewportService } from '@skyux/theme'; import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selector.component'; @@ -22,29 +16,17 @@ import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selecto export class AppComponent { public height = 80; - constructor( - private router: Router, - renderer: Renderer2, - themeService: SkyThemeService, - viewportService: SkyAppViewportService, - ) { - viewportService.reserveSpace({ + readonly #router = inject(Router); + + constructor() { + inject(SkyAppViewportService).reserveSpace({ id: 'playground-controls', position: 'top', size: this.height, }); - - themeService.init( - document.body, - renderer, - new SkyThemeSettings( - SkyTheme.presets['default'], - SkyThemeMode.presets.light, - ), - ); } public isHome(): boolean { - return this.router.url === '/'; + return this.#router.url === '/'; } } diff --git a/apps/playground/src/app/components/components.module.ts b/apps/playground/src/app/components/components.module.ts index f6a9962728..8b5680309f 100644 --- a/apps/playground/src/app/components/components.module.ts +++ b/apps/playground/src/app/components/components.module.ts @@ -61,6 +61,16 @@ export const componentRoutes: Routes = [ loadChildren: () => import('./forms/forms.module').then((m) => m.FormsModule), }, + { + path: 'grids', + loadChildren: () => + import('./grids/grids.module').then((m) => m.GridsModule), + }, + { + path: 'data-grid', + loadChildren: () => + import('./data-grid/data-grid.module').then((m) => m.DataGridModule), + }, { path: 'help-inline', loadChildren: () => @@ -78,6 +88,13 @@ export const componentRoutes: Routes = [ loadChildren: () => import('./layout/layout.module').then((m) => m.LayoutModule), }, + { + path: 'list-builder', + loadChildren: () => + import('./list-builder/list-builder.module').then( + (m) => m.ListBuilderModule, + ), + }, { path: 'lists', loadChildren: () => diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.html b/apps/playground/src/app/components/data-grid/basic/grid.component.html new file mode 100644 index 0000000000..6228918dc1 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html @@ -0,0 +1,183 @@ +

Grid

+ + +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid with multiselect enabled

+ + + + + + + +
+ + + + + @if (!hideCol3()) { + + } + + +

Selected rows: {{ selectedRowIds | json }}

+
+ +

Grid with inline help

+ +
+ + + + @if (!hideCol3()) { + + } + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid with scroll bars

+ +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid with row delete

+ +
+ + + + + @if (row.id) { + + + + + + + + } + + + + + @if (!hideCol3()) { + + } + + + + This is a numeric column. Click here to learn more. + +
+ +

Grid w/ aligned columns

+ +
+ + + + @if (!hideCol3()) { + + } + +
+ +

Grid w/ aligned columns and inline help

+ +
+ + + + @if (!hideCol3()) { + + } + +
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.scss b/apps/playground/src/app/components/data-grid/basic/grid.component.scss new file mode 100644 index 0000000000..cf3267ce27 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.scss @@ -0,0 +1,5 @@ +h1 { + // Margins throw off the screenshots; use padding for the same effect. + margin: 0; + padding: 15px 0; +} diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.ts b/apps/playground/src/app/components/data-grid/basic/grid.component.ts new file mode 100644 index 0000000000..f3963bfd52 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.ts @@ -0,0 +1,127 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + ViewChild, + inject, + model, +} from '@angular/core'; +import { + SkyAgGridColumnComponent, + SkyAgGridComponent, + SkyAgGridRowDeleteCancelArgs, + SkyAgGridRowDeleteConfirmArgs, +} from '@skyux/ag-grid'; +import { SkyGridModule } from '@skyux/grids'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: string; + myId?: string; +} + +@Component({ + selector: 'app-data-grid', + templateUrl: './grid.component.html', + styleUrls: ['./grid.component.scss'], + imports: [ + SkyAgGridComponent, + SkyAgGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + SkyGridModule, + JsonPipe, + ], +}) +export default class GridComponent { + public asyncPopover: any; + + public dataForRowDeleteGrid: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGrid: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' }, + ]; + + public selectedRowIds: string[] = []; + + public removeRowIds: string[] = []; + + protected readonly hideCol3 = model(false); + protected toggleCol3(): void { + this.hideCol3.update((show) => !show); + } + + @ViewChild('asyncPopoverRef') + private popoverTemplate: any; + + readonly #cdr = inject(ChangeDetectorRef); + + constructor() { + setTimeout(() => { + this.asyncPopover = this.popoverTemplate; + }, 1000); + } + + public selectAll(): void { + this.selectedRowIds = this.dataForSimpleGridWithMultiselect.map( + (item) => item.myId, + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds = []; + this.#cdr.markForCheck(); + } + + public cancelRowDelete(cancelArgs: SkyAgGridRowDeleteCancelArgs): void { + console.log('Item with id ' + cancelArgs.id + ' has not been deleted'); + } + + public deleteItem(id: string): void { + this.removeRowIds = [id, ...this.removeRowIds]; + } + + public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void { + setTimeout(() => { + console.log('Item with id ' + confirmArgs.id + ' has been deleted'); + // IF WORKED + this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter( + (data: any) => data.id !== confirmArgs.id, + ); + this.#cdr.markForCheck(); + }, 5000); + } + + public selectRow(): void { + this.selectedRowIds = ['101', '103', '105']; + this.#cdr.markForCheck(); + } +} diff --git a/apps/playground/src/app/components/data-grid/data-grid.module.ts b/apps/playground/src/app/components/data-grid/data-grid.module.ts new file mode 100644 index 0000000000..81e52cba21 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/data-grid.module.ts @@ -0,0 +1,38 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'basic', + loadComponent: () => import('./basic/grid.component'), + data: { + name: 'Data Grid (new)', + icon: 'table', + library: 'grids', + }, + }, + { + path: 'paging', + loadComponent: () => import('./paging/grid-paging.component'), + data: { + name: 'Data Grid Paging (new)', + icon: 'table', + library: 'grids', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class DataGridRoutingModule {} + +@NgModule({ + imports: [DataGridRoutingModule], +}) +export class DataGridModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html new file mode 100644 index 0000000000..59fb5688fb --- /dev/null +++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html @@ -0,0 +1,27 @@ +

Updates query string

+ + + + + + + {{ value.name }} + + + {{ value.name }} + + + +

No query string, just paging

+ + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts new file mode 100644 index 0000000000..db09c0c376 --- /dev/null +++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts @@ -0,0 +1,15 @@ +import { Component, input } from '@angular/core'; +import { SkyAgGridColumnComponent, SkyAgGridComponent } from '@skyux/ag-grid'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-data-grid-paging', + imports: [SkyAgGridComponent, SkyAgGridColumnComponent], + templateUrl: './grid-paging.component.html', +}) +export default class GridPagingComponent { + public readonly page = input(); + + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/grids/basic/grid.component.html b/apps/playground/src/app/components/grids/basic/grid.component.html new file mode 100644 index 0000000000..754a04e7b3 --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.html @@ -0,0 +1,240 @@ +

Grid

+ +
+ + + + + +
+ +

+ + +

+ +

Grid with multiselect enabled

+ + + + + + + +
+ + + + + + + +

Selected rows:

+

{{ selectedRowIdsDisplay }}

+
+ +

Grid with inline help

+ +
+ + + + + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid with scroll bars

+ +
+ + + + + +
+ +

Grid with row delete

+ +
+ + + + + + + + + + + + + + + + + + + + This is a numeric column. Click here to learn more. + + + + This is a string column. Click here to learn more. + +
+ +

Grid w/ aligned columns

+ +
+ + + + + +
+ +

Grid w/ aligned columns and inline help

+ +
+ + + + + +
diff --git a/apps/playground/src/app/components/grids/basic/grid.component.scss b/apps/playground/src/app/components/grids/basic/grid.component.scss new file mode 100644 index 0000000000..cf3267ce27 --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.scss @@ -0,0 +1,5 @@ +h1 { + // Margins throw off the screenshots; use padding for the same effect. + margin: 0; + padding: 15px 0; +} diff --git a/apps/playground/src/app/components/grids/basic/grid.component.ts b/apps/playground/src/app/components/grids/basic/grid.component.ts new file mode 100644 index 0000000000..d7d098087e --- /dev/null +++ b/apps/playground/src/app/components/grids/basic/grid.component.ts @@ -0,0 +1,217 @@ +import { CommonModule } from '@angular/common'; +import { Component, ViewChild } from '@angular/core'; +import { + SkyGridMessage, + SkyGridMessageType, + SkyGridModule, + SkyGridRowDeleteCancelArgs, + SkyGridRowDeleteConfirmArgs, + SkyGridSelectedRowsModelChange, +} from '@skyux/grids'; +import { ListSortFieldSelectorModel } from '@skyux/list-builder-common'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +import { Subject } from 'rxjs'; + +@Component({ + selector: 'app-grid', + templateUrl: './grid.component.html', + styleUrls: ['./grid.component.scss'], + imports: [CommonModule, SkyGridModule, SkyPopoverModule, SkyDropdownModule], +}) +export default class GridComponent { + public asyncPopover: any; + + public dataForRowDeleteGrid: any = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGrid = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg' }, + ]; + + public dataForSimpleGridWithMultiselect = [ + { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' }, + ]; + + public columnsForSimpleGrid = ['column1', 'column2', 'column3']; + + public sortFieldForSimpleGrid: ListSortFieldSelectorModel = { + descending: true, + fieldSelector: 'column2', + }; + + public gridController = new Subject(); + + public gridRowDeleteController = new Subject(); + + public highlightText: string; + + public rowHighlightedId: string; + + public selectedRowIds: string[]; + + public selectedRowIdsDisplay: string[]; + + public selectedRows: string; + + @ViewChild('asyncPopoverRef') + private popoverTemplate: any; + + constructor() { + setTimeout(() => { + this.asyncPopover = this.popoverTemplate; + }, 1000); + } + + public toggleCol3(): void { + const col3Index = this.columnsForSimpleGrid.indexOf('column3'); + if (col3Index === -1) { + this.columnsForSimpleGrid.push('column3'); + } else { + this.columnsForSimpleGrid.splice(col3Index, 1); + } + this.columnsForSimpleGrid = [...this.columnsForSimpleGrid]; + } + + public sortChangedSimpleGrid(activeSort: ListSortFieldSelectorModel): void { + this.dataForSimpleGrid = this.performSort( + activeSort, + this.dataForSimpleGrid, + ); + } + + public sortChangedMultiselectGrid( + activeSort: ListSortFieldSelectorModel, + ): void { + this.dataForSimpleGridWithMultiselect = this.performSort( + activeSort, + this.dataForSimpleGridWithMultiselect, + ); + } + + public triggerTextHighlight(): void { + if (!this.highlightText) { + this.highlightText = 'e'; + } else { + this.highlightText = undefined; + } + } + + public triggerRowHighlight(): void { + if (!this.rowHighlightedId) { + this.rowHighlightedId = '2'; + } else { + this.rowHighlightedId = undefined; + } + } + + public onMultiselectSelectionChange( + value: SkyGridSelectedRowsModelChange, + ): void { + this.selectedRowIdsDisplay = value.selectedRowIds; + } + + public selectAll(): void { + this.sendMessage(SkyGridMessageType.SelectAll); + } + + public clearAll(): void { + this.sendMessage(SkyGridMessageType.ClearAll); + } + + public cancelRowDelete(cancelArgs: SkyGridRowDeleteCancelArgs): void { + console.log('Item with id ' + cancelArgs.id + ' has not been deleted'); + } + + public deleteItem(id: string): void { + this.gridRowDeleteController.next({ + type: SkyGridMessageType.PromptDeleteRow, + data: { + promptDeleteRow: { + id: id, + }, + }, + }); + } + + public finishRowDelete(confirmArgs: SkyGridRowDeleteConfirmArgs): void { + setTimeout(() => { + console.log('Item with id ' + confirmArgs.id + ' has been deleted'); + // IF WORKED + this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter( + (data: any) => data.id !== confirmArgs.id, + ); + }, 5000); + } + + public selectRow(): void { + this.selectedRowIds = ['101', '103', '105']; + } + + public onSelectedColumnIdsChange(event: any[]): void { + console.log(event); + } + + public onColumnWidthChange(event: any): void { + console.log(event); + } + + private performSort( + activeSort: ListSortFieldSelectorModel, + data: any[], + ): Array { + const sortField = activeSort.fieldSelector; + const descending = activeSort.descending; + + return data + .sort((a: any, b: any) => { + let value1 = a[sortField]; + let value2 = b[sortField]; + + if (value1 && typeof value1 === 'string') { + value1 = value1.toLowerCase(); + } + + if (value2 && typeof value2 === 'string') { + value2 = value2.toLowerCase(); + } + + if (value1 === value2) { + return 0; + } + + let result = value1 > value2 ? 1 : -1; + + if (descending) { + result *= -1; + } + + return result; + }) + .slice(); + } + + private sendMessage(type: SkyGridMessageType): void { + const message: SkyGridMessage = { type }; + this.gridController.next(message); + } +} diff --git a/apps/playground/src/app/components/grids/grids.module.ts b/apps/playground/src/app/components/grids/grids.module.ts new file mode 100644 index 0000000000..8d59f1a9c4 --- /dev/null +++ b/apps/playground/src/app/components/grids/grids.module.ts @@ -0,0 +1,47 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'basic', + loadComponent: () => import('./basic/grid.component'), + data: { + name: 'Grid', + icon: 'table', + library: 'grids', + }, + }, + { + path: 'paging', + loadComponent: () => import('./paging/grid-paging.component'), + data: { + name: 'Grid Paging', + icon: 'table', + library: 'grids', + }, + }, + { + path: 'search', + loadComponent: () => import('./search/grid-search.component'), + data: { + name: 'Grid Search', + icon: 'table', + library: 'grids', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class GridsRoutingModule {} + +@NgModule({ + imports: [GridsRoutingModule], +}) +export class GridsModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.html b/apps/playground/src/app/components/grids/paging/grid-paging.component.html new file mode 100644 index 0000000000..4e85feca8e --- /dev/null +++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.html @@ -0,0 +1,18 @@ + + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.ts b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts new file mode 100644 index 0000000000..3ecdbc3b7d --- /dev/null +++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyGridModule } from '@skyux/grids'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-grid-paging', + imports: [CommonModule, SkyGridModule], + templateUrl: './grid-paging.component.html', +}) +export default class GridPagingComponent { + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.html b/apps/playground/src/app/components/grids/search/grid-search.component.html new file mode 100644 index 0000000000..4e85feca8e --- /dev/null +++ b/apps/playground/src/app/components/grids/search/grid-search.component.html @@ -0,0 +1,18 @@ + + + + + + + {{ value.name }} + + + {{ value.name }} + + diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.ts b/apps/playground/src/app/components/grids/search/grid-search.component.ts new file mode 100644 index 0000000000..60a5f46951 --- /dev/null +++ b/apps/playground/src/app/components/grids/search/grid-search.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyGridModule } from '@skyux/grids'; + +import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data'; + +@Component({ + selector: 'app-grid-search', + imports: [CommonModule, SkyGridModule], + templateUrl: './grid-search.component.html', +}) +export default class GridSearchComponent { + protected readonly data = AG_GRID_DEMO_DATA; +} diff --git a/apps/playground/src/app/components/list-builder/list-builder.module.ts b/apps/playground/src/app/components/list-builder/list-builder.module.ts new file mode 100644 index 0000000000..00a464c2c8 --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-builder.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { ComponentRouteInfo } from '../../shared/component-info/component-route-info'; + +const routes: ComponentRouteInfo[] = [ + { + path: 'grid', + loadComponent: () => import('./list-view-grid/list-view-grid.component'), + data: { + name: 'AAA List Builder Grid', + icon: 'table', + library: 'list-builder-view-grids', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +class ListBuilderRoutingModule {} + +@NgModule({ + imports: [ListBuilderRoutingModule], +}) +export class ListBuilderModule { + public static routes = routes; +} diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html new file mode 100644 index 0000000000..d47c89cffd --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html @@ -0,0 +1,148 @@ +
+ + + + + + + + + + + + + + + + +

+ +

+
+ +
+ + + + + + + + + + + + + + + +
+ +

List-view-grid with inline delete

+ +
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts new file mode 100644 index 0000000000..f1f5b15548 --- /dev/null +++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts @@ -0,0 +1,110 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { SkyAlertModule } from '@skyux/indicators'; +import { SkyListModule, SkyListToolbarModule } from '@skyux/list-builder'; +import { + SkyListViewGridMessage, + SkyListViewGridMessageType, + SkyListViewGridModule, + SkyListViewGridRowDeleteCancelArgs, + SkyListViewGridRowDeleteConfirmArgs, +} from '@skyux/list-builder-view-grids'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { BehaviorSubject, Subject } from 'rxjs'; + +@Component({ + selector: 'app-list-view-grid', + standalone: true, + templateUrl: './list-view-grid.component.html', + imports: [ + CommonModule, + SkyAlertModule, + SkyDropdownModule, + SkyListModule, + SkyListViewGridModule, + SkyListToolbarModule, + ], +}) +export default class ListViewGridTestComponent { + public gridController = new Subject(); + + public itemSubject: BehaviorSubject = new BehaviorSubject([]); + + public items = this.itemSubject.asObservable(); + + public rowHighlightedId: string; + + public selectedIds: string[]; + + private defaultItems = [ + { id: '1', column1: 101, column2: 'Apple', column3: 'Anne eats apples' }, + { id: '2', column1: 202, column2: 'Banana', column3: 'Ben eats bananas' }, + { id: '3', column1: 303, column2: 'Pear', column3: 'Patty eats pears' }, + { id: '4', column1: 404, column2: 'Grape', column3: 'George eats grapes' }, + { id: '5', column1: 505, column2: 'Banana', column3: 'Becky eats bananas' }, + { id: '6', column1: 606, column2: 'Lemon', column3: 'Larry eats lemons' }, + { + id: '7', + column1: 707, + column2: 'Strawberry', + column3: 'Sally eats strawberries', + }, + ]; + + constructor() { + this.itemSubject.next(this.defaultItems); + } + + public onToggleRowHighlightClick(): void { + this.rowHighlightedId = this.rowHighlightedId ? undefined : '2'; + } + + public onSelectedIdsChange(event: any): void { + console.log(event); + } + + public onRowDeleteCancel( + cancelArgs: SkyListViewGridRowDeleteCancelArgs, + ): void { + this.gridController.next({ + type: SkyListViewGridMessageType.AbortDeleteRow, + data: { + abortDeleteRow: { + id: cancelArgs.id, + }, + }, + }); + } + + public onRowDeleteConfirm( + confirmArgs: SkyListViewGridRowDeleteConfirmArgs, + ): void { + const removeIndex = this.defaultItems + .map((item) => { + return item.id; + }) + .indexOf(confirmArgs.id); + + if (removeIndex) { + setTimeout(() => { + this.defaultItems = this.defaultItems.filter( + (data: any) => data.id !== confirmArgs.id, + ); + this.itemSubject.next(this.defaultItems); + console.log('Item with id ' + confirmArgs.id + ' has been deleted.'); + }, 1000); + } + } + + public onDeleteItemClick(id: string): void { + this.gridController.next({ + type: SkyListViewGridMessageType.PromptDeleteRow, + data: { + promptDeleteRow: { + id: id, + }, + }, + }); + } +} diff --git a/apps/playground/src/main.ts b/apps/playground/src/main.ts index 52f5d3469c..bf02e81ff8 100644 --- a/apps/playground/src/main.ts +++ b/apps/playground/src/main.ts @@ -1,9 +1,13 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; -import { provideRouter, withHashLocation } from '@angular/router'; +import { + provideRouter, + withComponentInputBinding, + withHashLocation, +} from '@angular/router'; import { SKY_LOG_LEVEL, SkyHelpService, SkyLogLevel } from '@skyux/core'; import { provideIconPreview } from '@skyux/storybook/icon-preview'; -import { SkyThemeService } from '@skyux/theme'; +import { provideInitialTheme } from '@skyux/theme'; import { AppComponent } from './app/app.component'; import { routes } from './app/app.routes'; @@ -13,8 +17,8 @@ bootstrapApplication(AppComponent, { providers: [ provideAnimations(), provideIconPreview(), - provideRouter(routes, withHashLocation()), - SkyThemeService, + provideInitialTheme('modern'), + provideRouter(routes, withHashLocation(), withComponentInputBinding()), { provide: SkyHelpService, useClass: PlaygroundHelpService }, { provide: SKY_LOG_LEVEL, useValue: SkyLogLevel.Info }, ], diff --git a/libs/components/ag-grid/documentation.json b/libs/components/ag-grid/documentation.json index 704d9419c2..ae75cc3dad 100644 --- a/libs/components/ag-grid/documentation.json +++ b/libs/components/ag-grid/documentation.json @@ -38,6 +38,7 @@ }, "codeExamples": { "docsIds": [ + "AgGridBasicExampleComponent", "AgGridDataGridBasicExampleComponent", "AgGridDataGridBasicMultiselectExampleComponent", "AgGridDataGridDataManagerExampleComponent", diff --git a/libs/components/ag-grid/project.json b/libs/components/ag-grid/project.json index cd5d54676c..b31ff116cd 100644 --- a/libs/components/ag-grid/project.json +++ b/libs/components/ag-grid/project.json @@ -4,6 +4,8 @@ "projectType": "library", "sourceRoot": "libs/components/ag-grid/src", "prefix": "sky", + "tags": ["component", "npm"], + "implicitDependencies": ["phone-field", "validation"], "targets": { "build": { "executor": "@nx/angular:package", @@ -19,7 +21,21 @@ "tsConfig": "libs/components/ag-grid/tsconfig.lib.json" } }, - "defaultConfiguration": "production" + "defaultConfiguration": "production", + "dependsOn": [ + "^build", + { + "projects": ["core"], + "target": "build" + } + ], + "inputs": [ + "buildInputs", + "^buildInputs", + "{workspaceRoot}/libs/components/ag-grid/testing/src/**/*", + "!{workspaceRoot}/libs/components/ag-grid/testing/src/**/*.spec.ts", + "!{workspaceRoot}/libs/components/ag-grid/testing/src/**/fixtures/**/*" + ] }, "test": { "executor": "@angular-devkit/build-angular:karma", @@ -68,7 +84,5 @@ "command": "ts-node --project ./scripts/tsconfig.json ./scripts/postbuild-ag-grid.ts" } } - }, - "implicitDependencies": ["phone-field", "validation"], - "tags": ["component", "npm"] + } } diff --git a/libs/components/ag-grid/src/index.ts b/libs/components/ag-grid/src/index.ts index f04ebefc6f..12ad65bb84 100644 --- a/libs/components/ag-grid/src/index.ts +++ b/libs/components/ag-grid/src/index.ts @@ -30,6 +30,9 @@ export { SkyGetGridOptionsArgs } from './lib/modules/ag-grid/types/sky-grid-opti export { SkyAgGridTextProperties } from './lib/modules/ag-grid/types/text-properties'; export { SkyAgGridValidatorProperties } from './lib/modules/ag-grid/types/validator-properties'; +export { SkyAgGridComponent } from './lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component'; +export { SkyAgGridColumnComponent } from './lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component'; + // Components and directives must be exported to support Angular's "partial" Ivy compiler. // Obscure names are used to indicate types are not part of the public API. export { SkyAgGridDataManagerAdapterDirective as λ14 } from './lib/modules/ag-grid/ag-grid-data-manager-adapter.directive'; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html index d5f4e770f1..511e2b7f5d 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html @@ -78,6 +78,15 @@ } } - + + @if (params()?.helpPopoverContent) { + + } + diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts index ab0f13e206..12997639ef 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts @@ -17,6 +17,7 @@ import { SkyDynamicComponentLocation, SkyDynamicComponentService, } from '@skyux/core'; +import { SkyHelpInlineModule } from '@skyux/help-inline'; import { SkyI18nModule } from '@skyux/i18n'; import { SkyIconModule } from '@skyux/icon'; import { SkyThemeModule } from '@skyux/theme'; @@ -41,7 +42,13 @@ import { SkyAgGridHeaderParams } from '../types/header-params'; '[attr.aria-label]': 'displayName() || accessibleHeaderText()', '[attr.role]': '"note"', }, - imports: [SkyIconModule, SkyThemeModule, AsyncPipe, SkyI18nModule], + imports: [ + AsyncPipe, + SkyHelpInlineModule, + SkyI18nModule, + SkyIconModule, + SkyThemeModule, + ], }) export class SkyAgGridHeaderComponent implements IHeaderAngularComp, OnDestroy, AfterViewInit @@ -190,7 +197,12 @@ export class SkyAgGridHeaderComponent return; } - const inlineHelpComponent = this.params()?.inlineHelpComponent; + const params = this.params(); + const inlineHelpComponent = params?.inlineHelpComponent; + + if (params?.helpPopoverContent) { + return; + } if ( inlineHelpComponent && @@ -202,9 +214,9 @@ export class SkyAgGridHeaderComponent ); const headerInfo = new SkyAgGridHeaderInfo(); - headerInfo.column = this.params()?.column; - headerInfo.context = this.params()?.context; - headerInfo.displayName = this.params()?.displayName; + headerInfo.column = params?.column; + headerInfo.context = params?.context; + headerInfo.displayName = params?.displayName; this.#inlineHelpComponentRef = this.#dynamicComponentService.createComponent(inlineHelpComponent, { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html new file mode 100644 index 0000000000..9333e34bc3 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html @@ -0,0 +1,151 @@ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} +

Grid with multiselect enabled

+ + + + + + + +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (showCol3()) { + + } + } + +} +

Selected rows: {{ selectedRowIds() | json }}

+ +

Grid with row delete

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (row.id) { + + + + + + + + } + + + + + @if (showCol3()) { + + } @else { + + } + } + +} + + This is a numeric column. Click here to learn more. + + +

Grid w/ aligned columns and inline help

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts new file mode 100644 index 0000000000..35a6fa36bb --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts @@ -0,0 +1,113 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + inject, + input, + model, +} from '@angular/core'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +import { SkyAgGridRowDeleteCancelArgs } from '../../types/ag-grid-row-delete-cancel-args'; +import { SkyAgGridRowDeleteConfirmArgs } from '../../types/ag-grid-row-delete-confirm-args'; +import { SkyAgGridColumnComponent } from '../sky-ag-grid-column.component'; +import { SkyAgGridComponent } from '../sky-ag-grid.component'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: boolean; + myId?: string; +} + +@Component({ + selector: 'app-ag-grid-test', + templateUrl: './ag-grid-test.component.html', + imports: [ + SkyAgGridComponent, + SkyAgGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + JsonPipe, + ], +}) +export class AgGridTestComponent { + public dataForRowDeleteGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' }, + ]; + + public readonly removeRowIds = model([]); + public readonly rowHighlightedId = model(); + public readonly selectedRowIds = model([]); + public readonly visibleColumnIds = model([]); + + public page = 1; + public pageSize = 0; + public pageQueryParam = ''; + + protected readonly showAllColumns = input(true); + protected readonly showAllGrids = input(true); + protected readonly showCol3 = input(true); + protected readonly showCol3HeaderText = input(true); + + readonly #cdr = inject(ChangeDetectorRef); + + public selectAll(): void { + this.selectedRowIds.set( + (this.dataForSimpleGridWithMultiselect ?? []).map( + (item) => item.myId as string, + ), + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds.set([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public cancelRowDelete(_cancelArgs: SkyAgGridRowDeleteCancelArgs): void { + // noop + } + + public deleteItem(id: string): void { + this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]); + } + + public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void { + this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter( + (data: RowModel) => data.id !== confirmArgs.id, + ); + } + + public selectRow(): void { + this.selectedRowIds.set(['101', '103', '105']); + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts new file mode 100644 index 0000000000..e8153afe37 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts @@ -0,0 +1,58 @@ +import { + Component, + TemplateRef, + booleanAttribute, + computed, + contentChild, + input, + numberAttribute, +} from '@angular/core'; + +@Component({ + selector: 'sky-ag-grid-column', + template: '', +}) +export class SkyAgGridColumnComponent { + public readonly description = input(); + public readonly field = input(); + public readonly heading = input(); + public readonly id = input(); + + /** + * The title of the help popover. This property only applies when `helpPopoverContent` is + * also specified. + */ + public readonly helpPopoverTitle = input(); + + /** + * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * button is added to the column header. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) + * when clicked using the specified content and optional title. + */ + public readonly helpPopoverContent = input>(); + + public readonly hidden = input(false, { + transform: booleanAttribute, + }); + public readonly isSortable = input(true, { + transform: booleanAttribute, + }); + public readonly locked = input(false, { + transform: booleanAttribute, + }); + public readonly template = input>(); + public readonly type = input<'text' | 'number' | 'date' | 'boolean'>('text'); + public readonly width = input(0, { + transform: numberAttribute, + }); + + protected readonly templateChild = contentChild(TemplateRef); + + public readonly cellTemplate = computed | undefined>( + () => { + const template = this.template(); + const templateChild = this.templateChild(); + return template || templateChild; + }, + ); +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css new file mode 100644 index 0000000000..a06b21df93 --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css @@ -0,0 +1,4 @@ +:host { + display: block; + max-width: 100%; +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html new file mode 100644 index 0000000000..de6e2efc2a --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html @@ -0,0 +1,24 @@ + +@if (gridOptions(); as gridOptions) { + + + + + @if (pageCount(); as pageCount) { + + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts new file mode 100644 index 0000000000..51586cfcea --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts @@ -0,0 +1,270 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { provideLocationMocks } from '@angular/common/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { ActivatedRoute, Router, provideRouter } from '@angular/router'; +import { SkyWaitHarness } from '@skyux/indicators/testing'; +import { SkyPagingHarness } from '@skyux/lists/testing'; + +import { getGridApi } from 'ag-grid-community'; + +import { SkyAgGridWrapperComponent } from '../ag-grid-wrapper.component'; +import { SkyAgGridHeaderComponent } from '../header/header.component'; + +import { AgGridTestComponent } from './fixtures/ag-grid-test.component'; +import { SkyAgGridComponent } from './sky-ag-grid.component'; + +describe('SkyAgGridComponent', () => { + let fixture: ComponentFixture; + let component: AgGridTestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([]), provideLocationMocks()], + }); + fixture = TestBed.createComponent(AgGridTestComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should destroy and recreate grid', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + + fixture.componentRef.setInput('showAllGrids', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridComponent)), + ).toEqual([]); + + fixture.componentRef.setInput('showAllGrids', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridComponent)).length, + ).toEqual(4); + }); + + it('should destroy and recreate columns', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid + + fixture.componentRef.setInput('showCol3', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 2 + 3); // 4 grids with 2 columns each, plus 3 extra headers for multi-select, row delete, and date column + expect(component.visibleColumnIds()).toEqual(['column1', 'column2']); + + fixture.componentRef.setInput('showCol3', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid + expect(component.visibleColumnIds()).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(0); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridWrapperComponent)) + .length, + ).toEqual(0); + }); + + it('should handle empty data', async () => { + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyWaitHarness, + ); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + expect( + Array.from( + fixture.nativeElement + .querySelector('.ag-viewport') + .querySelectorAll('[role="row"]'), + ), + ).toHaveSize(0); + }); + + it('should select all rows', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyWaitHarness, + ); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getSelectedNodes()).toHaveSize(0); + api?.selectAll(); + expect(api?.getSelectedNodes()).toHaveSize(7); + }); + + it('should select some rows', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + fixture.detectChanges(); + await fixture.whenStable(); + const waitHarnesses = + await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses( + SkyWaitHarness, + ); + await expectAsync( + Promise.all(waitHarnesses.map((waitHarness) => waitHarness.isWaiting())), + ).toBeResolvedTo([false, false, false, false]); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="multiselect-grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getSelectedNodes()).toHaveSize(0); + component.selectedRowIds.set(['102', '104', '106']); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getSelectedNodes()).toHaveSize(3); + }); + + it('should highlight row', async () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + const waitHarness = await TestbedHarnessEnvironment.loader( + fixture, + ).getHarness(SkyWaitHarness.with({ ancestor: '[data-sky-id="grid"]' })); + await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false); + const api = getGridApi( + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), + ); + expect(api).toBeTruthy(); + expect(api?.getRowNode('2')?.isSelected()).toBeFalse(); + fixture.componentRef.setInput('rowHighlightedId', '2'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getRowNode('2')?.isSelected()).toBeTrue(); + }); + + it('should handle paging without url changes', async () => { + component.pageSize = 4; + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const pagingHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyPagingHarness, + ); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + await pagingHarness.clickNextButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + await pagingHarness.clickPreviousButton(); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + }); + + it('should handle paging with url changes', async () => { + component.pageSize = 2; + component.pageQueryParam = 'page'; + const router = TestBed.inject(Router); + const navSpy = spyOn(router, 'navigate'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + const pagingHarness = + await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyPagingHarness, + ); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + await pagingHarness.clickPageButton(2); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + expect(navSpy).toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 2 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page = 2; + fixture.detectChanges(); + + await pagingHarness.clickPageButton(1); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1); + expect(navSpy).toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: null }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page = 1; + fixture.detectChanges(); + + await pagingHarness.clickPageButton(3); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(3); + expect(navSpy).toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 3 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page = 3; + fixture.detectChanges(); + + await pagingHarness.clickPageButton(2); + await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2); + expect(navSpy).toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 2 }, + queryParamsHandling: 'merge', + }); + navSpy.calls.reset(); + + component.page = 0; + fixture.detectChanges(); + expect(navSpy).not.toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: 0 }, + queryParamsHandling: 'merge', + }); + + component.page = Number.POSITIVE_INFINITY; + fixture.detectChanges(); + expect(navSpy).not.toHaveBeenCalledWith(['.'], { + relativeTo: jasmine.any(ActivatedRoute), + queryParams: { page: Number.POSITIVE_INFINITY }, + queryParamsHandling: 'merge', + }); + }); +}); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts new file mode 100644 index 0000000000..fcca4cf17f --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts @@ -0,0 +1,432 @@ +import { + ChangeDetectionStrategy, + Component, + booleanAttribute, + computed, + contentChildren, + effect, + inject, + input, + linkedSignal, + model, + numberAttribute, + output, + signal, + untracked, +} from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SkyWaitModule } from '@skyux/indicators'; +import { SkyPagingModule } from '@skyux/lists'; + +import { AgGridAngular } from 'ag-grid-angular'; +import { + AllCommunityModule, + ColDef, + DisplayedColumnsChangedEvent, + GetRowIdParams, + GridApi, + GridOptions, + GridPreDestroyedEvent, + ModuleRegistry, + SelectionChangedEvent, +} from 'ag-grid-community'; +import { + distinctUntilChanged, + filter, + fromEvent, + fromEventPattern, + map, + switchMap, + takeUntil, +} from 'rxjs'; + +import { SkyAgGridRowDeleteDirective } from '../ag-grid-row-delete.directive'; +import { SkyAgGridWrapperComponent } from '../ag-grid-wrapper.component'; +import { SkyAgGridService } from '../ag-grid.service'; +import { SkyAgGridRowDeleteCancelArgs } from '../types/ag-grid-row-delete-cancel-args'; +import { SkyAgGridRowDeleteConfirmArgs } from '../types/ag-grid-row-delete-confirm-args'; +import { SkyCellType } from '../types/cell-type'; + +import { SkyAgGridColumnComponent } from './sky-ag-grid-column.component'; + +ModuleRegistry.registerModules([AllCommunityModule]); + +function arraySorted(arr: string[]): string[] { + return arr.slice().sort((a, b) => a.localeCompare(b)); +} + +function arrayIsEqual(a: string[], b: string[]): boolean { + if (a.length !== b.length) { + return false; + } + const bSorted = arraySorted(b); + return arraySorted(a).every((v, i) => v === bSorted[i]); +} + +@Component({ + selector: 'sky-ag-grid', + imports: [ + AgGridAngular, + SkyAgGridRowDeleteDirective, + SkyAgGridWrapperComponent, + SkyPagingModule, + SkyWaitModule, + ], + templateUrl: './sky-ag-grid.component.html', + styleUrl: './sky-ag-grid.component.css', + host: { + '[style.height.px]': 'height() || undefined', + '[style.width.px]': 'width() || undefined', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SkyAgGridComponent< + T extends { id: string } = Record & { id: string }, +> { + /** + * The data for the grid. Each item requires an `id` and a property that maps + * to the `field` or `id` property of each column in the grid. + */ + public readonly data = input(); + + /** + * The columns to display by default based on the ID or field of the item. + */ + public readonly displayedColumns = input([]); + + /** + * Fires when columns change. This includes changes to the displayed columns and changes + * to the order of columns. The event emits an array of IDs for the displayed columns that + * reflects the column order. + */ + public readonly selectedColumnIdsChange = output(); + + /** + * The columns to hide by default based on the ID or field of the item. + */ + public readonly hiddenColumns = input([]); + + /** + * Whether to enable the multiselect feature to display a column of + * checkboxes on the left side of the grid. You can specify a unique ID with + * the `multiselectRowId` property, but multiselect defaults to the `id` property on + * the `data` object. + * @default false + */ + public readonly enableMultiselect = input(false, { + transform: booleanAttribute, + }); + + /** + * How the grid fits to its parent. The valid options are `width`, + * which fits the grid to the parent's full width, and `scroll`, which allows the grid + * to exceed the parent's width. If the grid does not have enough columns to fill + * the parent's width, it always stretches to the parent's full width. + * @default "width" + */ + public readonly fit = input<'width' | 'scroll'>('width'); + + /** + * The height of the grid. + */ + public readonly height = input(0, { + transform: numberAttribute, + }); + + /** + * The unique ID that matches a property on the `data` object. + * By default, this property uses the `id` property. + */ + public readonly multiselectRowId = input('id'); + + /** + * The current page number of the grid. When using `pageQueryParam`, this value should come from the query parameter. + * @default 1 + */ + public readonly page = input(1, { + transform: numberAttribute, + }); + + /** + * The number of items to display per page. Set to `0` to disable pagination. + * @default 0 + */ + public readonly pageSize = input(0, { + transform: numberAttribute, + }); + + /** + * The query parameter name to use for the current page number. When set, page changes are reflected in the URL. + */ + public readonly pageQueryParam = input(); + + /** + * The width of the grid in pixels. + */ + public readonly width = input(0, { + transform: numberAttribute, + }); + + /** + * The ID of the row to highlight. The ID matches the `multiselectRowId` property + * of the `data` object. Typically, this property is used in conjunction with + * the flyout component to indicate the currently selected row. Other rows + * are de-selected in the grid. + */ + public readonly rowHighlightedId = input(); + + /** + * The set of IDs for the rows to select in a multiselect grid. + * The IDs match the `multiselectRowId` properties of the `data` objects. + * Rows with IDs that are not included are de-selected in the grid. + */ + public readonly selectedRowIds = input([]); + + /** + * The set of IDs for the rows selected in a multiselect grid. + * The IDs match the `multiselectRowId` properties of the `data` objects. + */ + public readonly selectedRowIdsChange = output(); + + /** + * The set of IDs for the rows to prompt for delete confirmation. + * The IDs match the `multiselectRowId` properties of the `data` objects. + */ + protected readonly rowDeleteIds = model([]); + + /** + * Fires when users cancel the deletion of a row. + */ + public readonly rowDeleteCancel = output(); + + /** + * Fires when users confirm the deletion of a row. + */ + public readonly rowDeleteConfirm = output(); + + protected readonly columns = contentChildren(SkyAgGridColumnComponent); + protected readonly gridReady = signal(false); + protected readonly gridApi = signal(undefined); + protected readonly pageNumber = linkedSignal(this.page); + + readonly #gridDestroyed = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEventPattern((handler) => + api.addEventListener('gridPreDestroyed', handler), + ), + ), + ); + readonly #gridService = inject(SkyAgGridService); + readonly #gridSelectedRowIds = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEvent(api, 'selectionChanged').pipe( + takeUntil(this.#gridDestroyed), + map((selection) => + ( + (selection.selectedNodes ?? []) + .map((node) => node.id as string) + .filter(Boolean) as string[] + ).sort((a, b) => a.localeCompare(b)), + ), + distinctUntilChanged(arrayIsEqual), + ), + ), + ); + readonly #gridDisplayedColumnIds = toObservable(this.gridApi).pipe( + filter(Boolean), + switchMap((api) => + fromEvent( + api, + 'displayedColumnsChanged', + ).pipe( + takeUntil(this.#gridDestroyed), + map((columnsEvent) => + columnsEvent.api + .getAllDisplayedColumns() + .map((col) => col.getColId()), + ), + distinctUntilChanged(arrayIsEqual), + ), + ), + ); + readonly #columnDefs = computed[]>(() => { + const columns = this.columns(); + const displayed = this.displayedColumns().filter(Boolean); + const hidden = this.hiddenColumns().filter(Boolean); + return columns.map((col): ColDef => { + const colDef: ColDef = { + headerName: col.heading(), + headerComponentParams: { + helpPopoverTitle: col.helpPopoverTitle(), + helpPopoverContent: col.helpPopoverContent() || col.description(), + }, + initialHide: + col.hidden() || + (displayed.length > 0 && + !displayed.includes(col.id() || col.field() || '')) || + hidden.includes(col.id() || col.field() || ''), + sortable: col.isSortable(), + lockPosition: col.locked(), + suppressMovable: col.locked(), + type: [], + }; + if (col.field()) { + colDef.field = col.field(); + } else if (col.id()) { + colDef.colId = col.id(); + } + if (col.cellTemplate()) { + (colDef.type as string[]).push(SkyCellType.Template); + colDef.cellRendererParams = { template: col.cellTemplate() }; + } else if (col.type() === 'date') { + (colDef.type as string[]).push(SkyCellType.Date); + } else if (col.type() === 'number') { + (colDef.type as string[]).push(SkyCellType.Number); + } else if (col.type() === 'boolean') { + colDef.cellDataType = 'boolean'; + } else { + (colDef.type as string[]).push(SkyCellType.Text); + } + if (col.width() > 0) { + colDef.initialWidth = col.width(); + colDef.suppressSizeToFit = true; + } + return colDef; + }); + }); + readonly #activatedRoute = inject(ActivatedRoute, { optional: true }); + readonly #router = inject(Router, { optional: true }); + + protected readonly gridOptions = computed(() => { + const columnDefs = this.#columnDefs(); + if (columnDefs.length === 0) { + return undefined; + } + return this.#gridService.getGridOptions({ + gridOptions: { + columnDefs, + domLayout: this.height() ? 'normal' : 'autoHeight', + onGridReady: (args) => { + this.gridApi.set(args.api); + this.gridReady.set(true); + }, + pagination: this.pageSize() > 0, + suppressPaginationPanel: true, + paginationPageSize: this.pageSize() || undefined, + rowData: this.data(), + getRowId: (params: GetRowIdParams) => + params.data[this.multiselectRowId() as keyof T] as string, + rowSelection: this.enableMultiselect() + ? { + checkboxes: true, + headerCheckbox: true, + mode: 'multiRow', + } + : { + checkboxes: false, + mode: 'singleRow', + }, + autoSizeStrategy: + this.fit() === 'width' || this.width() + ? { + type: 'fitGridWidth', + } + : { + type: 'fitCellContents', + }, + }, + }) as GridOptions; + }); + protected readonly pageCount = computed(() => { + const dataLength = this.data()?.length ?? 0; + const pageSize = this.pageSize(); + const gridReady = this.gridReady(); + if (!gridReady || pageSize === 0) { + return 0; + } + return Math.ceil(dataLength / pageSize); + }); + + constructor() { + effect(() => { + const api = untracked(() => this.gridApi()); + const data = this.data(); + api?.setGridOption('rowData', data ?? []); + }); + effect(() => { + const api = untracked(() => this.gridApi()); + const columns = this.#columnDefs(); + api?.setGridOption('columnDefs', columns); + }); + effect(() => { + const api = this.gridApi(); + const selectedRowIds = this.selectedRowIds(); + this.data(); + const currentSelectedRowIds = + api?.getSelectedNodes().map((node) => node.id as string) ?? []; + if (!arrayIsEqual(selectedRowIds, currentSelectedRowIds)) { + api?.deselectAll(); + selectedRowIds.forEach((rowId) => + api?.getRowNode(rowId)?.setSelected(true), + ); + } + }); + effect(() => { + const api = this.gridApi(); + const rowHighlightedId = this.rowHighlightedId(); + this.data(); + if (rowHighlightedId) { + const rowNode = api?.getRowNode(rowHighlightedId); + if (rowNode?.isSelected() === false) { + rowNode?.setSelected(true, true); + } + } + }); + effect(() => { + const api = this.gridApi(); + const pageNumber = this.pageNumber(); + const pageCount = this.pageCount(); + if (!pageCount || pageNumber < 1 || pageNumber > pageCount || !api) { + return; + } + api.paginationGoToPage(pageNumber - 1); + }); + this.#gridDestroyed.pipe(takeUntilDestroyed()).subscribe(() => { + this.gridApi.set(undefined); + this.gridReady.set(false); + }); + this.#gridSelectedRowIds.pipe(takeUntilDestroyed()).subscribe((rowIds) => { + this.selectedRowIdsChange.emit(rowIds); + }); + this.#gridDisplayedColumnIds + .pipe(takeUntilDestroyed()) + .subscribe((columnIds) => { + this.selectedColumnIdsChange.emit(columnIds); + }); + } + + protected pageChange(page: number): void { + const pageQueryParam = this.pageQueryParam(); + const pageNumber = numberAttribute(page, 1); + if ( + pageQueryParam && + this.#activatedRoute && + this.#activatedRoute.snapshot.queryParamMap.get(pageQueryParam) !== + `${page}` + ) { + // When using a query parameter, send the change through the router. + void this.#router?.navigate(['.'], { + relativeTo: this.#activatedRoute, + queryParams: { + [pageQueryParam]: pageNumber === 1 ? null : pageNumber, + }, + queryParamsHandling: 'merge', + }); + } else if (page) { + this.pageNumber.set(page); + } + } +} diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts index fa342faf28..5a9e4bc7f7 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts @@ -1,4 +1,4 @@ -import { Type } from '@angular/core'; +import { TemplateRef, Type } from '@angular/core'; import { IHeaderParams } from 'ag-grid-community'; @@ -20,4 +20,17 @@ export interface SkyAgGridHeaderParams extends IHeaderParams { * @see SkyAgGridHeaderInfo */ inlineHelpComponent?: Type; + + /** + * The title of the help popover. This property only applies when `helpPopoverContent` is + * also specified. + */ + helpPopoverTitle?: string | undefined; + + /** + * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * button is added to the column heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) + * when clicked using the specified content and optional title. + */ + helpPopoverContent?: string | TemplateRef | undefined; } diff --git a/libs/components/ag-grid/testing/eslint.config.js b/libs/components/ag-grid/testing/eslint.config.js new file mode 100644 index 0000000000..b4c331fb6a --- /dev/null +++ b/libs/components/ag-grid/testing/eslint.config.js @@ -0,0 +1,5 @@ +const prettier = require('eslint-config-prettier'); +const baseConfig = require('../../../../eslint-base.config'); +const overrides = require('../../../../eslint-overrides.config'); + +module.exports = [...baseConfig, ...overrides, prettier]; diff --git a/libs/components/ag-grid/testing/karma.conf.js b/libs/components/ag-grid/testing/karma.conf.js new file mode 100644 index 0000000000..2d434ab15e --- /dev/null +++ b/libs/components/ag-grid/testing/karma.conf.js @@ -0,0 +1,19 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +const { join } = require('path'); +const getBaseKarmaConfig = require('../../../../karma.conf'); + +module.exports = function (config) { + const baseConfig = getBaseKarmaConfig(); + config.set({ + ...baseConfig, + coverageReporter: { + ...baseConfig.coverageReporter, + dir: join( + __dirname, + '../../../../coverage/libs/components/ag-grid/testing', + ), + }, + }); +}; diff --git a/libs/components/ag-grid/testing/ng-package.json b/libs/components/ag-grid/testing/ng-package.json new file mode 100644 index 0000000000..fbafcc4448 --- /dev/null +++ b/libs/components/ag-grid/testing/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/libs/components/ag-grid/testing/project.json b/libs/components/ag-grid/testing/project.json new file mode 100644 index 0000000000..78b2919abf --- /dev/null +++ b/libs/components/ag-grid/testing/project.json @@ -0,0 +1,47 @@ +{ + "name": "ag-grid-testing", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "libs/components/ag-grid/testing/src", + "prefix": "sky", + "tags": ["testing"], + "targets": { + "test": { + "executor": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "libs/components/ag-grid/testing/tsconfig.spec.json", + "karmaConfig": "libs/components/ag-grid/testing/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": ["**/fixtures/**"], + "styles": [ + "libs/components/theme/src/lib/styles/sky.scss", + "libs/components/theme/src/lib/styles/themes/modern/styles.scss" + ], + "polyfills": [ + "zone.js", + "zone.js/testing", + "libs/components/packages/src/polyfills.js" + ], + "inlineStyleLanguage": "scss", + "stylePreprocessorOptions": { + "includePaths": ["{workspaceRoot}"] + } + }, + "configurations": { + "ci": { + "browsers": "ChromeHeadlessNoSandbox", + "codeCoverage": true, + "progress": false, + "sourceMap": true, + "watch": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["{projectRoot}/src/**/*.ts"] + } + } + } +} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts new file mode 100644 index 0000000000..d507802190 --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts @@ -0,0 +1,7 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyAgGridWrapperHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyAgGridWrapperHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts new file mode 100644 index 0000000000..72cc88689f --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts @@ -0,0 +1,43 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SkyAgGridModule } from '@skyux/ag-grid'; + +import { SkyAgGridWrapperHarness } from './ag-grid-wrapper-harness'; + +@Component({ + selector: 'app-test', + template: ``, + imports: [SkyAgGridModule], +}) +class TestComponent {} + +describe('SkyAgGridWrapperHarness', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({}); + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); + + it('should check if the grid is ready', async () => { + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyAgGridWrapperHarness.with({ dataSkyId: 'wrapper' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + }); + + it('should throw error if the grid is not available', async () => { + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyAgGridWrapperHarness.with({ dataSkyId: 'wrapper' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + await expectAsync(harness.getDisplayedColumnIds()).toBeRejectedWith( + 'Unable to retrieve displayed column IDs.', + ); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeRejectedWith( + 'Unable to retrieve displayed column header names.', + ); + }); +}); diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts new file mode 100644 index 0000000000..58d9b47255 --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts @@ -0,0 +1,80 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { UnitTestElement } from '@angular/cdk/testing/testbed'; +import { SkyComponentHarness } from '@skyux/core/testing'; + +import { GridApi, getGridApi } from 'ag-grid-community'; + +import { SkyAgGridWrapperHarnessFilters } from './ag-grid-wrapper-harness.filters'; + +/** + * Harness for interacting with SKY UX AG Grid components in tests. + */ +export class SkyAgGridWrapperHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-ag-grid-wrapper'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyAgGridWrapperHarness` that meets certain criteria + */ + public static with( + filters: SkyAgGridWrapperHarnessFilters, + ): HarnessPredicate { + return SkyAgGridWrapperHarness.getDataSkyIdPredicate(filters); + } + + /** + * Checks whether the grid is ready. + */ + public async isGridReady(): Promise { + const gridReady = this.locatorFactory.locatorFor( + '.ag-root.ag-unselectable', + ); + + return await gridReady() + .then((el) => !!el) + .catch(() => false); + } + + /** + * Retrieves the IDs of the currently displayed columns. + */ + public async getDisplayedColumnIds(): Promise { + return await this.#getGridApi() + .then((api) => api.getAllDisplayedColumns().map((col) => col.getColId())) + .catch(() => Promise.reject('Unable to retrieve displayed column IDs.')); + } + + /** + * Retrieves the header names of the currently displayed columns. + */ + public async getDisplayedColumnHeaderNames(): Promise { + return await this.#getGridApi() + .then((api) => + api + .getAllDisplayedColumns() + .map((col) => col.getColDef().headerName || ''), + ) + .catch(() => + Promise.reject('Unable to retrieve displayed column header names.'), + ); + } + + async #getGridApi(): Promise { + await this.waitForTasksOutsideAngular(); + const locator = this.locatorFactory.locatorFor('ag-grid-angular'); + return await locator().then((grid) => { + if (grid instanceof UnitTestElement) { + const api = getGridApi(grid.element); + if (api) { + return api; + } + } + // If this harness were used in an environment that did not provide UnitTestElement. + /* istanbul ignore next */ + throw new Error('Unable to get GridApi from AgGridAngular component.'); + }); + } +} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts new file mode 100644 index 0000000000..3f0dc980fe --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts @@ -0,0 +1,7 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyAgGridHarness` instances. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type +export interface SkyAgGridHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts new file mode 100644 index 0000000000..9a14cda5fa --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts @@ -0,0 +1,99 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SkyAgGridHarness } from './ag-grid-harness'; +import { AgGridTestComponent } from './fixtures/ag-grid-test.component'; + +describe('ag-grid-harness', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({}); + fixture = TestBed.createComponent(AgGridTestComponent); + }); + + it('should check if the grid is ready', async () => { + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyAgGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + + fixture.componentRef.setInput('showAllColumns', true); + fixture.detectChanges(); + + await expectAsync(harness.isGridReady()).toBeResolvedTo(true); + }); + + it('should get columns', async () => { + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyAgGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(true); + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + 'column3', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + 'Column3', + ]); + + fixture.componentRef.setInput('showCol3', false); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + ]); + }); + + it('should throw error if the grid is not available', async () => { + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + + const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness( + SkyAgGridHarness.with({ dataSkyId: 'grid' }), + ); + await expectAsync(harness.isGridReady()).toBeResolvedTo(false); + await expectAsync(harness.getDisplayedColumnIds()).toBeRejectedWith( + 'Unable to retrieve displayed column IDs.', + ); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeRejectedWith( + 'Unable to retrieve displayed column header names.', + ); + + fixture.componentRef.setInput('showAllColumns', true); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([ + 'column1', + 'column2', + 'column3', + ]); + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + 'Column3', + ]); + + fixture.componentRef.setInput('showCol3HeaderText', false); + fixture.detectChanges(); + + await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([ + 'Column1', + 'Column2', + '', + ]); + }); +}); diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts new file mode 100644 index 0000000000..4087d658fd --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts @@ -0,0 +1,59 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyQueryableComponentHarness } from '@skyux/core/testing'; + +import { SkyAgGridWrapperHarness } from '../ag-grid-wrapper/ag-grid-wrapper-harness'; + +import { SkyAgGridHarnessFilters } from './ag-grid-harness.filters'; + +/** + * Harness for interacting with SKY UX AG Grid components in tests. + */ +export class SkyAgGridHarness extends SkyQueryableComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-ag-grid'; + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyAgGridHarness` that meets certain criteria + */ + public static with( + filters: SkyAgGridHarnessFilters, + ): HarnessPredicate { + return SkyAgGridHarness.getDataSkyIdPredicate(filters); + } + + /** + * Checks whether the grid is ready. + */ + public async isGridReady(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.isGridReady()) + .catch(() => false); + } + + /** + * Retrieves the IDs of the currently displayed columns. + */ + public async getDisplayedColumnIds(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.getDisplayedColumnIds()) + .catch(() => Promise.reject('Unable to retrieve displayed column IDs.')); + } + + /** + * Retrieves the header names of the currently displayed columns. + */ + public async getDisplayedColumnHeaderNames(): Promise { + return await this.#getGridWrapper() + .then(async (grid) => await grid.getDisplayedColumnHeaderNames()) + .catch(() => + Promise.reject('Unable to retrieve displayed column header names.'), + ); + } + + async #getGridWrapper(): Promise { + return await this.queryHarness(SkyAgGridWrapperHarness); + } +} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html new file mode 100644 index 0000000000..21fc61885e --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html @@ -0,0 +1,151 @@ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} +

Grid with multiselect enabled

+ + + + + + + +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (showCol3()) { + + } + } + +} +

Selected rows: {{ selectedRowIds | json }}

+ +

Grid with row delete

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + + @if (row.id) { + + + + + + + + } + + + + + @if (showCol3()) { + + } @else { + + } + } + +} + + This is a numeric column. Click here to learn more. + + +

Grid w/ aligned columns and inline help

+ +@if (showAllGrids()) { + + @if (showAllColumns()) { + + + @if (showCol3()) { + + } + } + +} diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts new file mode 100644 index 0000000000..c5509f237a --- /dev/null +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts @@ -0,0 +1,114 @@ +import { JsonPipe } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + inject, + input, + model, +} from '@angular/core'; +import { + SkyAgGridColumnComponent, + SkyAgGridComponent, + SkyAgGridRowDeleteCancelArgs, + SkyAgGridRowDeleteConfirmArgs, +} from '@skyux/ag-grid'; +import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers'; + +interface RowModel { + id: string; + column1: string; + column2: string; + column3: boolean; + myId?: string; +} + +@Component({ + selector: 'app-ag-grid-test', + templateUrl: './ag-grid-test.component.html', + imports: [ + SkyAgGridComponent, + SkyAgGridColumnComponent, + SkyPopoverModule, + SkyDropdownModule, + JsonPipe, + ], +}) +export class AgGridTestComponent { + public dataForRowDeleteGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGrid: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + { id: '3', column1: '11', column2: 'Banana', column3: true }, + { id: '4', column1: '12', column2: 'Daikon', column3: false }, + { id: '5', column1: '13', column2: 'Edamame', column3: true }, + { id: '6', column1: '20', column2: 'Fig', column3: false }, + { id: '7', column1: '21', column2: 'Grape', column3: true }, + ]; + + public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [ + { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' }, + { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' }, + { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' }, + { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' }, + { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' }, + { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' }, + { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' }, + ]; + + public removeRowIds: string[] = []; + public rowHighlightedId: string | undefined; + public readonly selectedRowIds = model([]); + public visibleColumnIds: string[] = []; + + public page = 1; + public pageSize = 0; + public pageQueryParam = ''; + + protected readonly showAllColumns = input(true); + protected readonly showAllGrids = input(true); + protected readonly showCol3 = input(true); + protected readonly showCol3HeaderText = input(true); + + readonly #cdr = inject(ChangeDetectorRef); + + public selectAll(): void { + this.selectedRowIds.set( + (this.dataForSimpleGridWithMultiselect ?? []).map( + (item) => item.myId as string, + ), + ); + this.#cdr.markForCheck(); + } + + public clearAll(): void { + this.selectedRowIds.set([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public cancelRowDelete(_cancelArgs: SkyAgGridRowDeleteCancelArgs): void { + // noop + } + + public deleteItem(id: string): void { + this.removeRowIds = [id, ...this.removeRowIds]; + } + + public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void { + this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter( + (data: RowModel) => data.id !== confirmArgs.id, + ); + } + + public selectRow(): void { + this.selectedRowIds.set(['101', '103', '105']); + } +} diff --git a/libs/components/ag-grid/testing/src/public-api.ts b/libs/components/ag-grid/testing/src/public-api.ts new file mode 100644 index 0000000000..59d806d6bd --- /dev/null +++ b/libs/components/ag-grid/testing/src/public-api.ts @@ -0,0 +1 @@ +export const greeting = 'Hello World!'; diff --git a/libs/components/ag-grid/testing/tsconfig.spec.json b/libs/components/ag-grid/testing/tsconfig.spec.json new file mode 100644 index 0000000000..aad2a9f430 --- /dev/null +++ b/libs/components/ag-grid/testing/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.spec.json", + "compilerOptions": { + "outDir": "../../../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/components/code-examples/src/index.ts b/libs/components/code-examples/src/index.ts index 78af68d8e0..3803096f10 100644 --- a/libs/components/code-examples/src/index.ts +++ b/libs/components/code-examples/src/index.ts @@ -2,6 +2,7 @@ export { ActionBarsSummaryActionBarBasicExampleComponent } from './lib/modules/a export { ActionBarsSummaryActionBarErrorExampleComponent } from './lib/modules/action-bars/summary-action-bar/error/example.component'; export { ActionBarsSummaryActionBarModalExampleComponent } from './lib/modules/action-bars/summary-action-bar/modal/example.component'; export { ActionBarsSummaryActionBarTabExampleComponent } from './lib/modules/action-bars/summary-action-bar/tab/example.component'; +export { AgGridBasicExampleComponent } from './lib/modules/ag-grid/grid/basic/example.component'; export { AgGridDataEntryGridBasicExampleComponent } from './lib/modules/ag-grid/data-entry-grid/basic/example.component'; export { AgGridDataEntryGridDataManagerAddedExampleComponent } from './lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component'; export { AgGridDataEntryGridFocusExampleComponent } from './lib/modules/ag-grid/data-entry-grid/focus/example.component'; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html new file mode 100644 index 0000000000..1250f12398 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts new file mode 100644 index 0000000000..5dde61d98e --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts @@ -0,0 +1,36 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { SkyDropdownModule } from '@skyux/popovers'; + +import { AgGridDemoRow } from './data'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SkyDropdownModule], +}) +export class ContextMenuComponent { + public readonly row = input(); + + protected readonly contextMenuAriaLabel = computed( + () => `Context menu for ${this.row()?.name}`, + ); + protected readonly deleteAriaLabel = computed( + () => `Delete ${this.row()?.name}`, + ); + protected readonly markInactiveAriaLabel = computed( + () => `Mark ${this.row()?.name} inactive`, + ); + protected readonly moreInfoAriaLabel = computed( + () => `More info for ${this.row()?.name}`, + ); + + protected actionClicked(action: string): void { + alert(`${action} clicked for ${this.row()?.name}`); + } +} diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts new file mode 100644 index 0000000000..6e74f8d51a --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts @@ -0,0 +1,157 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + id: string; + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA = [ + { + id: '1', + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + id: '2', + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + id: '3', + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + id: '4', + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + id: '5', + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + id: '6', + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + id: '7', + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html new file mode 100644 index 0000000000..c4eed9302c --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html @@ -0,0 +1,15 @@ + + + + + + + + + + {{ row.department?.name }} + + + {{ row.jobTitle?.name }} + + diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts new file mode 100644 index 0000000000..e83eda4115 --- /dev/null +++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { SkyAgGridColumnComponent, SkyAgGridComponent } from '@skyux/ag-grid'; + +import { ContextMenuComponent } from './context-menu.component'; +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +/** + * @title Easy grid™️ + */ +@Component({ + selector: 'app-ag-grid-basic-example', + templateUrl: './example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ContextMenuComponent, SkyAgGridComponent, SkyAgGridColumnComponent], +}) +export class AgGridBasicExampleComponent { + protected gridData: AgGridDemoRow[] = AG_GRID_DEMO_DATA; +} diff --git a/tsconfig.base.json b/tsconfig.base.json index d382b60fcd..5e433ddf0b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -45,6 +45,9 @@ "libs/components/action-bars/testing/src/public-api.ts" ], "@skyux/ag-grid": ["libs/components/ag-grid/src/index.ts"], + "@skyux/ag-grid/testing": [ + "libs/components/ag-grid/testing/src/public-api.ts" + ], "@skyux/angular-tree-component": [ "libs/components/angular-tree-component/src/index.ts" ], From c62a986bf9da2ce56cd278e2ebbdfc6d0c8a497b Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:31:18 -0400 Subject: [PATCH 02/54] Add tests --- libs/components/ag-grid/package.json | 11 +-- .../fixtures/ag-grid-test.component.html | 4 ++ .../fixtures/ag-grid-test.component.ts | 1 + .../sky-ag-grid/sky-ag-grid.component.spec.ts | 48 +++++++++++++ .../sky-ag-grid/sky-ag-grid.component.ts | 72 ++++++++++++------- .../fixtures/ag-grid-test.component.html | 10 ++- .../fixtures/ag-grid-test.component.ts | 9 +-- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/libs/components/ag-grid/package.json b/libs/components/ag-grid/package.json index 88e7a6a6d0..fc78cba79f 100644 --- a/libs/components/ag-grid/package.json +++ b/libs/components/ag-grid/package.json @@ -21,26 +21,29 @@ } }, "peerDependencies": { + "@angular/cdk": "^20.2.3", "@angular/common": "^20.3.0", "@angular/core": "^20.3.0", "@angular/forms": "^20.3.0", + "@angular/router": "^20.3.0", "@skyux/autonumeric": "0.0.0-PLACEHOLDER", "@skyux/core": "0.0.0-PLACEHOLDER", "@skyux/data-manager": "0.0.0-PLACEHOLDER", "@skyux/datetime": "0.0.0-PLACEHOLDER", "@skyux/forms": "0.0.0-PLACEHOLDER", + "@skyux/help-inline": "0.0.0-PLACEHOLDER", "@skyux/i18n": "0.0.0-PLACEHOLDER", "@skyux/icon": "0.0.0-PLACEHOLDER", "@skyux/indicators": "0.0.0-PLACEHOLDER", "@skyux/layout": "0.0.0-PLACEHOLDER", + "@skyux/lists": "0.0.0-PLACEHOLDER", "@skyux/lookup": "0.0.0-PLACEHOLDER", "@skyux/popovers": "0.0.0-PLACEHOLDER", "@skyux/theme": "0.0.0-PLACEHOLDER", "ag-grid-angular": "^34.1.2", - "ag-grid-community": "^34.1.2" - }, - "dependencies": { - "tslib": "^2.8.1" + "ag-grid-community": "^34.1.2", + "rxjs": "7.8.2" }, + "dependencies": {}, "sideEffects": false } diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html index 9333e34bc3..7512c52397 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html @@ -4,6 +4,7 @@ height="200" width="600" [data]="dataForSimpleGrid" + [displayedColumns]="displayedColumns()" [page]="page" [pageQueryParam]="pageQueryParam" [pageSize]="pageSize" @@ -44,6 +45,7 @@

Grid with multiselect enabled

fit="scroll" multiselectRowId="myId" [data]="dataForSimpleGridWithMultiselect" + [displayedColumns]="displayedColumns()" [(selectedRowIds)]="selectedRowIds" > @if (showAllColumns()) { @@ -68,6 +70,7 @@

Grid with row delete

@@ -125,6 +128,7 @@

Grid w/ aligned columns and inline help

data-sky-id="inline-help-grid" fit="scroll" [data]="dataForSimpleGrid" + [displayedColumns]="displayedColumns()" > @if (showAllColumns()) { ([]); public readonly removeRowIds = model([]); public readonly rowHighlightedId = model(); public readonly selectedRowIds = model([]); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts index 51586cfcea..8e1a351459 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts @@ -95,6 +95,54 @@ describe('SkyAgGridComponent', () => { ).toEqual(0); }); + it('should respond to displayedColumns input', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + expect(component).toBeTruthy(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid + + fixture.componentRef.setInput('displayedColumns', ['column1', 'column2']); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 2); // 4 grids with 2 columns each + expect(component.visibleColumnIds()).toEqual(['column1', 'column2']); + + fixture.componentRef.setInput('displayedColumns', [ + 'column1', + 'column2', + 'column3', + ]); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(4 * 3); // 4 grids with 3 columns each + expect(component.visibleColumnIds()).toEqual([ + 'column1', + 'column2', + 'column3', + ]); + + fixture.componentRef.setInput('showAllColumns', false); + fixture.detectChanges(); + await fixture.whenStable(); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent)) + .length, + ).toEqual(0); + expect( + fixture.debugElement.queryAll(By.directive(SkyAgGridWrapperComponent)) + .length, + ).toEqual(0); + }); + it('should handle empty data', async () => { component.dataForSimpleGrid = undefined; fixture.detectChanges(); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts index fcca4cf17f..dee0f9e2d6 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts @@ -1,7 +1,12 @@ +import { + coerceArray, + coerceBooleanProperty, + coerceNumberProperty, + coerceStringArray, +} from '@angular/cdk/coercion'; import { ChangeDetectionStrategy, Component, - booleanAttribute, computed, contentChildren, effect, @@ -9,7 +14,6 @@ import { input, linkedSignal, model, - numberAttribute, output, signal, untracked, @@ -28,6 +32,7 @@ import { GridApi, GridOptions, GridPreDestroyedEvent, + IRowNode, ModuleRegistry, SelectionChangedEvent, } from 'ag-grid-community'; @@ -93,7 +98,9 @@ export class SkyAgGridComponent< /** * The columns to display by default based on the ID or field of the item. */ - public readonly displayedColumns = input([]); + public readonly displayedColumns = input([], { + transform: coerceStringArray, + }); /** * Fires when columns change. This includes changes to the displayed columns and changes @@ -105,7 +112,9 @@ export class SkyAgGridComponent< /** * The columns to hide by default based on the ID or field of the item. */ - public readonly hiddenColumns = input([]); + public readonly hiddenColumns = input([], { + transform: coerceStringArray, + }); /** * Whether to enable the multiselect feature to display a column of @@ -115,7 +124,7 @@ export class SkyAgGridComponent< * @default false */ public readonly enableMultiselect = input(false, { - transform: booleanAttribute, + transform: coerceBooleanProperty, }); /** @@ -131,7 +140,7 @@ export class SkyAgGridComponent< * The height of the grid. */ public readonly height = input(0, { - transform: numberAttribute, + transform: coerceNumberProperty, }); /** @@ -145,7 +154,7 @@ export class SkyAgGridComponent< * @default 1 */ public readonly page = input(1, { - transform: numberAttribute, + transform: coerceNumberProperty, }); /** @@ -153,7 +162,7 @@ export class SkyAgGridComponent< * @default 0 */ public readonly pageSize = input(0, { - transform: numberAttribute, + transform: coerceNumberProperty, }); /** @@ -165,7 +174,7 @@ export class SkyAgGridComponent< * The width of the grid in pixels. */ public readonly width = input(0, { - transform: numberAttribute, + transform: coerceNumberProperty, }); /** @@ -225,11 +234,9 @@ export class SkyAgGridComponent< fromEvent(api, 'selectionChanged').pipe( takeUntil(this.#gridDestroyed), map((selection) => - ( - (selection.selectedNodes ?? []) - .map((node) => node.id as string) - .filter(Boolean) as string[] - ).sort((a, b) => a.localeCompare(b)), + this.#getRowIds(selection.selectedNodes).sort((a, b) => + a.localeCompare(b), + ), ), distinctUntilChanged(arrayIsEqual), ), @@ -263,11 +270,11 @@ export class SkyAgGridComponent< helpPopoverTitle: col.helpPopoverTitle(), helpPopoverContent: col.helpPopoverContent() || col.description(), }, - initialHide: + hide: col.hidden() || (displayed.length > 0 && - !displayed.includes(col.id() || col.field() || '')) || - hidden.includes(col.id() || col.field() || ''), + !displayed.includes(this.#getColumnIdOrField(col))) || + hidden.includes(this.#getColumnIdOrField(col)), sortable: col.isSortable(), lockPosition: col.locked(), suppressMovable: col.locked(), @@ -278,17 +285,21 @@ export class SkyAgGridComponent< } else if (col.id()) { colDef.colId = col.id(); } - if (col.cellTemplate()) { - (colDef.type as string[]).push(SkyCellType.Template); - colDef.cellRendererParams = { template: col.cellTemplate() }; - } else if (col.type() === 'date') { + if (col.type() === 'date') { (colDef.type as string[]).push(SkyCellType.Date); + colDef.cellDataType = 'dateString'; } else if (col.type() === 'number') { (colDef.type as string[]).push(SkyCellType.Number); + colDef.cellDataType = 'number'; } else if (col.type() === 'boolean') { colDef.cellDataType = 'boolean'; } else { (colDef.type as string[]).push(SkyCellType.Text); + colDef.cellDataType = 'text'; + } + if (col.cellTemplate()) { + (colDef.type as string[]).push(SkyCellType.Template); + colDef.cellRendererParams = { template: col.cellTemplate() }; } if (col.width() > 0) { colDef.initialWidth = col.width(); @@ -297,6 +308,7 @@ export class SkyAgGridComponent< return colDef; }); }); + readonly #activatedRoute = inject(ActivatedRoute, { optional: true }); readonly #router = inject(Router, { optional: true }); @@ -365,8 +377,7 @@ export class SkyAgGridComponent< const api = this.gridApi(); const selectedRowIds = this.selectedRowIds(); this.data(); - const currentSelectedRowIds = - api?.getSelectedNodes().map((node) => node.id as string) ?? []; + const currentSelectedRowIds = this.#getRowIds(api?.getSelectedNodes()); if (!arrayIsEqual(selectedRowIds, currentSelectedRowIds)) { api?.deselectAll(); selectedRowIds.forEach((rowId) => @@ -410,7 +421,7 @@ export class SkyAgGridComponent< protected pageChange(page: number): void { const pageQueryParam = this.pageQueryParam(); - const pageNumber = numberAttribute(page, 1); + const pageNumber = coerceNumberProperty(page, 1); if ( pageQueryParam && this.#activatedRoute && @@ -429,4 +440,17 @@ export class SkyAgGridComponent< this.pageNumber.set(page); } } + + #getColumnIdOrField(col: SkyAgGridColumnComponent): string { + const id = col.id(); + /* istanbul ignore next */ + const field = col.field() || ''; + return id || field; + } + + #getRowIds(rows: (IRowNode | undefined)[] | null | undefined): string[] { + return coerceArray(rows) + .map((node) => node?.id as string) + .filter(Boolean) as string[]; + } } diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html index 21fc61885e..7512c52397 100644 --- a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html @@ -4,11 +4,12 @@ height="200" width="600" [data]="dataForSimpleGrid" + [displayedColumns]="displayedColumns()" [page]="page" [pageQueryParam]="pageQueryParam" [pageSize]="pageSize" - [rowHighlightedId]="rowHighlightedId" - (selectedColumnIdsChange)="visibleColumnIds = $event" + [rowHighlightedId]="rowHighlightedId()" + (selectedColumnIdsChange)="visibleColumnIds.set($event)" > @if (showAllColumns()) { @@ -44,6 +45,7 @@

Grid with multiselect enabled

fit="scroll" multiselectRowId="myId" [data]="dataForSimpleGridWithMultiselect" + [displayedColumns]="displayedColumns()" [(selectedRowIds)]="selectedRowIds" > @if (showAllColumns()) { @@ -60,7 +62,7 @@

Grid with multiselect enabled

}
} -

Selected rows: {{ selectedRowIds | json }}

+

Selected rows: {{ selectedRowIds() | json }}

Grid with row delete

@@ -68,6 +70,7 @@

Grid with row delete

@@ -125,6 +128,7 @@

Grid w/ aligned columns and inline help

data-sky-id="inline-help-grid" fit="scroll" [data]="dataForSimpleGrid" + [displayedColumns]="displayedColumns()" > @if (showAllColumns()) { ([]); + public readonly removeRowIds = model([]); + public readonly rowHighlightedId = model(); public readonly selectedRowIds = model([]); - public visibleColumnIds: string[] = []; + public readonly visibleColumnIds = model([]); public page = 1; public pageSize = 0; @@ -99,7 +100,7 @@ export class AgGridTestComponent { } public deleteItem(id: string): void { - this.removeRowIds = [id, ...this.removeRowIds]; + this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]); } public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void { From 19a1b98b424768146ae7f7f7dce3a913c38892d6 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Sat, 1 Nov 2025 01:29:41 -0400 Subject: [PATCH 03/54] Fix test coverage, linting --- .../data-grid/basic/grid.component.html | 2 +- .../grids/basic/grid.component.html | 20 +++---- .../components/grids/basic/grid.component.ts | 3 +- .../list-view-grid.component.html | 54 +++++++------------ .../list-view-grid.component.ts | 2 - .../fixtures/ag-grid-test.component.html | 2 +- .../sky-ag-grid-column.component.ts | 2 +- .../sky-ag-grid/sky-ag-grid.component.spec.ts | 34 ++++++++++++ .../sky-ag-grid/sky-ag-grid.component.ts | 11 ++-- .../fixtures/ag-grid-test.component.html | 2 +- .../ag-grid/grid/basic/example.component.html | 2 +- 11 files changed, 75 insertions(+), 59 deletions(-) diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.html b/apps/playground/src/app/components/data-grid/basic/grid.component.html index 6228918dc1..8bd3e3d58c 100644 --- a/apps/playground/src/app/components/data-grid/basic/grid.component.html +++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html @@ -103,7 +103,7 @@

Grid with row delete

(rowDeleteConfirm)="finishRowDelete($event)" > Grid with row delete > - - - - - - - + @if (row.id) { + + + + + + + + } - - + - + /> - + /> - + /> @@ -54,35 +50,31 @@ (selectedIdsChange)="onSelectedIdsChange($event)" > - - + - + /> - + /> - + /> @@ -95,16 +87,15 @@

List-view-grid with inline delete

> - - + List-view-grid with inline delete > - + @if (row.id) { + + } List-view-grid with inline delete field="column1" heading="Column1" [locked]="true" - > - + /> - + /> - + /> diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts index f1f5b15548..0c24293e7c 100644 --- a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts +++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { SkyAlertModule } from '@skyux/indicators'; import { SkyListModule, SkyListToolbarModule } from '@skyux/list-builder'; @@ -18,7 +17,6 @@ import { BehaviorSubject, Subject } from 'rxjs'; standalone: true, templateUrl: './list-view-grid.component.html', imports: [ - CommonModule, SkyAlertModule, SkyDropdownModule, SkyListModule, diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html index 7512c52397..77c141dc16 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html @@ -76,7 +76,7 @@

Grid with row delete

> @if (showAllColumns()) { (); public readonly field = input(); public readonly heading = input(); - public readonly id = input(); + public readonly columnId = input(); /** * The title of the help popover. This property only applies when `helpPopoverContent` is diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts index 8e1a351459..2f6efe28f8 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts @@ -161,6 +161,40 @@ describe('SkyAgGridComponent', () => { ).toHaveSize(0); }); + it('should handle data changing from populated to undefined', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector('[data-sky-id="grid"] ag-grid-angular'), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(7); + + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(0); + }); + + it('should handle data changing from undefined to populated', async () => { + component.dataForSimpleGrid = undefined; + fixture.detectChanges(); + await fixture.whenStable(); + const api = getGridApi( + fixture.nativeElement.querySelector('[data-sky-id="grid"] ag-grid-angular'), + ); + expect(api).toBeTruthy(); + expect(api?.getDisplayedRowCount()).toBe(0); + + component.dataForSimpleGrid = [ + { id: '1', column1: '1', column2: 'Apple', column3: true }, + { id: '2', column1: '01', column2: 'Banana', column3: false }, + ]; + fixture.detectChanges(); + await fixture.whenStable(); + expect(api?.getDisplayedRowCount()).toBe(2); + }); + it('should select all rows', async () => { fixture.detectChanges(); expect(component).toBeTruthy(); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts index dee0f9e2d6..df274d41b5 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts @@ -234,9 +234,7 @@ export class SkyAgGridComponent< fromEvent(api, 'selectionChanged').pipe( takeUntil(this.#gridDestroyed), map((selection) => - this.#getRowIds(selection.selectedNodes).sort((a, b) => - a.localeCompare(b), - ), + arraySorted(this.#getRowIds(selection.selectedNodes)), ), distinctUntilChanged(arrayIsEqual), ), @@ -282,8 +280,8 @@ export class SkyAgGridComponent< }; if (col.field()) { colDef.field = col.field(); - } else if (col.id()) { - colDef.colId = col.id(); + } else if (col.columnId()) { + colDef.colId = col.columnId(); } if (col.type() === 'date') { (colDef.type as string[]).push(SkyCellType.Date); @@ -442,8 +440,7 @@ export class SkyAgGridComponent< } #getColumnIdOrField(col: SkyAgGridColumnComponent): string { - const id = col.id(); - /* istanbul ignore next */ + const id = col.columnId(); const field = col.field() || ''; return id || field; } diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html index 7512c52397..77c141dc16 100644 --- a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html +++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html @@ -76,7 +76,7 @@

Grid with row delete

> @if (showAllColumns()) { - + From 7360dfd93efa79f2952cd0f281559739a92cdfab Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:42:51 -0500 Subject: [PATCH 04/54] Prettier, address peer issue --- .skyuxdev.json | 1 + .../ag-grid/ag-grid-data-manager-adapter.directive.ts | 11 +++++++++-- .../cell-editor-datepicker.component.ts | 3 +-- .../ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts | 8 ++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.skyuxdev.json b/.skyuxdev.json index ca42d8a37f..f6f1c54089 100644 --- a/.skyuxdev.json +++ b/.skyuxdev.json @@ -2,6 +2,7 @@ "baseBranch": "main", "releaseBranch": "release-please--branches--{baseBranch}--components--skyux", "implicitPeerDependencies": { + "ag-grid": ["rxjs"], "stylelint-config-skyux": [ "skyux-stylelint", "stylelint-config-recommended-scss" diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts index eda1f9202f..dfb6734ad4 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts @@ -27,8 +27,15 @@ import { IColumnLimit, RowSelectedEvent, } from 'ag-grid-community'; -import { Subject, Subscription, fromEvent, of, switchMap } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; +import { + Subject, + Subscription, + filter, + fromEvent, + of, + switchMap, + takeUntil, +} from 'rxjs'; import { SkyAgGridWrapperComponent } from './ag-grid-wrapper.component'; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.ts index 46813d2d69..9d1c1c9e26 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/cell-editors/cell-editor-datepicker/cell-editor-datepicker.component.ts @@ -19,8 +19,7 @@ import { SkyI18nModule } from '@skyux/i18n'; import { SkyThemeService } from '@skyux/theme'; import { ICellEditorAngularComp } from 'ag-grid-angular'; -import { fromEvent } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, fromEvent } from 'rxjs'; import { SkyCellEditorDatepickerParams } from '../../types/cell-editor-datepicker-params'; import { SkyAgGridCellEditorInitialAction } from '../../types/cell-editor-initial-action'; diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts index 2f6efe28f8..795bcdb4a9 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts @@ -165,7 +165,9 @@ describe('SkyAgGridComponent', () => { fixture.detectChanges(); await fixture.whenStable(); const api = getGridApi( - fixture.nativeElement.querySelector('[data-sky-id="grid"] ag-grid-angular'), + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), ); expect(api).toBeTruthy(); expect(api?.getDisplayedRowCount()).toBe(7); @@ -181,7 +183,9 @@ describe('SkyAgGridComponent', () => { fixture.detectChanges(); await fixture.whenStable(); const api = getGridApi( - fixture.nativeElement.querySelector('[data-sky-id="grid"] ag-grid-angular'), + fixture.nativeElement.querySelector( + '[data-sky-id="grid"] ag-grid-angular', + ), ); expect(api).toBeTruthy(); expect(api?.getDisplayedRowCount()).toBe(0); From 8462e389076cb971f4ec29a3656d20a68987b5ee Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:58:59 -0500 Subject: [PATCH 05/54] Updates from swap-grids branch --- .../list-summary/list-summary.component.cy.ts | 4 +- .../ag-grid/ag-grid-wrapper.component.ts | 2 +- .../ag-grid/header/header.component.html | 24 +- .../ag-grid/header/header.component.spec.ts | 52 ++-- .../ag-grid/header/header.component.ts | 229 +++++++++++------- .../fixtures/ag-grid-test.component.html | 8 +- .../sky-ag-grid/sky-ag-grid.component.ts | 42 ++-- .../fixtures/ag-grid-test.component.html | 11 +- 8 files changed, 235 insertions(+), 137 deletions(-) diff --git a/apps/e2e/lists-storybook-e2e/src/e2e/list-summary/list-summary.component.cy.ts b/apps/e2e/lists-storybook-e2e/src/e2e/list-summary/list-summary.component.cy.ts index 9eb2fe5ed3..254e5ee486 100644 --- a/apps/e2e/lists-storybook-e2e/src/e2e/list-summary/list-summary.component.cy.ts +++ b/apps/e2e/lists-storybook-e2e/src/e2e/list-summary/list-summary.component.cy.ts @@ -10,8 +10,8 @@ describe('list-summary', () => { ); it('should render the component', () => { - cy.skyReady(); - cy.get('app-list-summary') + cy.skyReady() + .get('app-list-summary') .should('exist') .should('be.visible') .screenshot(`list-summary-${theme}`); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts index 09f065f58b..39f9f593db 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts @@ -373,7 +373,7 @@ export class SkyAgGridWrapperComponent const skyAgGridTheme = getSkyAgGridTheme( hasEditableClass ? 'data-entry-grid' : 'data-grid', ); - this.agGrid?.api.setGridOption('theme', skyAgGridTheme); + this.agGrid?.api?.setGridOption('theme', skyAgGridTheme); const skyAgGridThemeClassName = getSkyAgGridThemeClassName( hasEditableClass, themeSettings, diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html index 511e2b7f5d..ba716a870b 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html @@ -1,5 +1,5 @@