diff --git a/package.json b/package.json index a1094a0..3d744b9 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@angular/common": ">=4.3.6", "@angular/core": ">=4.3.6", "@angular/forms": ">=4.3.6", + "@skyux/core": "^3.5.1", "@skyux/i18n": "^3.5.0", "@skyux/list-builder-common": "^3.0.0", "@skyux/forms": "^3.0.0", diff --git a/src/app/public/modules/grid/fixtures/grid-async.component.fixture.html b/src/app/public/modules/grid/fixtures/grid-async.component.fixture.html index c8c8168..6d8923f 100644 --- a/src/app/public/modules/grid/fixtures/grid-async.component.fixture.html +++ b/src/app/public/modules/grid/fixtures/grid-async.component.fixture.html @@ -5,4 +5,9 @@ [heading]="asyncHeading | async" [description]="asyncDescription | async"> + + diff --git a/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.html b/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.html index a129d3b..d6977ad 100644 --- a/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.html +++ b/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.html @@ -1,3 +1,9 @@ - + {{value}} diff --git a/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.ts b/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.ts index 86e9577..34f00fa 100644 --- a/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.ts +++ b/src/app/public/modules/grid/fixtures/grid-empty.component.fixture.ts @@ -21,6 +21,8 @@ export class GridEmptyTestComponent { public template: TemplateRef; public columns: Array; + public selectedColumnIds: string[]; + public settingsKey: string; public data: any[] = [ { diff --git a/src/app/public/modules/grid/fixtures/grid-fixtures.module.ts b/src/app/public/modules/grid/fixtures/grid-fixtures.module.ts index 2f53e3a..5ed8992 100644 --- a/src/app/public/modules/grid/fixtures/grid-fixtures.module.ts +++ b/src/app/public/modules/grid/fixtures/grid-fixtures.module.ts @@ -7,12 +7,17 @@ import { } from '@angular/common'; import { + SkyUIConfigService, SkyWindowRefService } from '@skyux/core'; +import { + Observable +} from 'rxjs/Observable'; + import { SkyGridModule -} from '../'; +} from '../grid.module'; import { GridTestComponent @@ -47,7 +52,16 @@ import { SkyGridModule ], providers: [ - SkyWindowRefService + SkyWindowRefService, + { + provide: SkyUIConfigService, + useValue: { + getConfig: () => Observable.of({ + selectedColumnIds: [] + }), + setConfig: () => Observable.of({}) + } + } ], exports: [ GridTestComponent, diff --git a/src/app/public/modules/grid/fixtures/grid.component.fixture.html b/src/app/public/modules/grid/fixtures/grid.component.fixture.html index 2c84eb3..e5d22db 100644 --- a/src/app/public/modules/grid/fixtures/grid.component.fixture.html +++ b/src/app/public/modules/grid/fixtures/grid.component.fixture.html @@ -10,6 +10,7 @@ [messageStream]="gridController" [multiselectRowId]="multiselectRowId" [rowHighlightedId]="rowHighlightedId" + [settingsKey]="settingsKey" [sortField]="sortField" (sortFieldChange)="onSort($event)" (columnWidthChange)="onResize($event)" diff --git a/src/app/public/modules/grid/fixtures/grid.component.fixture.ts b/src/app/public/modules/grid/fixtures/grid.component.fixture.ts index 184f6a3..a827aa7 100644 --- a/src/app/public/modules/grid/fixtures/grid.component.fixture.ts +++ b/src/app/public/modules/grid/fixtures/grid.component.fixture.ts @@ -53,6 +53,7 @@ export class GridTestComponent { public selectedRowsChange: SkyGridSelectedRowsModelChange; public gridController = new Subject(); public rowHighlightedId: string; + public settingsKey: string; public selectedColumnIds: string[] = [ 'column1', diff --git a/src/app/public/modules/grid/grid.component.spec.ts b/src/app/public/modules/grid/grid.component.spec.ts index eb7dd5d..b598962 100644 --- a/src/app/public/modules/grid/grid.component.spec.ts +++ b/src/app/public/modules/grid/grid.component.spec.ts @@ -19,10 +19,18 @@ import { tick } from '@angular/core/testing'; +import { + SkyUIConfigService +} from '@skyux/core'; + import { DragulaService } from 'ng2-dragula/ng2-dragula'; +import { + Observable +} from 'rxjs/Observable'; + const moment = require('moment'); import { @@ -54,7 +62,6 @@ import { } from './fixtures/mock-dragula.service'; import { - SkyGridModule, SkyGridComponent, SkyGridColumnModel } from './'; @@ -207,8 +214,7 @@ describe('Grid Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); })); @@ -590,7 +596,7 @@ describe('Grid Component', () => { }); }); - describe('Resiazable columns', () => { + describe('Resizeable columns', () => { it('should not resize if user does not use resize handle', fakeAsync(() => { // Get initial baseline for comparison. @@ -825,8 +831,7 @@ describe('Grid Component', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); }); @@ -946,8 +951,7 @@ describe('Grid Component', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); }); @@ -1266,8 +1270,7 @@ describe('Grid Component', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); }); @@ -1323,8 +1326,7 @@ describe('Grid Component', () => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); @@ -1552,8 +1554,7 @@ describe('Grid Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); })); @@ -1639,8 +1640,7 @@ describe('Grid Component', () => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }); @@ -1674,8 +1674,7 @@ describe('Grid Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - GridFixturesModule, - SkyGridModule + GridFixturesModule ] }).compileComponents(); })); @@ -1685,20 +1684,23 @@ describe('Grid Component', () => { element = fixture.debugElement as DebugElement; }); - it('should handle async column headings', fakeAsync(() => { - fixture.detectChanges(); - - expect(getColumnHeader('column1', element).nativeElement.textContent.trim()).toBe(''); + function verifyColumnHeaders(id: string) { + expect(getColumnHeader(id, element).nativeElement.textContent.trim()).toBe(''); tick(110); // wait for setTimeout fixture.detectChanges(); tick(); - expect(getColumnHeader('column1', element).nativeElement.textContent.trim()).toBe('Column1'); - })); + expect(getColumnHeader(id, element).nativeElement.textContent.trim()).toBe('Column1'); + } it('should handle async column headings', fakeAsync(() => { fixture.detectChanges(); + verifyColumnHeaders('column1'); + })); + + it('should handle async column descriptions', fakeAsync(() => { + fixture.detectChanges(); let col1 = fixture.componentInstance.grid.columns.find(col => col.id === 'column1'); expect(col1.description).toBe(''); @@ -1709,5 +1711,117 @@ describe('Grid Component', () => { expect(col1.description).toBe('Column1 Description'); })); + + it('should support the item `field` property if `id` not provided', fakeAsync(() => { + fixture.detectChanges(); + + verifyColumnHeaders('column2'); + })); + }); + + describe('UI config', () => { + let fixture: ComponentFixture; + let component: GridEmptyTestComponent; + let uiConfigService: SkyUIConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + GridFixturesModule + ] + }); + + fixture = TestBed.createComponent(GridEmptyTestComponent); + component = fixture.componentInstance; + uiConfigService = TestBed.get(SkyUIConfigService); + }); + + it('should call the UI config service when selected columns change', () => { + component.columns = [ + new SkyGridColumnModel(component.template, { + id: 'column1', + heading: 'Column 1' + }), + new SkyGridColumnModel(component.template, { + id: 'column2', + heading: 'Column 2' + }) + ]; + + fixture.detectChanges(); + + const spy = spyOn(uiConfigService, 'setConfig').and.callThrough(); + + component.settingsKey = 'foobar'; + component.selectedColumnIds = ['column1', 'column2']; + fixture.detectChanges(); + + expect(spy.calls.count()).toEqual(1); + + spy.calls.reset(); + component.selectedColumnIds = ['column2', 'column1']; + fixture.detectChanges(); + + expect(spy.calls.count()).toEqual(1); + }); + + it('should fetch UI config on init', () => { + const spy = spyOn(uiConfigService, 'getConfig').and.callThrough(); + + component.settingsKey = 'foobar'; + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledWith('foobar'); + }); + + it('should handle errors when setting config', () => { + const spy = spyOn(console, 'warn'); + + spyOn(uiConfigService, 'setConfig').and.callFake(() => { + return Observable.throw(new Error()); + }); + + component.columns = [ + new SkyGridColumnModel(component.template, { + id: 'column1', + heading: 'Column 1' + }), + new SkyGridColumnModel(component.template, { + id: 'column2', + heading: 'Column 2' + }) + ]; + + fixture.detectChanges(); + + component.settingsKey = 'foobar'; + component.selectedColumnIds = ['column1', 'column2']; + fixture.detectChanges(); + + expect(spy).toHaveBeenCalledWith('Could not save grid settings.'); + }); + + it('should suppress errors when getting config', () => { + spyOn(uiConfigService, 'getConfig').and.callFake(() => { + return Observable.throw(new Error()); + }); + + const spy = spyOn(fixture.componentInstance.grid as any, 'initColumns').and.callThrough(); + + component.columns = [ + new SkyGridColumnModel(component.template, { + id: 'column1', + heading: 'Column 1' + }) + ]; + + component.settingsKey = 'foobar'; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(spy).toHaveBeenCalled(); + }); + }); }); }); diff --git a/src/app/public/modules/grid/grid.component.ts b/src/app/public/modules/grid/grid.component.ts index 97cd56e..9a5ec92 100644 --- a/src/app/public/modules/grid/grid.component.ts +++ b/src/app/public/modules/grid/grid.component.ts @@ -17,6 +17,10 @@ import { OnInit } from '@angular/core'; +import { + SkyUIConfigService +} from '@skyux/core'; + import { Observable } from 'rxjs/Observable'; @@ -80,6 +84,10 @@ import { SkyGridMessageType } from './types'; +import { + SkyGridUIConfig +} from './types/grid-ui-config'; + import '../../polyfills'; let nextId = 0; @@ -134,6 +142,9 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On @Input() public rowHighlightedId: string; + @Input() + public settingsKey: string; + @Output() public selectedColumnIdsChange = new EventEmitter>(); @@ -184,7 +195,8 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On private dragulaService: DragulaService, private ref: ChangeDetectorRef, private gridAdapter: SkyGridAdapterService, - private skyWindow: SkyWindowRefService + private skyWindow: SkyWindowRefService, + private uiConfigService: SkyUIConfigService ) { this.displayedColumns = new Array(); this.items = new Array(); @@ -203,38 +215,15 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On } public ngAfterContentInit() { - if (this.columnComponents.length !== 0 || this.columns !== undefined) { - /* istanbul ignore else */ - /* sanity check */ - if (this.columnComponents.length > 0) { - this.getColumnsFromComponent(); - } - - this.transformData(); - this.setDisplayedColumns(true); + if (this.settingsKey) { + this.applyUserConfig().then(() => { + this.initColumns(); + }); + } else { + this.initColumns(); } - // Watch for added/removed columns: - this.subscriptions.push( - this.columnComponents.changes.subscribe(() => this.updateColumns()) - ); - - // Watch for column heading changes: - this.columnComponents.forEach((comp: SkyGridColumnComponent) => { - this.subscriptions.push( - comp.headingModelChanges - .subscribe((change: SkyGridColumnHeadingModelChange) => { - this.updateColumnHeading(change); - }) - ); - this.subscriptions.push( - comp.descriptionModelChanges - .subscribe((change: SkyGridColumnDescriptionModelChange) => { - this.updateColumnDescription(change); - }) - ); - }); - + // Setup column drag-and-drop. this.gridAdapter.initializeDragAndDrop( this.dragulaService, (selectedColumnIds: Array) => { @@ -243,23 +232,36 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On ); } - // Do an ngOnChanges where changes to selectedColumnIds and data are watched public ngOnChanges(changes: SimpleChanges) { - if (changes['columns'] && this.columns) { - this.setDisplayedColumns(true); - } else if (changes['selectedColumnIds'] && this.columns) { + if ( + changes.selectedColumnIds && + changes.selectedColumnIds.firstChange === false + ) { this.setDisplayedColumns(); - if (changes['selectedColumnIds'].previousValue !== changes['selectedColumnIds'].currentValue) { + + this.setUserConfig({ + selectedColumnIds: this.selectedColumnIds + }); + + /* istanbul ignore else */ + if ( + changes.selectedColumnIds.previousValue !== + changes.selectedColumnIds.currentValue + ) { this.selectedColumnIdsChange.emit(this.selectedColumnIds); this.resetTableWidth(); } } - if (changes['data'] && this.data) { + if (changes.columns && this.columns) { + this.setDisplayedColumns(true); + } + + if (changes.data && this.data) { this.transformData(); } - if (changes['sortField']) { + if (changes.sortField) { this.setSortHeaders(); } } @@ -493,6 +495,7 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On } public onRowClick(event: any, selectedItem: ListItemModel) { + /* istanbul ignore else */ if (this.enableMultiselect) { if (event.target === event.currentTarget || !this.isInteractiveElement(event)) { selectedItem.isSelected = !selectedItem.isSelected; @@ -548,6 +551,10 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On columnId => this.columns.filter(column => column.id === columnId)[0] ); + this.setUserConfig({ + selectedColumnIds: this.selectedColumnIds + }); + // mark for check because we are using ChangeDetectionStrategy.onPush this.ref.markForCheck(); } @@ -772,4 +779,80 @@ export class SkyGridComponent implements OnInit, AfterContentInit, OnChanges, On summary`; return event.target.closest(interactiveElSelectors); } + + private applyUserConfig(): Promise { + return new Promise((resolve) => { + this.uiConfigService.getConfig(this.settingsKey) + .take(1) + .subscribe((config) => { + /* istanbul ignore else */ + if (config && config.selectedColumnIds) { + this.selectedColumnIds = config.selectedColumnIds; + this.ref.markForCheck(); + } + + resolve(); + }, () => { + resolve(); + }); + }); + } + + private setUserConfig(config: SkyGridUIConfig): void { + if (!this.settingsKey) { + return; + } + + this.uiConfigService.setConfig( + this.settingsKey, + config + ) + .takeUntil(this.ngUnsubscribe) + .subscribe( + () => { }, + (err) => { + console.warn('Could not save grid settings.'); + console.warn(err); + } + ); + } + + private initColumns(): void { + /* istanbul ignore else */ + if ( + this.columnComponents.length !== 0 || + this.columns !== undefined + ) { + /* istanbul ignore else */ + /* sanity check */ + if (this.columnComponents.length > 0) { + this.getColumnsFromComponent(); + } + + this.transformData(); + this.setDisplayedColumns(true); + this.ref.markForCheck(); + } + + // Watch for added/removed columns: + this.subscriptions.push( + this.columnComponents.changes.subscribe(() => this.updateColumns()) + ); + + // Watch for column heading changes: + this.columnComponents.forEach((comp: SkyGridColumnComponent) => { + this.subscriptions.push( + comp.headingModelChanges + .subscribe((change: SkyGridColumnHeadingModelChange) => { + this.updateColumnHeading(change); + }) + ); + this.subscriptions.push( + comp.descriptionModelChanges + .subscribe((change: SkyGridColumnDescriptionModelChange) => { + this.updateColumnDescription(change); + }) + ); + }); + } } diff --git a/src/app/public/modules/grid/grid.module.ts b/src/app/public/modules/grid/grid.module.ts index 2990885..0b94954 100644 --- a/src/app/public/modules/grid/grid.module.ts +++ b/src/app/public/modules/grid/grid.module.ts @@ -14,6 +14,10 @@ import { DragulaModule } from 'ng2-dragula/ng2-dragula'; +import { + SkyUIConfigService +} from '@skyux/core'; + import { SkyCheckboxModule } from '@skyux/forms'; @@ -58,6 +62,9 @@ import { SkyGridComponent, SkyGridColumnComponent, SkyGridCellComponent + ], + providers: [ + SkyUIConfigService ] }) export class SkyGridModule { diff --git a/src/app/public/modules/grid/types/grid-ui-config.ts b/src/app/public/modules/grid/types/grid-ui-config.ts new file mode 100644 index 0000000..b58aa39 --- /dev/null +++ b/src/app/public/modules/grid/types/grid-ui-config.ts @@ -0,0 +1,3 @@ +export interface SkyGridUIConfig { + selectedColumnIds?: string[]; +}