From da92a03a2cd886bdad82b3d3632ddbcaa26d33da Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Mon, 27 Apr 2026 16:29:44 +0200 Subject: [PATCH 01/11] replaced sample datasets table with dynamic-mat-table and fixed pagination bugs --- .../sample-detail.component.html | 23 +++--- .../sample-detail/sample-detail.component.ts | 82 +++++++++++++------ .../effects/samples.effects.ts | 14 ++-- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.html b/src/app/samples/sample-detail/sample-detail.component.html index 4d02b56f88..104f2ea743 100644 --- a/src/app/samples/sample-detail/sample-detail.component.html +++ b/src/app/samples/sample-detail/sample-detail.component.html @@ -164,20 +164,23 @@ - folder Datasets + folder Related Datasets
- + [dataSource]="dataSource" + [setting]="setting" + [pagingMode]="paginationMode" + [pagination]="pagination" + [rowSelectionMode]="rowSelectionMode" + [showGlobalTextSearch]="false" + [emptyMessage]="'No datasets available'" + [emptyIcon]="'folder'" + (paginationChange)="onPaginationChange($event)" + (onRowEvent)="onRowEvent($event)" + >
diff --git a/src/app/samples/sample-detail/sample-detail.component.ts b/src/app/samples/sample-detail/sample-detail.component.ts index c73c583b94..57b6617046 100644 --- a/src/app/samples/sample-detail/sample-detail.component.ts +++ b/src/app/samples/sample-detail/sample-detail.component.ts @@ -1,6 +1,6 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Component, OnDestroy, OnInit } from "@angular/core"; -import { fromEvent, Subscription } from "rxjs"; +import { BehaviorSubject, fromEvent, Subscription } from "rxjs"; import { selectSampleDetailPageViewModel } from "../../state-management/selectors/samples.selectors"; import { Store } from "@ngrx/store"; import { @@ -15,10 +15,6 @@ import { } from "../../state-management/actions/samples.actions"; import { DatePipe, SlicePipe } from "@angular/common"; import { FileSizePipe } from "shared/pipes/filesize.pipe"; -import { - TableColumn, - PageChangeEvent, -} from "shared/modules/table/table.component"; import { PickedFile, SubmitCaptionEvent, @@ -27,12 +23,22 @@ import { EditableComponent } from "app-routing/pending-changes.guard"; import { AppConfigService } from "app-config.service"; import { CreateAttachmentV3Dto, - DatasetClass, OutputAttachmentV3Dto, OutputDatasetObsoleteDto, ReturnedUserDto, OutputSampleDto, } from "@scicatproject/scicat-sdk-ts-angular"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; +import { ITableSetting } from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { + IRowEvent, + RowEventType, + TableSelectionMode, +} from "shared/modules/dynamic-material-table/models/table-row.model"; export interface TableData { pid: string; @@ -65,17 +71,36 @@ export class SampleDetailComponent show = false; subscriptions: Subscription[] = []; - tableData: TableData[] = []; - tablePaginate = true; - tableColumns: TableColumn[] = [ - { name: "name", icon: "portrait", sort: false, inList: true }, - { name: "sourceFolder", icon: "explore", sort: false, inList: true }, - { name: "size", icon: "save", sort: false, inList: true }, - { name: "creationTime", icon: "calendar_today", sort: false, inList: true }, - { name: "owner", icon: "face", sort: false, inList: true }, - { name: "location", icon: "explore", sort: false, inList: true }, + tableColumns: TableField[] = [ + { name: "name", header: "Name" }, + { name: "sourceFolder", header: "Source Folder" }, + { name: "size", header: "Size" }, + { name: "creationTime", header: "Creation Time" }, + { name: "owner", header: "Owner" }, + { name: "location", header: "Location" }, ]; + setting: ITableSetting = { + rowStyle: { + "border-bottom": "1px solid #d2d2d2", + }, + }; + + dataSource: BehaviorSubject = new BehaviorSubject( + [], + ); + + paginationMode: TablePaginationMode = "server-side"; + + pagination: TablePagination = { + pageSizeOptions: [5, 10, 25, 50, 100], + pageIndex: 0, + pageSize: 25, + length: 0, + }; + + rowSelectionMode: TableSelectionMode = "none"; + constructor( private appConfigService: AppConfigService, private datePipe: DatePipe, @@ -143,11 +168,11 @@ export class SampleDetailComponent ); } - onPageChange(event: PageChangeEvent) { + onPaginationChange({ pageIndex, pageSize }: TablePagination) { this.store.dispatch( changeDatasetsPageAction({ - page: event.pageIndex, - limit: event.pageSize, + page: pageIndex, + limit: pageSize, }), ); this.store.dispatch( @@ -155,9 +180,11 @@ export class SampleDetailComponent ); } - onRowClick(dataset: DatasetClass) { - const id = encodeURIComponent(dataset.pid); - this.router.navigateByUrl("/datasets/" + id); + onRowEvent(event: IRowEvent) { + if (event.event === RowEventType.RowClick) { + const id = encodeURIComponent(event.sender.row.pid); + this.router.navigateByUrl("/datasets/" + id); + } } ngOnInit() { @@ -174,6 +201,14 @@ export class SampleDetailComponent if (vm.attachments) { this.attachments = vm.attachments; } + + this.dataSource.next(this.formatTableData(vm.datasets)); + this.pagination = { + ...this.pagination, + pageIndex: vm.datasetsPage || 0, + pageSize: vm.datasetsPerPage || this.pagination.pageSize, + length: vm.datasetsCount || 0, + }; }), ); // Prevent user from reloading page if there are unsave changes @@ -184,11 +219,6 @@ export class SampleDetailComponent } }), ); - this.subscriptions.push( - this.vm$.subscribe((vm) => { - this.tableData = this.formatTableData(vm.datasets); - }), - ); this.subscriptions.push( this.vm$.subscribe((vm) => { diff --git a/src/app/state-management/effects/samples.effects.ts b/src/app/state-management/effects/samples.effects.ts index 03a63e233d..1373e4d4cc 100644 --- a/src/app/state-management/effects/samples.effects.ts +++ b/src/app/state-management/effects/samples.effects.ts @@ -134,9 +134,11 @@ export class SampleEffects { .datasetsControllerFindAllV3( JSON.stringify({ where: { sampleId }, - order, - skip, - limit, + limits: { + order, + skip, + limit, + }, }), ) .pipe( @@ -155,11 +157,11 @@ export class SampleEffects { ofType(fromActions.fetchSampleDatasetsCountAction), switchMap(({ sampleId }) => this.datasetApi - .datasetsControllerFindAllV3(JSON.stringify({ where: { sampleId } })) + .datasetsControllerCountV3(JSON.stringify({ where: { sampleId } })) .pipe( - map((datasets) => + map(({ count }) => fromActions.fetchSampleDatasetsCountCompleteAction({ - count: datasets.length, + count, }), ), catchError(() => From df6e90d59b007b10691dbd5be961e0091a2500c2 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 09:26:44 +0200 Subject: [PATCH 02/11] test fix --- .../sample-detail.component.spec.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.spec.ts b/src/app/samples/sample-detail/sample-detail.component.spec.ts index f936d04b41..26360377fe 100644 --- a/src/app/samples/sample-detail/sample-detail.component.spec.ts +++ b/src/app/samples/sample-detail/sample-detail.component.spec.ts @@ -16,7 +16,6 @@ import { waitForAsync, } from "@angular/core/testing"; import { NgxJsonViewerModule } from "ngx-json-viewer"; -import { PageChangeEvent } from "shared/modules/table/table.component"; import { changeDatasetsPageAction, fetchSampleDatasetsAction, @@ -36,10 +35,8 @@ import { MatIconModule } from "@angular/material/icon"; import { MatTabsModule } from "@angular/material/tabs"; import { FlexLayoutModule } from "@ngbracket/ngx-layout"; import { AppConfigService } from "app-config.service"; -import { - DatasetClass, - ReturnedUserDto, -} from "@scicatproject/scicat-sdk-ts-angular"; +import { ReturnedUserDto } from "@scicatproject/scicat-sdk-ts-angular"; +import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; const getConfig = () => ({ editMetadataEnabled: true, @@ -204,20 +201,20 @@ describe("SampleDetailComponent", () => { }); }); - describe("#onPageChange()", () => { + describe("#onPaginationChange()", () => { it("should dispatch a changeDatasetsPageAction and a fetchSampleDatasetsAction", () => { dispatchSpy = spyOn(store, "dispatch"); const sample = mockSample; sample.sampleId = "testId"; component.sample = sample; - const event: PageChangeEvent = { + const event: TablePagination = { pageIndex: 1, pageSize: 25, length: 25, }; - component.onPageChange(event); + component.onPaginationChange(event); expect(dispatchSpy).toHaveBeenCalledTimes(2); expect(dispatchSpy).toHaveBeenCalledWith( @@ -232,12 +229,15 @@ describe("SampleDetailComponent", () => { }); }); - describe("#onRowClick()", () => { + describe("#onRowEvent()", () => { it("should navigate to a dataset", () => { const dataset = mockDataset; dataset.pid = "testId"; - component.onRowClick(dataset as DatasetClass); + component.onRowEvent({ + event: RowEventType.RowClick, + sender: { row: dataset }, + } as any); expect(router.navigateByUrl).toHaveBeenCalledTimes(1); expect(router.navigateByUrl).toHaveBeenCalledWith( From ecc209d69265df8b8879625f9a31407e0f4e5dc0 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 10:04:45 +0200 Subject: [PATCH 03/11] added missing import --- src/app/samples/sample-detail/sample-detail.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/samples/sample-detail/sample-detail.component.spec.ts b/src/app/samples/sample-detail/sample-detail.component.spec.ts index 26360377fe..5b8c25c931 100644 --- a/src/app/samples/sample-detail/sample-detail.component.spec.ts +++ b/src/app/samples/sample-detail/sample-detail.component.spec.ts @@ -37,6 +37,7 @@ import { FlexLayoutModule } from "@ngbracket/ngx-layout"; import { AppConfigService } from "app-config.service"; import { ReturnedUserDto } from "@scicatproject/scicat-sdk-ts-angular"; import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; +import { TablePagination } from "shared/modules/dynamic-material-table/models/table-pagination.model"; const getConfig = () => ({ editMetadataEnabled: true, From c706a88eaf978b4f96b08670d530f437956dacab Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 10:55:21 +0200 Subject: [PATCH 04/11] tests updated using the correct endpoint --- .../state-management/effects/samples.effects.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/state-management/effects/samples.effects.spec.ts b/src/app/state-management/effects/samples.effects.spec.ts index 61c1d5a6b4..1dd3b7ca00 100644 --- a/src/app/state-management/effects/samples.effects.spec.ts +++ b/src/app/state-management/effects/samples.effects.spec.ts @@ -75,6 +75,7 @@ describe("SampleEffects", () => { provide: DatasetsService, useValue: jasmine.createSpyObj("datasetApi", [ "datasetsControllerFindAllV3", + "datasetsControllerCountV3", ]), }, ], @@ -285,16 +286,14 @@ describe("SampleEffects", () => { const sampleId = "testId"; it("should result in a fetchSampleDatasetsCountCompleteAction", () => { - const count = 1; - const datasets = [dataset]; const action = fromActions.fetchSampleDatasetsCountAction({ sampleId }); const outcome = fromActions.fetchSampleDatasetsCountCompleteAction({ - count, + count: 1, }); actions = hot("-a", { a: action }); - const response = cold("-a|", { a: datasets }); - datasetApi.datasetsControllerFindAllV3.and.returnValue(response); + const response = cold("-a|", { a: { count: 1 } }); + datasetApi.datasetsControllerCountV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchSampleDatasetsCount$).toBeObservable(expected); @@ -306,7 +305,7 @@ describe("SampleEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - datasetApi.datasetsControllerFindAllV3.and.returnValue(response); + datasetApi.datasetsControllerCountV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchSampleDatasetsCount$).toBeObservable(expected); From 65f7420c53e642b084cbd4b0ab8259c39c4ccbd7 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Mon, 11 May 2026 14:40:50 +0200 Subject: [PATCH 05/11] now reads from usersettings + defaultDatasetsListSettings --- .../sample-detail.component.html | 2 +- .../sample-detail.component.spec.ts | 52 +++++-- .../sample-detail/sample-detail.component.ts | 137 +++++++++++------- src/app/samples/samples.module.ts | 3 +- 4 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.html b/src/app/samples/sample-detail/sample-detail.component.html index 104f2ea743..384995da4a 100644 --- a/src/app/samples/sample-detail/sample-detail.component.html +++ b/src/app/samples/sample-detail/sample-detail.component.html @@ -169,7 +169,7 @@
({ editMetadataEnabled: true, @@ -67,7 +73,35 @@ describe("SampleDetailComponent", () => { SharedScicatFrontendModule, StoreModule.forRoot({}), ], - providers: [DatePipe, FileSizePipe, SlicePipe], + providers: [ + DatePipe, + FileSizePipe, + SlicePipe, + JsonHeadPipe, + provideMockStore({ + selectors: [ + { + selector: selectSampleDetailPageViewModel, + value: { + sample: mockSample, + user: null, + attachments: [], + datasets: [], + datasetsPage: 0, + datasetsPerPage: 25, + datasetsCount: 0, + }, + }, + { + selector: selectColumnsWithHasFetchedSettings, + value: { + columns: [], + hasFetchedSettings: true, + }, + }, + ], + }), + ], }); TestBed.overrideComponent(SampleDetailComponent, { set: { @@ -80,6 +114,7 @@ describe("SampleDetailComponent", () => { }, { provide: Router, useValue: router }, { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: DatasetsListService, useClass: MockDatasetsListService }, ], }, }); @@ -104,21 +139,6 @@ describe("SampleDetailComponent", () => { expect(component).toBeTruthy(); }); - describe("#formatTableData()", () => { - it("should return empty array if there are no datasets", () => { - const data = component.formatTableData(null); - - expect(data).toEqual([]); - }); - - it("should return an array of data objects if there are datasets", () => { - const datasets = [mockDataset]; - const data = component.formatTableData(datasets); - - expect(data.length).toEqual(1); - }); - }); - describe("#onSaveCharacteristics()", () => { it("should dispatch a saveCharacteristicsAction", () => { dispatchSpy = spyOn(store, "dispatch"); diff --git a/src/app/samples/sample-detail/sample-detail.component.ts b/src/app/samples/sample-detail/sample-detail.component.ts index 57b6617046..cb2d55cce6 100644 --- a/src/app/samples/sample-detail/sample-detail.component.ts +++ b/src/app/samples/sample-detail/sample-detail.component.ts @@ -1,6 +1,12 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Component, OnDestroy, OnInit } from "@angular/core"; -import { BehaviorSubject, fromEvent, Subscription } from "rxjs"; +import { + BehaviorSubject, + fromEvent, + Subscription, + take, + combineLatest, +} from "rxjs"; import { selectSampleDetailPageViewModel } from "../../state-management/selectors/samples.selectors"; import { Store } from "@ngrx/store"; import { @@ -13,8 +19,6 @@ import { removeAttachmentAction, fetchSampleAttachmentsAction, } from "../../state-management/actions/samples.actions"; -import { DatePipe, SlicePipe } from "@angular/common"; -import { FileSizePipe } from "shared/pipes/filesize.pipe"; import { PickedFile, SubmitCaptionEvent, @@ -39,16 +43,11 @@ import { RowEventType, TableSelectionMode, } from "shared/modules/dynamic-material-table/models/table-row.model"; - -export interface TableData { - pid: string; - name: string; - sourceFolder: string; - size: string; - creationTime: string | null; - owner: string; - location: string; -} +import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings"; +import { TableConfigService } from "shared/services/table-config.service"; +import { DatasetsListService } from "shared/services/datasets-list.service"; +import { TableColumn } from "state-management/models"; +import { selectColumnsWithHasFetchedSettings } from "state-management/selectors/user.selectors"; @Component({ selector: "app-sample-detail", @@ -62,6 +61,16 @@ export class SampleDetailComponent private _hasUnsavedChanges = false; vm$ = this.store.select(selectSampleDetailPageViewModel); + selectColumnsWithFetchedSettings$ = this.store.select( + selectColumnsWithHasFetchedSettings, + ); + + tableName = "sampleDatasetsTable"; + + columns: TableField[]; + + setting: ITableSetting = {}; + appConfig = this.appConfigService.getConfig(); sample: OutputSampleDto; @@ -71,24 +80,26 @@ export class SampleDetailComponent show = false; subscriptions: Subscription[] = []; - tableColumns: TableField[] = [ - { name: "name", header: "Name" }, - { name: "sourceFolder", header: "Source Folder" }, - { name: "size", header: "Size" }, - { name: "creationTime", header: "Creation Time" }, - { name: "owner", header: "Owner" }, - { name: "location", header: "Location" }, - ]; - - setting: ITableSetting = { + tableDefaultSettingsConfig: ITableSetting = { + visibleActionMenu: actionMenu, + saveSettingMode: "none", + settingList: [ + { + visibleActionMenu: actionMenu, + saveSettingMode: "none", + isDefaultSetting: true, + isCurrentSetting: true, + columnSetting: [], + }, + ], rowStyle: { "border-bottom": "1px solid #d2d2d2", }, }; - dataSource: BehaviorSubject = new BehaviorSubject( - [], - ); + dataSource: BehaviorSubject = new BehaviorSubject< + OutputDatasetObsoleteDto[] + >([]); paginationMode: TablePaginationMode = "server-side"; @@ -103,32 +114,24 @@ export class SampleDetailComponent constructor( private appConfigService: AppConfigService, - private datePipe: DatePipe, - private filesizePipe: FileSizePipe, private router: Router, private route: ActivatedRoute, - private slicePipe: SlicePipe, private store: Store, + private tableConfigService: TableConfigService, + private datasetsListService: DatasetsListService, ) {} - formatTableData(datasets: OutputDatasetObsoleteDto[]): TableData[] { - let tableData: TableData[] = []; - if (datasets) { - tableData = datasets.map((dataset: any) => ({ - pid: dataset.pid, - name: dataset.datasetName, - sourceFolder: - "..." + this.slicePipe.transform(dataset.sourceFolder, -14), - size: this.filesizePipe.transform(dataset.size), - creationTime: this.datePipe.transform( - dataset.creationTime, - "yyyy-MM-dd HH:mm", - ), - owner: dataset.owner, - location: dataset.creationLocation, - })); - } - return tableData; + initTable( + settingConfig: ITableSetting, + paginationConfig: TablePagination, + ): void { + const currentColumnSetting = settingConfig.settingList.find( + (s) => s.isCurrentSetting, + )?.columnSetting; + + this.columns = currentColumnSetting; + this.setting = settingConfig; + this.pagination = paginationConfig; } onSaveCharacteristics(characteristics: Record) { @@ -180,7 +183,7 @@ export class SampleDetailComponent ); } - onRowEvent(event: IRowEvent) { + onRowEvent(event: IRowEvent) { if (event.event === RowEventType.RowClick) { const id = encodeURIComponent(event.sender.row.pid); this.router.navigateByUrl("/datasets/" + id); @@ -189,7 +192,10 @@ export class SampleDetailComponent ngOnInit() { this.subscriptions.push( - this.vm$.subscribe((vm) => { + combineLatest([ + this.vm$, + this.selectColumnsWithFetchedSettings$.pipe(take(1)), + ]).subscribe(([vm, defaultTableColumns]) => { if (vm.sample) { this.sample = vm.sample; @@ -202,7 +208,38 @@ export class SampleDetailComponent this.attachments = vm.attachments; } - this.dataSource.next(this.formatTableData(vm.datasets)); + this.dataSource.next(vm.datasets); + + const defaultConfigColumns = + this.appConfig?.defaultDatasetsListSettings?.columns || []; + + const userTableConfigColumns = + this.datasetsListService.convertSavedDatasetColumns( + defaultTableColumns.columns, + ); + + this.tableDefaultSettingsConfig.settingList[0].columnSetting = + this.datasetsListService.convertSavedDatasetColumns( + defaultConfigColumns as TableColumn[], + ); + + const tableSettingsConfig = + this.tableConfigService.getTableSettingsConfig( + this.tableName, + this.tableDefaultSettingsConfig, + userTableConfigColumns, + ); + + const paginationConfig = { + pageSizeOptions: [5, 10, 25, 50, 100], + pageIndex: vm.datasetsPage || 0, + pageSize: vm.datasetsPerPage || this.pagination.pageSize, + length: vm.datasetsCount || 0, + }; + + if (tableSettingsConfig?.settingList.length) { + this.initTable(tableSettingsConfig, paginationConfig); + } this.pagination = { ...this.pagination, pageIndex: vm.datasetsPage || 0, diff --git a/src/app/samples/samples.module.ts b/src/app/samples/samples.module.ts index 06e615acdd..a50d02a725 100644 --- a/src/app/samples/samples.module.ts +++ b/src/app/samples/samples.module.ts @@ -23,6 +23,7 @@ import { MatChipsModule } from "@angular/material/chips"; import { FileSizePipe } from "shared/pipes/filesize.pipe"; import { MatExpansionModule } from "@angular/material/expansion"; import { SharedConditionModule } from "shared/modules/shared-condition/shared-condition.module"; +import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; @NgModule({ imports: [ @@ -52,6 +53,6 @@ import { SharedConditionModule } from "shared/modules/shared-condition/shared-co SampleDialogComponent, SampleDashboardComponent, ], - providers: [FileSizePipe], + providers: [FileSizePipe, JsonHeadPipe], }) export class SamplesModule {} From 9f1412d009bf5bf51d8046f4bc51c55e3844a09c Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Mon, 27 Apr 2026 16:29:44 +0200 Subject: [PATCH 06/11] replaced sample datasets table with dynamic-mat-table and fixed pagination bugs --- .../sample-detail.component.html | 23 +++--- .../sample-detail/sample-detail.component.ts | 82 +++++++++++++------ .../effects/samples.effects.ts | 14 ++-- 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.html b/src/app/samples/sample-detail/sample-detail.component.html index 4d02b56f88..104f2ea743 100644 --- a/src/app/samples/sample-detail/sample-detail.component.html +++ b/src/app/samples/sample-detail/sample-detail.component.html @@ -164,20 +164,23 @@ - folder Datasets + folder Related Datasets
- + [dataSource]="dataSource" + [setting]="setting" + [pagingMode]="paginationMode" + [pagination]="pagination" + [rowSelectionMode]="rowSelectionMode" + [showGlobalTextSearch]="false" + [emptyMessage]="'No datasets available'" + [emptyIcon]="'folder'" + (paginationChange)="onPaginationChange($event)" + (onRowEvent)="onRowEvent($event)" + >
diff --git a/src/app/samples/sample-detail/sample-detail.component.ts b/src/app/samples/sample-detail/sample-detail.component.ts index c73c583b94..57b6617046 100644 --- a/src/app/samples/sample-detail/sample-detail.component.ts +++ b/src/app/samples/sample-detail/sample-detail.component.ts @@ -1,6 +1,6 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Component, OnDestroy, OnInit } from "@angular/core"; -import { fromEvent, Subscription } from "rxjs"; +import { BehaviorSubject, fromEvent, Subscription } from "rxjs"; import { selectSampleDetailPageViewModel } from "../../state-management/selectors/samples.selectors"; import { Store } from "@ngrx/store"; import { @@ -15,10 +15,6 @@ import { } from "../../state-management/actions/samples.actions"; import { DatePipe, SlicePipe } from "@angular/common"; import { FileSizePipe } from "shared/pipes/filesize.pipe"; -import { - TableColumn, - PageChangeEvent, -} from "shared/modules/table/table.component"; import { PickedFile, SubmitCaptionEvent, @@ -27,12 +23,22 @@ import { EditableComponent } from "app-routing/pending-changes.guard"; import { AppConfigService } from "app-config.service"; import { CreateAttachmentV3Dto, - DatasetClass, OutputAttachmentV3Dto, OutputDatasetObsoleteDto, ReturnedUserDto, OutputSampleDto, } from "@scicatproject/scicat-sdk-ts-angular"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; +import { ITableSetting } from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { + IRowEvent, + RowEventType, + TableSelectionMode, +} from "shared/modules/dynamic-material-table/models/table-row.model"; export interface TableData { pid: string; @@ -65,17 +71,36 @@ export class SampleDetailComponent show = false; subscriptions: Subscription[] = []; - tableData: TableData[] = []; - tablePaginate = true; - tableColumns: TableColumn[] = [ - { name: "name", icon: "portrait", sort: false, inList: true }, - { name: "sourceFolder", icon: "explore", sort: false, inList: true }, - { name: "size", icon: "save", sort: false, inList: true }, - { name: "creationTime", icon: "calendar_today", sort: false, inList: true }, - { name: "owner", icon: "face", sort: false, inList: true }, - { name: "location", icon: "explore", sort: false, inList: true }, + tableColumns: TableField[] = [ + { name: "name", header: "Name" }, + { name: "sourceFolder", header: "Source Folder" }, + { name: "size", header: "Size" }, + { name: "creationTime", header: "Creation Time" }, + { name: "owner", header: "Owner" }, + { name: "location", header: "Location" }, ]; + setting: ITableSetting = { + rowStyle: { + "border-bottom": "1px solid #d2d2d2", + }, + }; + + dataSource: BehaviorSubject = new BehaviorSubject( + [], + ); + + paginationMode: TablePaginationMode = "server-side"; + + pagination: TablePagination = { + pageSizeOptions: [5, 10, 25, 50, 100], + pageIndex: 0, + pageSize: 25, + length: 0, + }; + + rowSelectionMode: TableSelectionMode = "none"; + constructor( private appConfigService: AppConfigService, private datePipe: DatePipe, @@ -143,11 +168,11 @@ export class SampleDetailComponent ); } - onPageChange(event: PageChangeEvent) { + onPaginationChange({ pageIndex, pageSize }: TablePagination) { this.store.dispatch( changeDatasetsPageAction({ - page: event.pageIndex, - limit: event.pageSize, + page: pageIndex, + limit: pageSize, }), ); this.store.dispatch( @@ -155,9 +180,11 @@ export class SampleDetailComponent ); } - onRowClick(dataset: DatasetClass) { - const id = encodeURIComponent(dataset.pid); - this.router.navigateByUrl("/datasets/" + id); + onRowEvent(event: IRowEvent) { + if (event.event === RowEventType.RowClick) { + const id = encodeURIComponent(event.sender.row.pid); + this.router.navigateByUrl("/datasets/" + id); + } } ngOnInit() { @@ -174,6 +201,14 @@ export class SampleDetailComponent if (vm.attachments) { this.attachments = vm.attachments; } + + this.dataSource.next(this.formatTableData(vm.datasets)); + this.pagination = { + ...this.pagination, + pageIndex: vm.datasetsPage || 0, + pageSize: vm.datasetsPerPage || this.pagination.pageSize, + length: vm.datasetsCount || 0, + }; }), ); // Prevent user from reloading page if there are unsave changes @@ -184,11 +219,6 @@ export class SampleDetailComponent } }), ); - this.subscriptions.push( - this.vm$.subscribe((vm) => { - this.tableData = this.formatTableData(vm.datasets); - }), - ); this.subscriptions.push( this.vm$.subscribe((vm) => { diff --git a/src/app/state-management/effects/samples.effects.ts b/src/app/state-management/effects/samples.effects.ts index 03a63e233d..1373e4d4cc 100644 --- a/src/app/state-management/effects/samples.effects.ts +++ b/src/app/state-management/effects/samples.effects.ts @@ -134,9 +134,11 @@ export class SampleEffects { .datasetsControllerFindAllV3( JSON.stringify({ where: { sampleId }, - order, - skip, - limit, + limits: { + order, + skip, + limit, + }, }), ) .pipe( @@ -155,11 +157,11 @@ export class SampleEffects { ofType(fromActions.fetchSampleDatasetsCountAction), switchMap(({ sampleId }) => this.datasetApi - .datasetsControllerFindAllV3(JSON.stringify({ where: { sampleId } })) + .datasetsControllerCountV3(JSON.stringify({ where: { sampleId } })) .pipe( - map((datasets) => + map(({ count }) => fromActions.fetchSampleDatasetsCountCompleteAction({ - count: datasets.length, + count, }), ), catchError(() => From 980ceafcb0d15cdc0149e7b6c0a30704f532b0b5 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 09:26:44 +0200 Subject: [PATCH 07/11] test fix --- .../sample-detail.component.spec.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.spec.ts b/src/app/samples/sample-detail/sample-detail.component.spec.ts index f936d04b41..26360377fe 100644 --- a/src/app/samples/sample-detail/sample-detail.component.spec.ts +++ b/src/app/samples/sample-detail/sample-detail.component.spec.ts @@ -16,7 +16,6 @@ import { waitForAsync, } from "@angular/core/testing"; import { NgxJsonViewerModule } from "ngx-json-viewer"; -import { PageChangeEvent } from "shared/modules/table/table.component"; import { changeDatasetsPageAction, fetchSampleDatasetsAction, @@ -36,10 +35,8 @@ import { MatIconModule } from "@angular/material/icon"; import { MatTabsModule } from "@angular/material/tabs"; import { FlexLayoutModule } from "@ngbracket/ngx-layout"; import { AppConfigService } from "app-config.service"; -import { - DatasetClass, - ReturnedUserDto, -} from "@scicatproject/scicat-sdk-ts-angular"; +import { ReturnedUserDto } from "@scicatproject/scicat-sdk-ts-angular"; +import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; const getConfig = () => ({ editMetadataEnabled: true, @@ -204,20 +201,20 @@ describe("SampleDetailComponent", () => { }); }); - describe("#onPageChange()", () => { + describe("#onPaginationChange()", () => { it("should dispatch a changeDatasetsPageAction and a fetchSampleDatasetsAction", () => { dispatchSpy = spyOn(store, "dispatch"); const sample = mockSample; sample.sampleId = "testId"; component.sample = sample; - const event: PageChangeEvent = { + const event: TablePagination = { pageIndex: 1, pageSize: 25, length: 25, }; - component.onPageChange(event); + component.onPaginationChange(event); expect(dispatchSpy).toHaveBeenCalledTimes(2); expect(dispatchSpy).toHaveBeenCalledWith( @@ -232,12 +229,15 @@ describe("SampleDetailComponent", () => { }); }); - describe("#onRowClick()", () => { + describe("#onRowEvent()", () => { it("should navigate to a dataset", () => { const dataset = mockDataset; dataset.pid = "testId"; - component.onRowClick(dataset as DatasetClass); + component.onRowEvent({ + event: RowEventType.RowClick, + sender: { row: dataset }, + } as any); expect(router.navigateByUrl).toHaveBeenCalledTimes(1); expect(router.navigateByUrl).toHaveBeenCalledWith( From 4018afdec462b1478d449817e08a2a9def4ac85c Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 10:04:45 +0200 Subject: [PATCH 08/11] added missing import --- src/app/samples/sample-detail/sample-detail.component.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/samples/sample-detail/sample-detail.component.spec.ts b/src/app/samples/sample-detail/sample-detail.component.spec.ts index 26360377fe..5b8c25c931 100644 --- a/src/app/samples/sample-detail/sample-detail.component.spec.ts +++ b/src/app/samples/sample-detail/sample-detail.component.spec.ts @@ -37,6 +37,7 @@ import { FlexLayoutModule } from "@ngbracket/ngx-layout"; import { AppConfigService } from "app-config.service"; import { ReturnedUserDto } from "@scicatproject/scicat-sdk-ts-angular"; import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; +import { TablePagination } from "shared/modules/dynamic-material-table/models/table-pagination.model"; const getConfig = () => ({ editMetadataEnabled: true, From 254915e846f0e5162bcbee55c8b4dbf0cb4aa3c1 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 29 Apr 2026 10:55:21 +0200 Subject: [PATCH 09/11] tests updated using the correct endpoint --- .../state-management/effects/samples.effects.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/state-management/effects/samples.effects.spec.ts b/src/app/state-management/effects/samples.effects.spec.ts index 61c1d5a6b4..1dd3b7ca00 100644 --- a/src/app/state-management/effects/samples.effects.spec.ts +++ b/src/app/state-management/effects/samples.effects.spec.ts @@ -75,6 +75,7 @@ describe("SampleEffects", () => { provide: DatasetsService, useValue: jasmine.createSpyObj("datasetApi", [ "datasetsControllerFindAllV3", + "datasetsControllerCountV3", ]), }, ], @@ -285,16 +286,14 @@ describe("SampleEffects", () => { const sampleId = "testId"; it("should result in a fetchSampleDatasetsCountCompleteAction", () => { - const count = 1; - const datasets = [dataset]; const action = fromActions.fetchSampleDatasetsCountAction({ sampleId }); const outcome = fromActions.fetchSampleDatasetsCountCompleteAction({ - count, + count: 1, }); actions = hot("-a", { a: action }); - const response = cold("-a|", { a: datasets }); - datasetApi.datasetsControllerFindAllV3.and.returnValue(response); + const response = cold("-a|", { a: { count: 1 } }); + datasetApi.datasetsControllerCountV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchSampleDatasetsCount$).toBeObservable(expected); @@ -306,7 +305,7 @@ describe("SampleEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - datasetApi.datasetsControllerFindAllV3.and.returnValue(response); + datasetApi.datasetsControllerCountV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchSampleDatasetsCount$).toBeObservable(expected); From 8e344c2eb4d49c43b0eac2e6887b7995d125cc98 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Mon, 11 May 2026 14:40:50 +0200 Subject: [PATCH 10/11] now reads from usersettings + defaultDatasetsListSettings --- .../sample-detail.component.html | 2 +- .../sample-detail.component.spec.ts | 52 +++++-- .../sample-detail/sample-detail.component.ts | 137 +++++++++++------- src/app/samples/samples.module.ts | 3 +- 4 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/app/samples/sample-detail/sample-detail.component.html b/src/app/samples/sample-detail/sample-detail.component.html index 104f2ea743..384995da4a 100644 --- a/src/app/samples/sample-detail/sample-detail.component.html +++ b/src/app/samples/sample-detail/sample-detail.component.html @@ -169,7 +169,7 @@
({ editMetadataEnabled: true, @@ -67,7 +73,35 @@ describe("SampleDetailComponent", () => { SharedScicatFrontendModule, StoreModule.forRoot({}), ], - providers: [DatePipe, FileSizePipe, SlicePipe], + providers: [ + DatePipe, + FileSizePipe, + SlicePipe, + JsonHeadPipe, + provideMockStore({ + selectors: [ + { + selector: selectSampleDetailPageViewModel, + value: { + sample: mockSample, + user: null, + attachments: [], + datasets: [], + datasetsPage: 0, + datasetsPerPage: 25, + datasetsCount: 0, + }, + }, + { + selector: selectColumnsWithHasFetchedSettings, + value: { + columns: [], + hasFetchedSettings: true, + }, + }, + ], + }), + ], }); TestBed.overrideComponent(SampleDetailComponent, { set: { @@ -80,6 +114,7 @@ describe("SampleDetailComponent", () => { }, { provide: Router, useValue: router }, { provide: ActivatedRoute, useClass: MockActivatedRoute }, + { provide: DatasetsListService, useClass: MockDatasetsListService }, ], }, }); @@ -104,21 +139,6 @@ describe("SampleDetailComponent", () => { expect(component).toBeTruthy(); }); - describe("#formatTableData()", () => { - it("should return empty array if there are no datasets", () => { - const data = component.formatTableData(null); - - expect(data).toEqual([]); - }); - - it("should return an array of data objects if there are datasets", () => { - const datasets = [mockDataset]; - const data = component.formatTableData(datasets); - - expect(data.length).toEqual(1); - }); - }); - describe("#onSaveCharacteristics()", () => { it("should dispatch a saveCharacteristicsAction", () => { dispatchSpy = spyOn(store, "dispatch"); diff --git a/src/app/samples/sample-detail/sample-detail.component.ts b/src/app/samples/sample-detail/sample-detail.component.ts index 57b6617046..cb2d55cce6 100644 --- a/src/app/samples/sample-detail/sample-detail.component.ts +++ b/src/app/samples/sample-detail/sample-detail.component.ts @@ -1,6 +1,12 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Component, OnDestroy, OnInit } from "@angular/core"; -import { BehaviorSubject, fromEvent, Subscription } from "rxjs"; +import { + BehaviorSubject, + fromEvent, + Subscription, + take, + combineLatest, +} from "rxjs"; import { selectSampleDetailPageViewModel } from "../../state-management/selectors/samples.selectors"; import { Store } from "@ngrx/store"; import { @@ -13,8 +19,6 @@ import { removeAttachmentAction, fetchSampleAttachmentsAction, } from "../../state-management/actions/samples.actions"; -import { DatePipe, SlicePipe } from "@angular/common"; -import { FileSizePipe } from "shared/pipes/filesize.pipe"; import { PickedFile, SubmitCaptionEvent, @@ -39,16 +43,11 @@ import { RowEventType, TableSelectionMode, } from "shared/modules/dynamic-material-table/models/table-row.model"; - -export interface TableData { - pid: string; - name: string; - sourceFolder: string; - size: string; - creationTime: string | null; - owner: string; - location: string; -} +import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings"; +import { TableConfigService } from "shared/services/table-config.service"; +import { DatasetsListService } from "shared/services/datasets-list.service"; +import { TableColumn } from "state-management/models"; +import { selectColumnsWithHasFetchedSettings } from "state-management/selectors/user.selectors"; @Component({ selector: "app-sample-detail", @@ -62,6 +61,16 @@ export class SampleDetailComponent private _hasUnsavedChanges = false; vm$ = this.store.select(selectSampleDetailPageViewModel); + selectColumnsWithFetchedSettings$ = this.store.select( + selectColumnsWithHasFetchedSettings, + ); + + tableName = "sampleDatasetsTable"; + + columns: TableField[]; + + setting: ITableSetting = {}; + appConfig = this.appConfigService.getConfig(); sample: OutputSampleDto; @@ -71,24 +80,26 @@ export class SampleDetailComponent show = false; subscriptions: Subscription[] = []; - tableColumns: TableField[] = [ - { name: "name", header: "Name" }, - { name: "sourceFolder", header: "Source Folder" }, - { name: "size", header: "Size" }, - { name: "creationTime", header: "Creation Time" }, - { name: "owner", header: "Owner" }, - { name: "location", header: "Location" }, - ]; - - setting: ITableSetting = { + tableDefaultSettingsConfig: ITableSetting = { + visibleActionMenu: actionMenu, + saveSettingMode: "none", + settingList: [ + { + visibleActionMenu: actionMenu, + saveSettingMode: "none", + isDefaultSetting: true, + isCurrentSetting: true, + columnSetting: [], + }, + ], rowStyle: { "border-bottom": "1px solid #d2d2d2", }, }; - dataSource: BehaviorSubject = new BehaviorSubject( - [], - ); + dataSource: BehaviorSubject = new BehaviorSubject< + OutputDatasetObsoleteDto[] + >([]); paginationMode: TablePaginationMode = "server-side"; @@ -103,32 +114,24 @@ export class SampleDetailComponent constructor( private appConfigService: AppConfigService, - private datePipe: DatePipe, - private filesizePipe: FileSizePipe, private router: Router, private route: ActivatedRoute, - private slicePipe: SlicePipe, private store: Store, + private tableConfigService: TableConfigService, + private datasetsListService: DatasetsListService, ) {} - formatTableData(datasets: OutputDatasetObsoleteDto[]): TableData[] { - let tableData: TableData[] = []; - if (datasets) { - tableData = datasets.map((dataset: any) => ({ - pid: dataset.pid, - name: dataset.datasetName, - sourceFolder: - "..." + this.slicePipe.transform(dataset.sourceFolder, -14), - size: this.filesizePipe.transform(dataset.size), - creationTime: this.datePipe.transform( - dataset.creationTime, - "yyyy-MM-dd HH:mm", - ), - owner: dataset.owner, - location: dataset.creationLocation, - })); - } - return tableData; + initTable( + settingConfig: ITableSetting, + paginationConfig: TablePagination, + ): void { + const currentColumnSetting = settingConfig.settingList.find( + (s) => s.isCurrentSetting, + )?.columnSetting; + + this.columns = currentColumnSetting; + this.setting = settingConfig; + this.pagination = paginationConfig; } onSaveCharacteristics(characteristics: Record) { @@ -180,7 +183,7 @@ export class SampleDetailComponent ); } - onRowEvent(event: IRowEvent) { + onRowEvent(event: IRowEvent) { if (event.event === RowEventType.RowClick) { const id = encodeURIComponent(event.sender.row.pid); this.router.navigateByUrl("/datasets/" + id); @@ -189,7 +192,10 @@ export class SampleDetailComponent ngOnInit() { this.subscriptions.push( - this.vm$.subscribe((vm) => { + combineLatest([ + this.vm$, + this.selectColumnsWithFetchedSettings$.pipe(take(1)), + ]).subscribe(([vm, defaultTableColumns]) => { if (vm.sample) { this.sample = vm.sample; @@ -202,7 +208,38 @@ export class SampleDetailComponent this.attachments = vm.attachments; } - this.dataSource.next(this.formatTableData(vm.datasets)); + this.dataSource.next(vm.datasets); + + const defaultConfigColumns = + this.appConfig?.defaultDatasetsListSettings?.columns || []; + + const userTableConfigColumns = + this.datasetsListService.convertSavedDatasetColumns( + defaultTableColumns.columns, + ); + + this.tableDefaultSettingsConfig.settingList[0].columnSetting = + this.datasetsListService.convertSavedDatasetColumns( + defaultConfigColumns as TableColumn[], + ); + + const tableSettingsConfig = + this.tableConfigService.getTableSettingsConfig( + this.tableName, + this.tableDefaultSettingsConfig, + userTableConfigColumns, + ); + + const paginationConfig = { + pageSizeOptions: [5, 10, 25, 50, 100], + pageIndex: vm.datasetsPage || 0, + pageSize: vm.datasetsPerPage || this.pagination.pageSize, + length: vm.datasetsCount || 0, + }; + + if (tableSettingsConfig?.settingList.length) { + this.initTable(tableSettingsConfig, paginationConfig); + } this.pagination = { ...this.pagination, pageIndex: vm.datasetsPage || 0, diff --git a/src/app/samples/samples.module.ts b/src/app/samples/samples.module.ts index 06e615acdd..a50d02a725 100644 --- a/src/app/samples/samples.module.ts +++ b/src/app/samples/samples.module.ts @@ -23,6 +23,7 @@ import { MatChipsModule } from "@angular/material/chips"; import { FileSizePipe } from "shared/pipes/filesize.pipe"; import { MatExpansionModule } from "@angular/material/expansion"; import { SharedConditionModule } from "shared/modules/shared-condition/shared-condition.module"; +import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; @NgModule({ imports: [ @@ -52,6 +53,6 @@ import { SharedConditionModule } from "shared/modules/shared-condition/shared-co SampleDialogComponent, SampleDashboardComponent, ], - providers: [FileSizePipe], + providers: [FileSizePipe, JsonHeadPipe], }) export class SamplesModule {} From aa90e982de76c0ddee09ed75c364a08bf903ba25 Mon Sep 17 00:00:00 2001 From: Abdi Mo Date: Wed, 24 Jun 2026 14:11:37 +0200 Subject: [PATCH 11/11] error fix: added instrumentsReducer in samples module --- src/app/samples/samples.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/samples/samples.module.ts b/src/app/samples/samples.module.ts index a50d02a725..7742233079 100644 --- a/src/app/samples/samples.module.ts +++ b/src/app/samples/samples.module.ts @@ -24,6 +24,7 @@ import { FileSizePipe } from "shared/pipes/filesize.pipe"; import { MatExpansionModule } from "@angular/material/expansion"; import { SharedConditionModule } from "shared/modules/shared-condition/shared-condition.module"; import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; +import { instrumentsReducer } from "state-management/reducers/instruments.reducer"; @NgModule({ imports: [ @@ -46,6 +47,7 @@ import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; StoreModule.forFeature("samples", samplesReducer), MatExpansionModule, SharedConditionModule, + StoreModule.forFeature("instruments", instrumentsReducer), ], exports: [SampleDetailComponent, SampleDialogComponent], declarations: [