From b68b0cfbaae0e37f9fd9b1af260075fdfc30a327 Mon Sep 17 00:00:00 2001 From: Carlo Minotti <50220438+minottic@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:06:49 +0100 Subject: [PATCH] Use CDKScroll for overview * Add scroll component based on cdk * Add tests * Simplify logbook cover not to depend on matcard * Fix scrolling and update on resize * Add missing html change * Add overview table preliminary changes * Fix overview tests * Enable search, add spinner * Fix delete and edit for scroll * Fit image in given space --- scilog/src/app/app.module.ts | 4 +- scilog/src/app/core/remote-data.service.ts | 2 +- .../app/core/toolbar/toolbar.component.html | 2 +- .../core/toolbar/toolbar.component.spec.ts | 2 +- .../src/app/core/toolbar/toolbar.component.ts | 7 +- .../search-window.component.spec.ts | 2 - .../search-window/search-window.component.ts | 3 - .../logbook-cover/logbook-cover.component.css | 1 - .../logbook-cover.component.html | 88 +++------ .../logbook-cover.component.spec.ts | 19 -- .../logbook-cover/logbook-cover.component.ts | 3 - .../overview-scroll.component.css | 20 ++ .../overview-scroll.component.html | 9 + .../overview-scroll.component.spec.ts | 181 ++++++++++++++++++ .../overview-scroll.component.ts | 179 +++++++++++++++++ .../overview-table.component.spec.ts | 29 ++- .../overview-table.component.ts | 31 ++- .../src/app/overview/overview.component.html | 20 +- .../app/overview/overview.component.spec.ts | 98 ++-------- scilog/src/app/overview/overview.component.ts | 83 ++------ 20 files changed, 507 insertions(+), 276 deletions(-) create mode 100644 scilog/src/app/overview/overview-scroll/overview-scroll.component.css create mode 100644 scilog/src/app/overview/overview-scroll/overview-scroll.component.html create mode 100644 scilog/src/app/overview/overview-scroll/overview-scroll.component.spec.ts create mode 100644 scilog/src/app/overview/overview-scroll/overview-scroll.component.ts diff --git a/scilog/src/app/app.module.ts b/scilog/src/app/app.module.ts index 4d6905d8..cc7876ab 100644 --- a/scilog/src/app/app.module.ts +++ b/scilog/src/app/app.module.ts @@ -87,6 +87,7 @@ import { ResizedDirective } from '@shared/directives/resized.directive'; import { OverviewTableComponent } from './overview/overview-table/overview-table.component'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatSortModule } from '@angular/material/sort'; +import { OverviewScrollComponent } from './overview/overview-scroll/overview-scroll.component'; const appConfigInitializerFn = (appConfig: AppConfigService) => { return () => appConfig.loadAppConfig(); @@ -133,7 +134,8 @@ const appConfigInitializerFn = (appConfig: AppConfigService) => { SearchWindowComponent, TaskComponent, ResizedDirective, - OverviewTableComponent + OverviewTableComponent, + OverviewScrollComponent ], imports: [ BrowserModule, diff --git a/scilog/src/app/core/remote-data.service.ts b/scilog/src/app/core/remote-data.service.ts index c4bbc4f0..5754b8ff 100644 --- a/scilog/src/app/core/remote-data.service.ts +++ b/scilog/src/app/core/remote-data.service.ts @@ -622,7 +622,7 @@ export class SearchDataService extends RemoteDataService { return this._searchString; } public set searchString(value: string) { - this._searchString = value; + this._searchString = value ?? ''; } protected addIncludeScope(): Object { diff --git a/scilog/src/app/core/toolbar/toolbar.component.html b/scilog/src/app/core/toolbar/toolbar.component.html index f032fa52..e3c4f467 100644 --- a/scilog/src/app/core/toolbar/toolbar.component.html +++ b/scilog/src/app/core/toolbar/toolbar.component.html @@ -12,7 +12,7 @@
- +
- - - - - - - - - -
- -
- -

- {{ logbook?.description }} -

-
- - - - - - - - - -
- {{ logbook?.name }} - {{ logbook?.description }} - {{ logbook?.ownerGroup }} - {{ logbook?.createdAt | date }} - -
- - + -
-
+ +
+ +
+ +

+ {{ logbook?.description }} +

+
+ + + + + diff --git a/scilog/src/app/overview/logbook-cover/logbook-cover.component.spec.ts b/scilog/src/app/overview/logbook-cover/logbook-cover.component.spec.ts index cc6f5c91..12becaef 100644 --- a/scilog/src/app/overview/logbook-cover/logbook-cover.component.spec.ts +++ b/scilog/src/app/overview/logbook-cover/logbook-cover.component.spec.ts @@ -6,7 +6,6 @@ import { LogbookInfoService } from '@shared/logbook-info.service'; import { of } from 'rxjs'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { Logbooks } from '@model/logbooks'; -import { MatCardType } from '../overview.component'; class UserPreferencesMock { userInfo = { @@ -79,22 +78,4 @@ describe('LogbookWidgetComponent', () => { expect(isAnyEditAllowedSpy).toHaveBeenCalledTimes(1); }); - ['logbook-module', 'logbook-headline'].forEach(t => { - it(`should test ng-template: ${t}`, () => { - component.matView = t as MatCardType; - fixture.detectChanges(); - expect(fixture.debugElement.nativeElement.querySelector(`.${t}`)).toEqual(jasmine.anything()); - }) - }) - - it('should test on doubleClick', () => { - component.matView = 'logbook-headline'; - const selectOnDoubleClickSpy = spyOn(component, 'selectOnDoubleClick'); - fixture.detectChanges(); - const cardContainer = fixture.debugElement.nativeElement.querySelector('.card-container'); - cardContainer.dispatchEvent(new Event('click')); - cardContainer.dispatchEvent(new Event('dblclick')); - expect(selectOnDoubleClickSpy).toHaveBeenCalledTimes(1); - }); - }); diff --git a/scilog/src/app/overview/logbook-cover/logbook-cover.component.ts b/scilog/src/app/overview/logbook-cover/logbook-cover.component.ts index 06a07ecc..8393da65 100644 --- a/scilog/src/app/overview/logbook-cover/logbook-cover.component.ts +++ b/scilog/src/app/overview/logbook-cover/logbook-cover.component.ts @@ -21,9 +21,6 @@ export class LogbookWidgetComponent implements OnInit { @Input() logbook: Logbooks; - @Input() - matView: MatCardType; - @ViewChild('cardHeader') cardHeader: ElementRef; imageToShow: any; diff --git a/scilog/src/app/overview/overview-scroll/overview-scroll.component.css b/scilog/src/app/overview/overview-scroll/overview-scroll.component.css new file mode 100644 index 00000000..5f5385b1 --- /dev/null +++ b/scilog/src/app/overview/overview-scroll/overview-scroll.component.css @@ -0,0 +1,20 @@ +cdk-virtual-scroll-viewport { + height: 400px; + width: 100%; + border: 1px solid #ccc; +} + +.logbook-container { + overflow: scroll; + height: calc(100vh - 250px); + width: calc(100vw - 20px); + border-color: transparent; +} + +.logbook-content > .logbook-inline { + padding: 7px; +} + +.logbook-inline { + display: inline-flex; +} diff --git a/scilog/src/app/overview/overview-scroll/overview-scroll.component.html b/scilog/src/app/overview/overview-scroll/overview-scroll.component.html new file mode 100644 index 00000000..3bf30f7d --- /dev/null +++ b/scilog/src/app/overview/overview-scroll/overview-scroll.component.html @@ -0,0 +1,9 @@ + +
+ + + + +
+
diff --git a/scilog/src/app/overview/overview-scroll/overview-scroll.component.spec.ts b/scilog/src/app/overview/overview-scroll/overview-scroll.component.spec.ts new file mode 100644 index 00000000..e322b99b --- /dev/null +++ b/scilog/src/app/overview/overview-scroll/overview-scroll.component.spec.ts @@ -0,0 +1,181 @@ +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; +import { LogbookDataService } from 'src/app/core/remote-data.service'; +import { UserPreferencesService } from 'src/app/core/user-preferences.service'; +import { Logbooks } from 'src/app/core/model/logbooks'; +import { OverviewScrollComponent } from './overview-scroll.component'; +import { ResizedEvent } from 'src/app/core/directives/resized.directive'; +import { ElementRef, QueryList } from '@angular/core'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + +class UserPreferencesMock { + userInfo = { roles: ["roles"] }; +} + +describe('OverviewScrollComponent', () => { + let component: OverviewScrollComponent; + let fixture: ComponentFixture; + const logbookDataSpy = jasmine.createSpyObj( + 'LogbookDataService', + ['getDataBuffer', 'deleteLogbook'], + ); + logbookDataSpy.getDataBuffer.and.returnValue([{ abc: 1 }, {def: 2}, {ghi: 3}, {jkl: 4}]); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [OverviewScrollComponent], + providers: [ + { provide: LogbookDataService, useValue: logbookDataSpy }, + { provide: UserPreferencesService, useClass: UserPreferencesMock }, + ], + imports: [ScrollingModule] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OverviewScrollComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.config = { general: {}, filter: {}, view: {} }; + component['groupSize'] = 3; + component['pageSize'] = 20; + component['endOfData'] = false; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should test itemsCount', () => { + expect(component.itemSize).toEqual(432); + }); + + it('should test setPageSize', () => { + component['setPageSize'](2); + expect(component['pageSize']).toEqual(21); + component['setPageSize'](3); + expect(component['pageSize']).toEqual(21); + }); + + it('should test setGroupSize', () => { + component['groupSize'] = 3; + component['setGroupSize']({width: 332 * 2.6, height: 432 * 6}); + expect(component['groupSize']).toEqual(2); + expect(component['pageSize']).toEqual(20); + }); + + it('should test elementSize', () => { + expect(component['elementSize']( + {nativeElement: {getBoundingClientRect: () => ({width: 10, height: 20})}}) + ).toEqual({width: 10, height: 20}) + }); + + it('should test splitIntoGroups', () => { + const groups = component['splitIntoGroups']([0,1,2,3,4,5,6,7,8,9] as Logbooks[]); + expect(groups).toEqual([[0,1,2],[3,4,5],[6,7,8],[9]]); + }); + + it('should test regroupLogbooks', () => { + component.logbooks = [[0,1,2],[3,4,5],[6,7,8],[9]] as Logbooks[][]; + component['groupSize'] = 4; + expect(component['regroupLogbooks']()).toEqual([[0,1,2,3],[4,5,6,7],[8,9]]); + }); + + it('should test getLogbooks', async () => { + logbookDataSpy.getDataBuffer.calls.reset(); + await component['getLogbooks'](); + expect(logbookDataSpy.getDataBuffer).toHaveBeenCalledOnceWith(0, 20, { general: {}, filter: {}, view: {} }); + }); + + it('should test getAndGroupLogbooks', fakeAsync(async () => { + logbookDataSpy.getDataBuffer.and.returnValue([{ abc: 1 }, {def: 2}, {ghi: 3}, {jkl: 4}]); + const logbooks = await component['getAndGroupLogbooks'](); + expect(component['currentPage']).toEqual(1); + expect(logbooks).toEqual([[{ abc: 1 }, {def: 2}, {ghi: 3}], [{jkl: 4}]]); + expect(component['endOfData']).toEqual(true); + expect(component.isLoaded).toEqual(true); + })); + + [ + {sizes: [3, 19], spy: 'reshapeOnResize'}, + {sizes: [7, 20], spy: 'reshapeOnResize'}, + {sizes: [5, 21], spy: 'regroupLogbooks'}, + ].forEach((t, i) => { + it(`should test refreshLogbooks ${i}`, async () => { + const spy = spyOn(component, t.spy); + await component['refreshLogbooks'](t.sizes[0], t.sizes[1]); + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + it('should test compareAndRefreshSizes', async () => { + const setGroupSizeSpy = spyOn(component, 'setGroupSize'); + const refreshLogbooksSpy = spyOn(component, 'refreshLogbooks'); + await component['compareAndRefreshSizes']({width: 10, height: 20}); + expect(setGroupSizeSpy).toHaveBeenCalledTimes(1); + expect(refreshLogbooksSpy).toHaveBeenCalledTimes(1); + }); + + it('should test onScroll', async () => { + const getLogbooksSpy = spyOn(component, 'getLogbooks').and.resolveTo([]); + await component.onScroll(0); + await component.onScroll(1); + expect(getLogbooksSpy).toHaveBeenCalledTimes(1); + }); + + it('should test onResized', async () => { + const compareAndRefreshSizesSpy = spyOn(component, 'compareAndRefreshSizes'); + await component.onResized({newRect: {width: 1, height: 2}} as ResizedEvent); + expect(compareAndRefreshSizesSpy).toHaveBeenCalledOnceWith({width: 1, height: 2}); + }); + + it('should test trackByGroupId', () => { + const _id = component.trackByGroupId(1, [{id: '123'}, {id: '456'}, {id: '789'}]); + expect(_id).toEqual('123456789'); + }); + + it('should test ngAfterViewChecked', () => { + spyOn(component, 'elementSize'); + const compareAndRefreshSizesSpy = spyOn(component, 'compareAndRefreshSizes'); + component.logbookWidgetComponent = [1, 2, 3] as unknown as QueryList; + expect(component.logbookWidgetComponent.length).toEqual(3); + component.ngAfterViewChecked(); + expect(component['updateSizes']).toEqual(false); + expect(compareAndRefreshSizesSpy).toHaveBeenCalledTimes(1); + }); + + [ + {pageSize: 18, spyCalls: 1, logbooks: [[1,2,3],[4,5,6],[7,8,9]]}, + {pageSize: 22, logbooks: [[1,2,3],[4,5]]}, + {endOfData: true, logbooks: [[1,2,3],[4,5,6],[7]]} + ].forEach((t, i) => { + it(`should test reshapeOnResize ${i}`, async () => { + const getLogbooksSpy = spyOn(component, 'getLogbooks').and.resolveTo([8,9]); + component.logbooks = [[1,2,3], [4,5,6], [7]] as Logbooks[][]; + await component['reshapeOnResize'](t.pageSize); + expect(getLogbooksSpy).toHaveBeenCalledTimes(t.spyCalls ?? 0); + if (t.spyCalls) expect(getLogbooksSpy).toHaveBeenCalledOnceWith(t.pageSize, 2); + expect(component.logbooks).toEqual(t.logbooks as Logbooks[][]); + }); + }); + + [true, false, 'abc'] + .forEach((t, i) => { + it(`should test reloadLogbooks ${i}`, fakeAsync(async () => { + spyOn(component, 'getAndGroupLogbooks'); + const scrollToOffset = spyOn(component.viewPort, 'scrollToOffset'); + await component.reloadLogbooks(t && true); + if (typeof t === 'string') + component['dataService'].searchString === 'abc'; + expect(scrollToOffset).toHaveBeenCalledTimes(1); + })); + }); + + it('should test deleteLogbook', async () => { + const reloadSpy = spyOn(component, 'reloadLogbooks'); + await component.deleteLogbook('123'); + expect(logbookDataSpy.deleteLogbook).toHaveBeenCalledOnceWith('123'); + expect(reloadSpy).toHaveBeenCalledTimes(1); + }); + +}) diff --git a/scilog/src/app/overview/overview-scroll/overview-scroll.component.ts b/scilog/src/app/overview/overview-scroll/overview-scroll.component.ts new file mode 100644 index 00000000..65985837 --- /dev/null +++ b/scilog/src/app/overview/overview-scroll/overview-scroll.component.ts @@ -0,0 +1,179 @@ +import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, QueryList, ViewChild, ViewChildren } from "@angular/core"; +import { ResizedEvent } from "src/app/core/directives/resized.directive"; +import { WidgetItemConfig } from "src/app/core/model/config"; +import { Logbooks } from "src/app/core/model/logbooks"; +import { LogbookDataService } from "src/app/core/remote-data.service"; +import { LogbookWidgetComponent } from "../logbook-cover/logbook-cover.component"; +import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling"; +import { Subscription } from "rxjs"; + +type Sizes = { + width: number, + height: number, +} + +@Component({ + selector: 'overview-scroll', + templateUrl: './overview-scroll.component.html', + styleUrls: ['./overview-scroll.component.css'] +}) +export class OverviewScrollComponent { + + @Input() config: WidgetItemConfig; + + @Output() logbookEdit = new EventEmitter(); + @Output() logbookSelection = new EventEmitter(); + + @ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport; + @ViewChildren(LogbookWidgetComponent, { read: ElementRef }) logbookWidgetComponent: QueryList; + + logbooks: Logbooks[][] = []; + isLoaded: boolean; + private contentSize: Sizes = {width: 332, height: 432}; + private minPageSize = 20; + private currentPage = 0; + private pageSize: number; + private groupSize: number; + private updateSizes = true; + private endOfData = false; + private renderedRangeSubscription: Subscription; + + constructor(private dataService: LogbookDataService, private changeRef: ChangeDetectorRef) {} + + async ngAfterViewInit() { + this.setGroupSize(this.elementSize(this.viewPort.elementRef)); + this.logbooks = await this.getAndGroupLogbooks(); + this.renderedRangeSubscription = this.viewPort.renderedRangeStream.subscribe( + async ({start, end}) => await this.onScroll(end)) + } + + ngAfterViewChecked() { + if (!this.updateSizes) return; + this.logbookWidgetComponent.some(logbookWidget => { + this.contentSize = this.elementSize(logbookWidget); + this.updateSizes = false; + this.compareAndRefreshSizes(this.elementSize(this.viewPort.elementRef)); + return true; + }) + } + + get itemSize() { + return this.contentSize.height; + } + + private setPageSize(pageSize: number) { + const minSize = this.minPageSize; + if (this.pageSize && this.pageSize % this.groupSize === 0) return + this.pageSize = pageSize > minSize? pageSize: Math.ceil(minSize / this.groupSize) * this.groupSize; + } + + private setGroupSize(containerSize: Sizes) { + const cols = Math.max(Math.round(containerSize.width / this.contentSize.width) - 1, 1); + const rows = Math.ceil(containerSize.height / this.contentSize.height); + this.groupSize = cols; + this.setPageSize(cols * rows * 2); + } + + private elementSize(element: ElementRef) { + const elementSize = element.nativeElement.getBoundingClientRect(); + return {width: elementSize.width, height: elementSize.height} + } + + private splitIntoGroups(logbooks: Logbooks[]) { + const groups = []; + while (logbooks.length) groups.push(logbooks.splice(0, this.groupSize)); + return groups; + } + + private regroupLogbooks() { + return this.splitIntoGroups([...this.logbooks.flat()]); + } + + private async getLogbooks(pageSize = this.pageSize, limit = this.pageSize) { + return await this.dataService.getDataBuffer(pageSize * this.currentPage, limit, this.config); + } + + private async getAndGroupLogbooks() { + this.isLoaded = false; + const logbooks = await this.getLogbooks(); + if ( + logbooks.length < this.pageSize || + logbooks.length === 0 + ) this.endOfData = true + this.currentPage++; + this.isLoaded = true; + return this.splitIntoGroups(logbooks); + } + + private async refreshLogbooks(oldGroupSize: number, oldPageSize: number) { + if (this.groupSize === oldGroupSize && this.pageSize === oldPageSize) return; + else if ( + this.pageSize > oldPageSize || + oldPageSize % this.groupSize + ) await this.reshapeOnResize(oldPageSize); + else this.logbooks = this.regroupLogbooks(); + } + + private async reshapeOnResize(oldPageSize: number, logbooks: Logbooks[] = undefined) { + const pageDiff = this.pageSize - oldPageSize; + const _logbooks = logbooks ?? this.logbooks.flat(); + if (!this.endOfData) + pageDiff > 0 ? + _logbooks.push(...(await this.getLogbooks(oldPageSize, pageDiff))) : + _logbooks.splice(pageDiff, -pageDiff); + this.logbooks = this.splitIntoGroups(_logbooks); + } + + async reloadLogbooks(resetSort = true, search?: string) { + if (search !== null && search !== undefined) this.dataService.searchString = search; + this.viewPort.scrollToOffset(0); + this.currentPage = 0; + this.endOfData = false; + this.logbooks = await this.getAndGroupLogbooks(); + } + + private async compareAndRefreshSizes(containerSizes: Sizes) { + const oldGroupSize = this.groupSize; + const oldPageSize = this.pageSize; + this.setGroupSize(containerSizes); + await this.refreshLogbooks(oldGroupSize, oldPageSize); + } + + async onScroll(index: number) { + if (this.endOfData) return; + if (index < this.logbooks.length) return; + const logbooks = await this.getAndGroupLogbooks(); + this.logbooks = this.logbooks.concat(logbooks); + this.changeRef.detectChanges(); + } + + @HostListener('window:resize') + async onResized(event: ResizedEvent) { + if (!event) return + await this.compareAndRefreshSizes(event.newRect); + } + + trackByGroupId(index: number, logbooksGroup: Logbooks[]): string { + return logbooksGroup.reduce((previousValue, currentValue) => previousValue += currentValue.id, ''); + } + + editLogbook(logbook: Logbooks) { + this.logbookEdit.emit(logbook); + } + + async deleteLogbook(logbookId: string) { + await this.dataService.deleteLogbook(logbookId); + await this.reloadLogbooks(); + } + + logbookSelected(logbookId: string) { + this.logbookSelection.emit(logbookId); + } + + ngOnDestroy() { + if (this.renderedRangeSubscription) { + this.renderedRangeSubscription.unsubscribe(); + } + } + +} diff --git a/scilog/src/app/overview/overview-table/overview-table.component.spec.ts b/scilog/src/app/overview/overview-table/overview-table.component.spec.ts index b8450005..e02265dd 100644 --- a/scilog/src/app/overview/overview-table/overview-table.component.spec.ts +++ b/scilog/src/app/overview/overview-table/overview-table.component.spec.ts @@ -16,7 +16,7 @@ describe('OverviewTableComponent', () => { let fixture: ComponentFixture; const logbookDataSpy = jasmine.createSpyObj( 'LogbookDataService', - ['getDataBuffer', 'getCount'], + ['getDataBuffer', 'getCount', 'deleteLogbook'], { imagesLocation: 'server/images' } ); logbookDataSpy.getCount.and.returnValue({ count: 1 }); @@ -62,6 +62,7 @@ describe('OverviewTableComponent', () => { await component.getLogbooks(); expect(logbookDataSpy.getDataBuffer).toHaveBeenCalledOnceWith(0, 5, component.config); expect(component.dataSource.data).toEqual([{ abc: 1 } as Logbooks]); + expect(component.isLoaded).toEqual(true); }); it('should test openLogbook', () => { @@ -95,11 +96,27 @@ describe('OverviewTableComponent', () => { expect(getDatasetsSpy).toHaveBeenCalledTimes(1); }); - it('should test resetSortAndReload', async () => { - await component.resetSortAndReload(); - expect(component.sort.active).toEqual(''); - expect(component.sort.direction).toEqual(''); - expect(component['_config']).toEqual({ general: {}, filter: {}, view: {} }); + [true, false, 'abc'] + .forEach((t, i) => { + it(`should test reloadLogbooks ${i}`, async () => { + const getLogbooksSpy = spyOn(component, 'getLogbooks'); + await component.reloadLogbooks(); + expect(getLogbooksSpy).toHaveBeenCalledTimes(1); + if (typeof t === 'string') + component['dataService'].searchString === 'abc'; + else if (t) { + expect(component.sort.active).toEqual(''); + expect(component.sort.direction).toEqual(''); + expect(component['_config']).toEqual({ general: {}, filter: {}, view: {} }); + } + }); + }); + + it('should test deleteLogbook', async () => { + const reloadSpy = spyOn(component, 'reloadLogbooks'); + await component.deleteLogbook('123'); + expect(logbookDataSpy.deleteLogbook).toHaveBeenCalledOnceWith('123'); + expect(reloadSpy).toHaveBeenCalledOnceWith(false); }); }) diff --git a/scilog/src/app/overview/overview-table/overview-table.component.ts b/scilog/src/app/overview/overview-table/overview-table.component.ts index da9ed99c..23b6011a 100644 --- a/scilog/src/app/overview/overview-table/overview-table.component.ts +++ b/scilog/src/app/overview/overview-table/overview-table.component.ts @@ -20,7 +20,7 @@ export class OverviewTableComponent implements OnInit { @Input() config: WidgetItemConfig; @Output() logbookEdit = new EventEmitter(); - @Output() logbookDelete = new EventEmitter(); + @Output() logbookSelection = new EventEmitter(); @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort!: MatSort; @@ -29,6 +29,7 @@ export class OverviewTableComponent implements OnInit { totalItems: number; displayedColumns = ['name', 'description', 'ownerGroup', 'createdAt', 'thumbnail', 'actions']; private _config: WidgetItemConfig; + isLoaded: boolean; constructor( private dataService: LogbookDataService, @@ -47,7 +48,7 @@ export class OverviewTableComponent implements OnInit { onSortChange(): void { this.paginator.pageIndex = 0; - this._config.view.order = [`${this.sort.active} ${this.sort.direction || 'DESC'}`]; + this._config.view.order = [`${this.sort.active || 'defaultOrder'} ${this.sort.direction || 'DESC'}`]; this.getLogbooks(); }; @@ -60,16 +61,21 @@ export class OverviewTableComponent implements OnInit { } async getLogbooks() { + this.isLoaded = false; const data = await this.dataService.getDataBuffer(this.paginator.pageIndex * this.paginator.pageSize, this.paginator.pageSize, this._config); + this.isLoaded = true; this.dataSource = new MatTableDataSource(data); } - async resetSortAndReload() { - this.sort.active = ''; - this.sort.direction = ''; - this.sort.sort({ id: '', start: 'asc', disableClear: false }); - this.paginator.pageIndex = 0; - this._config = JSON.parse(JSON.stringify(this.config)); + async reloadLogbooks(resetSort = true, search?: string) { + if (search !== null && search !== undefined) this.dataService.searchString = search; + if (resetSort) { + this.sort.active = ''; + this.sort.direction = ''; + this.sort.sort({ id: '', start: 'asc', disableClear: false }); + this.paginator.pageIndex = 0; + this._config = JSON.parse(JSON.stringify(this.config)); + } await this.getLogbooks(); } @@ -90,8 +96,13 @@ export class OverviewTableComponent implements OnInit { this.logbookEdit.emit(logbook); } - deleteLogbook(logbookId: string) { - this.logbookDelete.emit(logbookId); + async deleteLogbook(logbookId: string) { + await this.dataService.deleteLogbook(logbookId); + await this.reloadLogbooks(false); + } + + logbookSelected(logbookId: string) { + this.logbookSelection.emit(logbookId); } } diff --git a/scilog/src/app/overview/overview.component.html b/scilog/src/app/overview/overview.component.html index 9b49c37a..f04fc1f6 100644 --- a/scilog/src/app/overview/overview.component.html +++ b/scilog/src/app/overview/overview.component.html @@ -1,4 +1,4 @@ - +

Logbooks

@@ -7,20 +7,10 @@

Logbooks

view_headline
- + -
-
- - - - -
-
- + (logbookSelection)="logbookSelected($event)"> +
diff --git a/scilog/src/app/overview/overview.component.spec.ts b/scilog/src/app/overview/overview.component.spec.ts index fd82530d..4ee04c66 100644 --- a/scilog/src/app/overview/overview.component.spec.ts +++ b/scilog/src/app/overview/overview.component.spec.ts @@ -8,9 +8,7 @@ import { CookieService } from 'ngx-cookie-service'; import { LogbookDataService } from '@shared/remote-data.service'; import { of } from 'rxjs'; import { RouterTestingModule } from '@angular/router/testing'; -import { ResizedEvent } from '@shared/directives/resized.directive'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { LogbookIconScrollService } from './logbook-icon-scroll-service.service'; class UserPreferencesMock { userInfo = { @@ -36,12 +34,12 @@ describe('OverviewComponent', () => { cookiesSpy = jasmine.createSpyObj("CookieService", ["lastLogbook"]); cookiesSpy.lastLogbook.and.returnValue([]); - const tableSpy = jasmine.createSpyObj("OverviewTableComponent", ['getLogbooks', 'resetSortAndReload']); - const logbookIconSpy = jasmine.createSpyObj("logbookIconScrollService", ['reload', 'initialize']); + const tableSpy = jasmine.createSpyObj("OverviewTableComponent", ['reloadLogbooks']); + const scrollSpy = jasmine.createSpyObj("OverviewScrollComponent", ['reloadLogbooks']); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ OverviewComponent], + declarations: [OverviewComponent], imports: [MatDialogModule, RouterTestingModule, BrowserAnimationsModule], providers: [ {provide: MAT_DIALOG_DATA, useValue: {}}, @@ -49,8 +47,6 @@ describe('OverviewComponent', () => { {provide: UserPreferencesService, useClass: UserPreferencesMock}, {provide: CookieService}, {provide: LogbookDataService, useValue: logbookDataSpy}, - {provide: LogbookIconScrollService, useValue: logbookIconSpy}, - ] }) .compileComponents(); @@ -60,8 +56,8 @@ describe('OverviewComponent', () => { fixture = TestBed.createComponent(OverviewComponent); component = fixture.componentInstance; fixture.detectChanges(); - component.logbookIconScrollService.groupSize = 3; component.overviewTable = tableSpy; + component.overviewSroll = scrollSpy; }); it('should create', () => { @@ -69,86 +65,28 @@ describe('OverviewComponent', () => { }); [ - {}, - undefined, - {clientWidth: 0, clientHeight: 0}, - {clientWidth: 10, clientHeight: 20}, - ].forEach((t, i) => { - [['logbook-module', 10], ['logbook-headline', 20]].forEach(st => { - it(`should test get matCardSide ${i}:${st[0]}`, () => { - spyOn(component, 'getFirstVisibleElement').and.returnValue(t); - component.matCardType = st[0] as MatCardType; - const expected = st[0] === 'logbook-module' ? 352 : 47; - expect(component.matCardSide).toEqual(i === 3 ? st[1] as number : expected); - }); - }); - }); - - [[1, 1], [800, 2]].forEach(t => { - it(`should test groupSize ${t[0]}`, () => { - expect(component.groupSize(t[0])).toEqual(t[1]); - }); - }); - - [ - [{newRect: {width: 1056}}, [0, 0]], - [{newRect: {width: 700}, oldRect: {width: 300}}, [1, 0]], - [{newRect: {width: 100}, oldRect: {width: 300}}, [1, 0]], - [{newRect: {width: 200}, oldRect: {width: 300}}, [0, 1]], - [{newRect: {width: 400}, oldRect: {width: 300}}, [0, 1]], - ].forEach((t, i) => { - it(`should test onResized ${i}:logbook-module`, () => { - logbookIconSpy.initialize.calls.reset(); - logbookIconSpy.reload.calls.reset(); - component.onResized(t[0] as ResizedEvent); - expect(logbookIconSpy.initialize).toHaveBeenCalledTimes(t[1][0]); - expect(logbookIconSpy.reload).toHaveBeenCalledTimes(t[1][1]); - }); - }); - - [ - [{newRect: {height: 147}}, [0, 0]], - [{newRect: {height: 700}, oldRect: {height: 300}}, [1, 0]], - [{newRect: {height: 100}, oldRect: {height: 300}}, [1, 0]], - [{newRect: {height: 200}, oldRect: {height: 300}}, [0, 1]], - [{newRect: {height: 400}, oldRect: {height: 300}}, [0, 1]], + ['logbook-module', scrollSpy, 'add', true], + ['logbook-module', scrollSpy, 'edit', false], + ['logbook-headline', tableSpy, 'add', true], + ['logbook-headline', tableSpy, 'edit', false], ].forEach((t, i) => { - it(`should test onResized ${i}:logbook-headline`, () => { - logbookIconSpy.initialize.calls.reset(); - logbookIconSpy.reload.calls.reset(); - component.matCardType = 'logbook-headline'; - component.onResized(t[0] as ResizedEvent); - expect(logbookIconSpy.initialize).toHaveBeenCalledTimes(t[1][0]); - expect(logbookIconSpy.reload).toHaveBeenCalledTimes(t[1][1]); - }); - }); - - [ - ['logbook-module', 'clientWidth'], - ['logbook-headline', 'clientHeight'] - ].forEach(t => { - it(`should test clientSide ${t[0]}`, () => { + it(`should test reloadData ${i}`, async () => { + t[1].reloadLogbooks.calls.reset(); component.matCardType = t[0] as MatCardType; - expect(component.clientSide).toEqual(t[1]); + await component['reloadData'](t[2] as 'edit' | 'add'); + expect(t[1].reloadLogbooks).toHaveBeenCalledOnceWith(t[3]); }); }); - it('should trigger onResized on window resize', () => { - const onResizedSpy = spyOn(component, 'onResized'); - window.dispatchEvent(new Event('resize')); - expect(onResizedSpy).toHaveBeenCalled(); - }); - [ - ['logbook-module', logbookIconSpy.reload, 'add'], - ['logbook-headline', tableSpy.getLogbooks, 'edit'], - ['logbook-headline', tableSpy.resetSortAndReload, 'add'], + ['logbook-module', scrollSpy], + ['logbook-headline', tableSpy], ].forEach((t, i) => { - it(`should test reloadData ${i}`, async() => { - t[1].calls.reset(); + it(`should test setSearch ${i}`, async () => { + t[1].reloadLogbooks.calls.reset(); component.matCardType = t[0] as MatCardType; - await component['reloadData'](t[2] as 'edit' | 'add'); - expect(t[1]).toHaveBeenCalledTimes(1); + await component.setSearch('abc'); + expect(t[1].reloadLogbooks).toHaveBeenCalledOnceWith(true, 'abc'); }); }); diff --git a/scilog/src/app/overview/overview.component.ts b/scilog/src/app/overview/overview.component.ts index dfa0dd6b..5ef08392 100644 --- a/scilog/src/app/overview/overview.component.ts +++ b/scilog/src/app/overview/overview.component.ts @@ -1,6 +1,6 @@ -import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { Logbooks } from '@model/logbooks'; -import { Subscription } from 'rxjs'; +import { from, Subscription, switchMap } from 'rxjs'; import { UserPreferencesService } from '@shared/user-preferences.service'; import { CollectionConfig, WidgetItemConfig } from '@model/config'; import { MatLegacyDialog as MatDialog, MatLegacyDialogConfig as MatDialogConfig } from '@angular/material/legacy-dialog'; @@ -8,11 +8,9 @@ import { AddCollectionComponent } from './add-collection/add-collection.componen import { AddLogbookComponent } from './add-logbook/add-logbook.component'; import { LogbookInfoService } from '@shared/logbook-info.service'; import { CookiesService } from '@shared/cookies.service'; -import { LogbookDataService } from '@shared/remote-data.service'; -import { LogbookIconScrollService } from './logbook-icon-scroll-service.service'; -import { ResizedEvent } from '@shared/directives/resized.directive'; import { animate, style, transition, trigger } from '@angular/animations'; import { OverviewTableComponent } from './overview-table/overview-table.component'; +import { OverviewScrollComponent } from './overview-scroll/overview-scroll.component'; enum ContentType { COLLECTION = 'collection', @@ -44,21 +42,16 @@ export class OverviewComponent implements OnInit { fileToUpload: File = null; - logbookSubscription: Subscription = null; subscriptions: Subscription[] = []; - _matCardSide = { 'logbook-module': 352, 'logbook-headline': 47 }; - @ViewChild('logbookContainer', { static: true }) logbookContainer: ElementRef; - @ViewChild(OverviewTableComponent) overviewTable: OverviewTableComponent + @ViewChild(OverviewTableComponent) overviewTable: OverviewTableComponent; + @ViewChild(OverviewScrollComponent) overviewSroll: OverviewScrollComponent; matCardType: MatCardType = 'logbook-module'; - constructor( - public logbookIconScrollService: LogbookIconScrollService, private userPreferences: UserPreferencesService, public dialog: MatDialog, private logbookInfo: LogbookInfoService, - private cookie: CookiesService, - private dataService: LogbookDataService) {} + private cookie: CookiesService) {} ngOnInit(): void { this.logbookInfo.logbookInfo = null; @@ -71,48 +64,14 @@ export class OverviewComponent implements OnInit { if (this.collections.length == 1) { this.collectionSelected(this.collections[0]); } - if (this.logbookSubscription != null) { - this.logbookSubscription.unsubscribe(); - } this.config = this._prepareConfig(); - this.logbookIconScrollService.initialize(this.config); })); } - @HostListener('window:resize') - onResized(event: ResizedEvent) { - if (!event) return - const side = this.matCardType === 'logbook-module' ? 'width' : 'height' - const newSize = this.groupSize(event.newRect[side]); - if (newSize === this.logbookIconScrollService.groupSize) return - this.logbookIconScrollService.groupSize = newSize; - if (event.newRect?.[side] > 2 * event.oldRect?.[side] || event.oldRect?.[side] > 2 * event.newRect?.[side]) { - this.logbookIconScrollService.initialize(this.config); - } - else - this.logbookIconScrollService.reload(); - } - - get clientSide() { - return this.matCardType === 'logbook-module' ? 'clientWidth' : 'clientHeight' - } - - get matCardSide() { - const matCardType = this.matCardType; - const element = this.getFirstVisibleElement(matCardType); - const matCardSide = element?.[this.clientSide]; - if (!matCardSide) - return this._matCardSide[matCardType]; - this._matCardSide[matCardType] = matCardSide; - return this._matCardSide[matCardType] - } - - private getFirstVisibleElement(matCardType: string) { - return this.logbookIconScrollService?.datasource?.adapter?.firstVisible?.element?.querySelector?.(`.${matCardType}`); - } - - groupSize(viewPortSide: number) { - return Math.floor(viewPortSide / this.matCardSide) || 1; + get overviewComponent() { + return this.matCardType === 'logbook-module' + ? this.overviewSroll: + this.overviewTable; } collectionSelected(collection: CollectionConfig) { @@ -143,23 +102,13 @@ export class OverviewComponent implements OnInit { let dialogRef: any; dialogRef = this.dialog.open(AddLogbookComponent, dialogConfig); - this.subscriptions.push(dialogRef.afterClosed().subscribe(async result => { - console.log("Dialog result:", result); - await this.reloadData('edit'); - })); + this.subscriptions.push(dialogRef.afterClosed().pipe( + switchMap(() => from(this.reloadData('edit'))) + ).subscribe()); } private async reloadData(action: 'edit' | 'add') { - const overviewMethod = action === 'edit'? 'getLogbooks': 'resetSortAndReload'; - this.matCardType === 'logbook-module' - ? await this.logbookIconScrollService.reload() - : await this.overviewTable[overviewMethod](); - } - - async deleteLogbook(logbookId: string) { - await this.dataService.deleteLogbook(logbookId); - await this.logbookIconScrollService.reload(); - console.log("deleted logbook ", logbookId); + await this.overviewComponent.reloadLogbooks(!(action === 'edit')); } addCollectionLogbook(contentType: string) { @@ -204,6 +153,10 @@ export class OverviewComponent implements OnInit { return _config; } + async setSearch(search: string) { + await this.overviewComponent.reloadLogbooks(true, search); + } + ngOnDestroy(): void { this.subscriptions.forEach( (subscription) => subscription.unsubscribe());