-
+
add_circle
Create Dataset
@@ -31,27 +25,13 @@
+ [selectedSets]="selectedSets$ | async">
-
+ (pageChange)="onPageChange($event)">
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/app/datasets/dashboard/dashboard.component.spec.ts b/src/app/datasets/dashboard/dashboard.component.spec.ts
index be1c837c4..dc7f23a4c 100644
--- a/src/app/datasets/dashboard/dashboard.component.spec.ts
+++ b/src/app/datasets/dashboard/dashboard.component.spec.ts
@@ -20,22 +20,15 @@ import {
addDatasetAction,
changePageAction,
} from "state-management/actions/datasets.actions";
-import {
- selectColumnAction,
- deselectColumnAction,
-} from "state-management/actions/user.actions";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
-import { SelectColumnEvent } from "datasets/dataset-table-settings/dataset-table-settings.component";
import { provideMockStore } from "@ngrx/store/testing";
import { selectSelectedDatasets } from "state-management/selectors/datasets.selectors";
-import { TableColumn } from "state-management/models";
import {
selectColumns,
selectIsLoggedIn,
} from "state-management/selectors/user.selectors";
import { MatDialog, MatDialogModule } from "@angular/material/dialog";
-import { MatSidenav, MatSidenavModule } from "@angular/material/sidenav";
-import { MatCheckboxChange } from "@angular/material/checkbox";
+import { MatSidenavModule } from "@angular/material/sidenav";
import { MatCardModule } from "@angular/material/card";
import { MatIconModule } from "@angular/material/icon";
import { AppConfigService } from "app-config.service";
@@ -129,91 +122,6 @@ describe("DashboardComponent", () => {
expect(component).toBeTruthy();
});
- describe("#onSettingsClick()", () => {
- it("should toggle the sideNav", () => {
- const toggleSpy = spyOn(component.sideNav, "toggle");
-
- component.onSettingsClick();
-
- expect(toggleSpy).toHaveBeenCalled();
- });
-
- it("should not clear the search column if sidenav is open", () => {
- component.sideNav.opened = false;
- // The opened status is toggled when onSettingsClick is called
- component.onSettingsClick();
-
- expect(component.clearColumnSearch).toEqual(false);
- });
-
- it("should clear the search column if sidenav is closed", () => {
- component.sideNav.opened = true;
- // The opened status is toggled when onSettingsClick is called
- component.onSettingsClick();
-
- expect(component.clearColumnSearch).toEqual(true);
- });
- });
-
- describe("#onCloseClick()", () => {
- it("should close the sideNav", () => {
- const closeSpy = spyOn(component.sideNav, "close");
-
- component.onCloseClick();
-
- expect(closeSpy).toHaveBeenCalled();
- });
- });
-
- describe("#onSelectColumn()", () => {
- const column: TableColumn = {
- name: "test",
- order: 0,
- type: "standard",
- enabled: false,
- };
-
- it("should dispatch a selectColumnAction if checkBoxChange.checked is true", () => {
- dispatchSpy = spyOn(store, "dispatch");
-
- const checkBoxChange = {
- checked: true,
- } as MatCheckboxChange;
-
- const event: SelectColumnEvent = {
- checkBoxChange,
- column,
- };
-
- component.onSelectColumn(event);
-
- expect(dispatchSpy).toHaveBeenCalledTimes(1);
- expect(dispatchSpy).toHaveBeenCalledWith(
- selectColumnAction({ name: column.name, columnType: column.type }),
- );
- });
-
- it("should dispatch a deselectColumnAction if checkBoxChange.checked is false", () => {
- dispatchSpy = spyOn(store, "dispatch");
-
- const checkBoxChange = {
- checked: false,
- } as MatCheckboxChange;
-
- const event: SelectColumnEvent = {
- checkBoxChange,
- column,
- };
-
- component.onSelectColumn(event);
-
- expect(dispatchSpy).toHaveBeenCalledTimes(1);
- expect(dispatchSpy).toHaveBeenCalledWith(
- deselectColumnAction({ name: column.name, columnType: column.type }),
- );
- });
- });
-
describe("#onRowClick()", () => {
it("should navigate to a dataset", () => {
component.onRowClick(dataset);
@@ -296,32 +204,4 @@ describe("DashboardComponent", () => {
);
});
});
-
- describe("#tableColumn$ observable", () => {
- it("should show 'select' column when user is logged in", () => {
- const testColumn: TableColumn = {
- name: "test",
- order: 0,
- type: "standard",
- enabled: false,
- };
- const selectColumn: TableColumn = {
- name: "select",
- order: 1,
- type: "standard",
- enabled: true,
- };
- selectColumns.setResult([testColumn, selectColumn]);
- selectIsLoggedIn.setResult(true);
-
- component.tableColumns$.subscribe((result) => {
- expect(result.length).toEqual(2);
- });
-
- selectIsLoggedIn.setResult(false);
- component.tableColumns$.subscribe((result) =>
- expect(result).toEqual([testColumn]),
- );
- });
- });
});
diff --git a/src/app/datasets/dashboard/dashboard.component.ts b/src/app/datasets/dashboard/dashboard.component.ts
index 3e7413ad6..5c1c6bb8d 100644
--- a/src/app/datasets/dashboard/dashboard.component.ts
+++ b/src/app/datasets/dashboard/dashboard.component.ts
@@ -28,23 +28,19 @@ import { distinctUntilChanged, filter, map, take } from "rxjs/operators";
import { MatDialog } from "@angular/material/dialog";
import { MatSidenav } from "@angular/material/sidenav";
import { AddDatasetDialogComponent } from "datasets/add-dataset-dialog/add-dataset-dialog.component";
-import { combineLatest, Subscription } from "rxjs";
+import { combineLatest, Subscription, lastValueFrom } from "rxjs";
import {
selectProfile,
selectCurrentUser,
selectColumns,
selectIsLoggedIn,
+ selectHasFetchedSettings,
} from "state-management/selectors/user.selectors";
import {
OutputDatasetObsoleteDto,
ReturnedUserDto,
} from "@scicatproject/scicat-sdk-ts-angular";
-import {
- selectColumnAction,
- deselectColumnAction,
- loadDefaultSettings,
-} from "state-management/actions/user.actions";
-import { SelectColumnEvent } from "datasets/dataset-table-settings/dataset-table-settings.component";
+import { loadDefaultSettings } from "state-management/actions/user.actions";
import { AppConfigService } from "app-config.service";
@Component({
@@ -61,15 +57,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
loggedIn$ = this.store.select(selectIsLoggedIn);
selectedSets$ = this.store.select(selectSelectedDatasets);
selectColumns$ = this.store.select(selectColumns);
+ selectHasFetchedSettings$ = this.store.select(selectHasFetchedSettings);
- tableColumns$ = combineLatest([this.selectColumns$, this.loggedIn$]).pipe(
- map(([columns, loggedIn]) =>
- columns.filter((column) => loggedIn || column.name !== "select"),
- ),
- );
- selectableColumns$ = this.selectColumns$.pipe(
- map((columns) => columns.filter((column) => column.name !== "select")),
- );
public nonEmpty$ = this.store.select(selectIsBatchNonEmpty);
subscriptions: Subscription[] = [];
@@ -97,33 +86,6 @@ export class DashboardComponent implements OnInit, OnDestroy {
);
}
- onSettingsClick(): void {
- this.sideNav.toggle();
- if (this.sideNav.opened) {
- this.clearColumnSearch = false;
- } else {
- this.clearColumnSearch = true;
- }
- }
-
- onCloseClick(): void {
- this.clearColumnSearch = true;
- this.sideNav.close();
- }
-
- onSelectColumn(event: SelectColumnEvent): void {
- const { checkBoxChange, column } = event;
- if (checkBoxChange.checked) {
- this.store.dispatch(
- selectColumnAction({ name: column.name, columnType: column.type }),
- );
- } else if (!checkBoxChange.checked) {
- this.store.dispatch(
- deselectColumnAction({ name: column.name, columnType: column.type }),
- );
- }
- }
-
onRowClick(dataset: OutputDatasetObsoleteDto): void {
const pid = encodeURIComponent(dataset.pid);
this.router.navigateByUrl("/datasets/" + pid);
@@ -178,12 +140,29 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.store.dispatch(fetchMetadataKeysAction());
this.subscriptions.push(
- combineLatest([this.pagination$, this.readyToFetch$, this.loggedIn$])
+ combineLatest([
+ this.pagination$,
+ this.readyToFetch$,
+ this.loggedIn$,
+ this.selectHasFetchedSettings$,
+ ])
.pipe(
- map(([pagination, , loggedIn]) => [pagination, loggedIn]),
+ map(([pagination, , loggedIn, hasFetchedSettings]) => [
+ pagination,
+ loggedIn,
+ hasFetchedSettings,
+ ]),
distinctUntilChanged(deepEqual),
)
- .subscribe(([pagination, loggedIn]) => {
+ .subscribe(async ([pagination, loggedIn]) => {
+ const hasFetchedSettings = await lastValueFrom(
+ this.selectHasFetchedSettings$.pipe(take(1)),
+ );
+
+ if (!hasFetchedSettings) {
+ return;
+ }
+
this.store.dispatch(fetchDatasetsAction());
this.store.dispatch(fetchFacetCountsAction());
this.router.navigate(["/datasets"], {
diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html
index a840501b2..0910b29dd 100644
--- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html
+++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html
@@ -76,7 +76,12 @@
{{ value ?? "-" }}
diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts
index 98fe7ad01..85e5b957a 100644
--- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts
+++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts
@@ -16,6 +16,7 @@ import {
TranslationObject,
} from "@ngx-translate/core";
import { DatasetDetailDynamicComponent } from "./dataset-detail-dynamic.component";
+import { InternalLinkType } from "state-management/models";
class MockTranslateLoader implements TranslateLoader {
getTranslation(): Observable {
return of({});
@@ -75,6 +76,176 @@ describe("DatasetDetailDynamicComponent", () => {
expect(component).toBeTruthy();
});
+ describe("getNestedValue with instrument name resolution", () => {
+ it("should return instrument name when path is 'instrumentName' and instrument exists", () => {
+ component.instrument = {
+ pid: "instrument1",
+ name: "Test Instrument",
+ } as any;
+ const dataset = {} as any;
+ const result = component.getNestedValue(dataset, "instrumentName");
+ expect(result).toBe("Test Instrument");
+ });
+
+ it("should return '-' when path is 'instrumentName' but instrument has no name", () => {
+ component.instrument = { pid: "instrument1" } as any;
+ const dataset = {} as any;
+ const result = component.getNestedValue(dataset, "instrumentName");
+ expect(result).toBe("-");
+ });
+
+ it("should return undefined when path is 'instrumentName' but no instrument", () => {
+ component.instrument = undefined;
+ const dataset = {} as any;
+ const result = component.getNestedValue(dataset, "instrumentName");
+ expect(result).toBeUndefined();
+ });
+
+ it("should work normally for non-instrumentName paths", () => {
+ component.instrument = {
+ pid: "instrument1",
+ name: "Test Instrument",
+ } as any;
+ const dataset = { pid: "test-pid" } as any;
+ const result = component.getNestedValue(dataset, "pid");
+ expect(result).toBe("test-pid");
+ });
+
+ it("should handle nested property paths", () => {
+ component.instrument = undefined;
+ const dataset = { nested: { property: "nested-value" } } as any;
+ const result = component.getNestedValue(dataset, "nested.property");
+ expect(result).toBe("nested-value");
+ });
+
+ it("should return undefined for non-existent paths", () => {
+ component.instrument = undefined;
+ const dataset = { pid: "test-pid" } as any;
+ const result = component.getNestedValue(dataset, "nonexistent.path");
+ expect(result).toBeUndefined();
+ });
+
+ it("should return error message when path is missing", () => {
+ component.instrument = undefined;
+ const dataset = {} as any;
+ const result = component.getNestedValue(dataset, "");
+ expect(result).toBe("field source is missing");
+ });
+
+ it("should return null when dataset is null", () => {
+ component.instrument = undefined;
+ const result = component.getNestedValue(null, "any.path");
+ expect(result).toBeNull();
+ });
+ });
+
+ describe("getInternalLinkValue", () => {
+ it("should return instrument pid when path is 'instrumentName' and instrument exists", () => {
+ component.instrument = {
+ pid: "instrument1",
+ name: "Test Instrument",
+ } as any;
+ const dataset = {} as any;
+ const result = component.getInternalLinkValue(dataset, "instrumentName");
+ expect(result).toBe("instrument1");
+ });
+
+ it("should return empty string when path is 'instrumentName' but instrument has no pid", () => {
+ component.instrument = { name: "Test Instrument" } as any;
+ const dataset = {} as any;
+ const result = component.getInternalLinkValue(dataset, "instrumentName");
+ expect(result).toBe("");
+ });
+
+ it("should return empty string when path is 'instrumentName' but no instrument", () => {
+ component.instrument = undefined;
+ const dataset = {} as any;
+ const result = component.getInternalLinkValue(dataset, "instrumentName");
+ expect(result).toBe("");
+ });
+
+ it("should use getNestedValue for non-instrumentName paths", () => {
+ component.instrument = {
+ pid: "instrument1",
+ name: "Test Instrument",
+ } as any;
+ const dataset = { pid: "test-pid" } as any;
+ const result = component.getInternalLinkValue(dataset, "pid");
+ expect(result).toBe("test-pid");
+ });
+
+ it("should handle nested paths correctly", () => {
+ component.instrument = undefined;
+ const dataset = {
+ nested: { value: "test-value" },
+ } as any;
+ const result = component.getInternalLinkValue(dataset, "nested.value");
+ expect(result).toBe("test-value");
+ });
+
+ it("should return empty string for null/undefined values", () => {
+ component.instrument = undefined;
+ const dataset = {} as any;
+ const result = component.getInternalLinkValue(dataset, "nonexistent");
+ expect(result).toBe("");
+ });
+ });
+
+ describe("onClickInternalLink with instrument support", () => {
+ beforeEach(() => {
+ (component["router"].navigateByUrl as jasmine.Spy).calls.reset();
+ spyOn(component["snackBar"], "open");
+ });
+
+ it("should navigate to instruments page when internalLinkType is 'instruments'", () => {
+ component.onClickInternalLink(
+ InternalLinkType.INSTRUMENTS,
+ "instrument123",
+ );
+ expect(component["router"].navigateByUrl).toHaveBeenCalledWith(
+ "/instruments/instrument123",
+ );
+ });
+
+ it("should navigate to instruments page when internalLinkType is 'instrumentsName'", () => {
+ component.onClickInternalLink(
+ InternalLinkType.INSTRUMENTS_NAME,
+ "instrument123",
+ );
+ expect(component["router"].navigateByUrl).toHaveBeenCalledWith(
+ "/instruments/instrument123",
+ );
+ });
+
+ it("should encode special characters in instrument ID", () => {
+ component.onClickInternalLink(
+ InternalLinkType.INSTRUMENTS,
+ "instrument with spaces",
+ );
+ expect(component["router"].navigateByUrl).toHaveBeenCalledWith(
+ "/instruments/instrument%20with%20spaces",
+ );
+ });
+
+ it("should navigate to datasets page for dataset links", () => {
+ component.onClickInternalLink(InternalLinkType.DATASETS, "dataset123");
+ expect(component["router"].navigateByUrl).toHaveBeenCalledWith(
+ "/datasets/dataset123",
+ );
+ });
+
+ it("should show error message for invalid link types", () => {
+ component.onClickInternalLink("invalid", "test123");
+ expect(component["snackBar"].open).toHaveBeenCalledWith(
+ "The URL is not valid",
+ "Close",
+ {
+ duration: 2000,
+ },
+ );
+ });
+ });
+
describe("getScientificMetadata", () => {
type TestCase = {
desc: string;
diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts
index e317cf1c9..394ec88a5 100644
--- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts
+++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts
@@ -1,7 +1,8 @@
-import { Component, OnInit } from "@angular/core";
+import { Component, OnInit, OnDestroy } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import { Store } from "@ngrx/store";
+import { Subscription } from "rxjs";
import { showMessageAction } from "state-management/actions/user.actions";
import {
@@ -10,6 +11,7 @@ import {
selectCurrentDatasetWithoutFileInfo,
} from "state-management/selectors/datasets.selectors";
import { selectIsLoading } from "state-management/selectors/user.selectors";
+import { selectCurrentInstrument } from "state-management/selectors/instruments.selectors";
import { AppConfigService } from "app-config.service";
@@ -26,6 +28,7 @@ import { AttachmentService } from "shared/services/attachment.service";
import { TranslateService } from "@ngx-translate/core";
import { DatePipe } from "@angular/common";
import { OutputDatasetObsoleteDto } from "@scicatproject/scicat-sdk-ts-angular/model/outputDatasetObsoleteDto";
+import { Instrument } from "@scicatproject/scicat-sdk-ts-angular";
import { Router } from "@angular/router";
import { MatSnackBar } from "@angular/material/snack-bar";
@@ -41,7 +44,9 @@ import { MatSnackBar } from "@angular/material/snack-bar";
styleUrls: ["./dataset-detail-dynamic.component.scss"],
standalone: false,
})
-export class DatasetDetailDynamicComponent implements OnInit {
+export class DatasetDetailDynamicComponent implements OnInit, OnDestroy {
+ private subscriptions: Subscription[] = [];
+
datasetView: CustomizationItem[];
form: FormGroup;
cols = 10;
@@ -55,6 +60,8 @@ export class DatasetDetailDynamicComponent implements OnInit {
loading$ = this.store.select(selectIsLoading);
show = false;
+ instrument: Instrument | undefined;
+
constructor(
public appConfigService: AppConfigService,
public dialog: MatDialog,
@@ -82,6 +89,12 @@ export class DatasetDetailDynamicComponent implements OnInit {
});
this.datasetView = sortedDatasetView;
+
+ this.subscriptions.push(
+ this.store.select(selectCurrentInstrument).subscribe((instrument) => {
+ this.instrument = instrument;
+ }),
+ );
}
onCopy(value: string) {
@@ -175,9 +188,23 @@ export class DatasetDetailDynamicComponent implements OnInit {
return null;
}
+ if (path === "instrumentName" && this.instrument) {
+ return this.instrument.name || "-";
+ }
+
return path
.split(".")
- .reduce((prev, curr) => (prev ? prev[curr] : undefined), obj);
+ .reduce((prev, curr) => (prev != null ? prev[curr] : undefined), obj);
+ }
+
+ getInternalLinkValue(obj: OutputDatasetObsoleteDto, path: string): string {
+ // For instrumentName internal links, return the instrument ID instead of the name
+ if (path === "instrumentName" && this.instrument) {
+ return this.instrument.pid || "";
+ }
+
+ const value = this.getNestedValue(obj, path);
+ return Array.isArray(value) ? value[0] || "" : (value as string) || "";
}
onClickInternalLink(internalLinkType: string, id: string): void {
@@ -194,6 +221,7 @@ export class DatasetDetailDynamicComponent implements OnInit {
this.router.navigateByUrl("/proposals/" + encodedId);
break;
case InternalLinkType.INSTRUMENTS:
+ case InternalLinkType.INSTRUMENTS_NAME:
this.router.navigateByUrl("/instruments/" + encodedId);
break;
default:
@@ -221,4 +249,10 @@ export class DatasetDetailDynamicComponent implements OnInit {
// Ensure the result is a valid object for metadata display
return result && typeof result === "object" ? result : null;
}
+
+ ngOnDestroy() {
+ this.subscriptions.forEach((subscription) => {
+ subscription.unsubscribe();
+ });
+ }
}
diff --git a/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts
index 35af74dc5..c2273f935 100644
--- a/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts
+++ b/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NO_ERRORS_SCHEMA } from "@angular/core";
-import { AppConfig, AppConfigService } from "app-config.service";
+import { AppConfigInterface, AppConfigService } from "app-config.service";
import { DatasetDetailWrapperComponent } from "./dataset-detail-wrapper.component";
import { DatasetDetailComponent } from "./dataset-detail/dataset-detail.component";
import { DatasetDetailDynamicComponent } from "./dataset-detail-dynamic/dataset-detail-dynamic.component";
@@ -69,7 +69,7 @@ describe("DatasetDetailWrapperComponent", () => {
datasetDetailComponent: {
enableCustomizedComponent: true,
},
- } as AppConfig);
+ } as AppConfigInterface);
fixture.detectChanges();
@@ -82,7 +82,7 @@ describe("DatasetDetailWrapperComponent", () => {
datasetDetailComponent: {
enableCustomizedComponent: false,
},
- } as AppConfig);
+ } as AppConfigInterface);
fixture.detectChanges();
diff --git a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html
index 3151afb81..3d2a9802e 100644
--- a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html
+++ b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html
@@ -1,61 +1,36 @@
-
+
My Data
All Public Data
-
-
+
+
{{ mode | titlecase }}
-
-
-
-
+
\ No newline at end of file
diff --git a/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss b/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss
deleted file mode 100644
index 2507c89d8..000000000
--- a/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@use "@angular/material" as mat;
-@use "sass:map";
-
-@mixin color($theme) {
- $color-config: map.get($theme, "color");
- $hover: map.get($color-config, "hover");
-
- .close-button {
- color: mat.m2-get-color-from-palette($hover, "default");
- }
-}
-
-@mixin theme($theme) {
- $color-config: mat.m2-get-color-config($theme);
- @if $color-config != null {
- @include color($theme);
- }
-}
diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html
deleted file mode 100644
index 40aa3eb44..000000000
--- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html
+++ /dev/null
@@ -1,32 +0,0 @@
-Columns
-
-
- clear
-
-
-
-
-
-
-
-
-
-
- {{ column.name }}
-
- |
-
-
diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss
deleted file mode 100644
index 833352ce7..000000000
--- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.title {
- float: left;
- transform: translateY(25%);
-}
-
-.close-button {
- float: right;
-}
diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts
deleted file mode 100644
index a2bab6472..000000000
--- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
-
-import {
- DatasetTableSettingsComponent,
- SelectColumnEvent,
-} from "./dataset-table-settings.component";
-import { NO_ERRORS_SCHEMA } from "@angular/core";
-
-import { SearchBarModule } from "shared/modules/search-bar/search-bar.module";
-import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
-import { TableColumn } from "state-management/models";
-import { MatButtonModule } from "@angular/material/button";
-import {
- MatCheckboxModule,
- MatCheckboxChange,
-} from "@angular/material/checkbox";
-import { MatIconModule } from "@angular/material/icon";
-
-describe("DatasetTableSettingsComponent", () => {
- let component: DatasetTableSettingsComponent;
- let fixture: ComponentFixture;
-
- let emitSpy;
-
- beforeEach(waitForAsync(() => {
- TestBed.configureTestingModule({
- schemas: [NO_ERRORS_SCHEMA],
- declarations: [DatasetTableSettingsComponent],
- imports: [
- BrowserAnimationsModule,
- MatButtonModule,
- MatCheckboxModule,
- MatIconModule,
- SearchBarModule,
- ],
- }).compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DatasetTableSettingsComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it("should create", () => {
- expect(component).toBeTruthy();
- });
-
- describe("#doCloseClick()", () => {
- it("should emit a MouseEvent", () => {
- emitSpy = spyOn(component.closeClick, "emit");
-
- const event = {} as MouseEvent;
-
- component.doCloseClick(event);
-
- expect(emitSpy).toHaveBeenCalledTimes(1);
- expect(emitSpy).toHaveBeenCalledWith(event);
- });
- });
-
- describe("#doSelectColumn()", () => {
- it("should emit a SelectColumnEvent", () => {
- emitSpy = spyOn(component.selectColumn, "emit");
-
- const event = {} as MatCheckboxChange;
- const column: TableColumn = {
- name: "test",
- order: 0,
- type: "standard",
- enabled: true,
- };
-
- const emittedEvent: SelectColumnEvent = {
- checkBoxChange: event,
- column,
- };
-
- component.doSelectColumn(event, column);
-
- expect(emitSpy).toHaveBeenCalledTimes(1);
- expect(emitSpy).toHaveBeenCalledWith(emittedEvent);
- });
- });
-
- describe("#doSearch()", () => {
- it("should set filteredColumns based on the input value", () => {
- component.selectableColumns = [
- { name: "test", order: 0, type: "standard", enabled: true },
- { name: "filter", order: 1, type: "custom", enabled: true },
- ];
-
- component.doSearch("test");
-
- expect(component.filteredColumns.length).toEqual(1);
- });
- });
-});
diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts
deleted file mode 100644
index 75e460f2c..000000000
--- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Component, Input, Output, EventEmitter } from "@angular/core";
-import { TableColumn } from "state-management/models";
-import { MatCheckboxChange } from "@angular/material/checkbox";
-
-export interface SelectColumnEvent {
- checkBoxChange: MatCheckboxChange;
- column: TableColumn;
-}
-
-@Component({
- selector: "dataset-table-settings",
- templateUrl: "./dataset-table-settings.component.html",
- styleUrls: ["./dataset-table-settings.component.scss"],
- standalone: false,
-})
-export class DatasetTableSettingsComponent {
- @Input() clearSearchBar = false;
- @Input() selectableColumns: TableColumn[] | null = null;
- filteredColumns: TableColumn[] = [];
-
- @Output() closeClick = new EventEmitter();
- @Output() selectColumn = new EventEmitter();
-
- doCloseClick(event: MouseEvent): void {
- this.closeClick.emit(event);
- }
-
- doSelectColumn(event: MatCheckboxChange, column: TableColumn): void {
- const selectColumnEvent: SelectColumnEvent = {
- checkBoxChange: event,
- column,
- };
- this.selectColumn.emit(selectColumnEvent);
- }
-
- doSearch(value: string): void {
- const filterValue = value.toLowerCase();
- this.filteredColumns = this.selectableColumns!.filter(({ name }) =>
- name.toLowerCase().includes(filterValue),
- );
- }
-}
diff --git a/src/app/datasets/dataset-table/dataset-table.component.html b/src/app/datasets/dataset-table/dataset-table.component.html
index fe8299015..9a7ce2e99 100644
--- a/src/app/datasets/dataset-table/dataset-table.component.html
+++ b/src/app/datasets/dataset-table/dataset-table.component.html
@@ -1,406 +1,10 @@
-
-
-
-
-
-
-
-
-
- settings
-
-
-
-
-
-
-
-
-
- 0 && isAllSelected()
- "
- [indeterminate]="
- selectedSets && selectedSets.length > 0 && !isAllSelected()
- "
- (change)="onSelectAll($event)"
- >
-
-
-
-
-
-
-
- shopping_cart
-
-
-
-
-
-
-
-
-
- perm_identity
-
-
Pid
-
-
-
- {{ dataset.pid }}
-
-
-
-
-
-
-
-
- fingerprint
-
-
Name
-
-
-
- {{ dataset.datasetName }}
-
-
-
-
-
-
-
-
- explore
-
-
Source Folder
-
-
-
- ...{{ dataset.sourceFolder | slice: -10 }}
-
-
-
-
-
-
-
-
- directions_run
-
-
Run No.
-
-
-
-
- {{ dataset.scientificMetadata.runNumber.value }}
-
-
-
-
-
-
-
-
-
-
-
-
{{ dataset.size | filesize }}
-
= 4000000000
- ? 35 + (5 * dataset.size) / 40000000000
- : 5 + (15 * dataset.size) / 4000000000,
- background: dataset.size >= 4000000000 ? 'red' : 'green',
- }"
- >
-
-
-
-
-
-
-
-
-
-
-
- calendar_today
-
-
Start Time
-
-
-
-
-
{{ dataset.creationTime | date }}
-
-
-
-
-
-
-
-
-
- bubble_chart
-
-
Type
-
-
- {{ dataset.type }}
-
-
-
-
-
-
-
- camera_alt
-
-
Image
-
-
-
-
-
-
-
-
-
-
-
-
- assessment
-
-
Science Metadata
-
-
-
-
- {{
- dataset.scientificMetadata | jsonHead
- }}
- {{
- dataset.scientificMetadata | json | slice: 0 : 100
- }}...
-
-
-
-
-
-
-
-
- spa
-
-
Proposal ID
-
-
- {{
- dataset.proposalId | StripProposalPrefix
- }}
-
-
-
-
-
-
-
- {{ dataset.ownerGroup }}
-
-
-
-
-
-
-
- archive/ cloud_upload
-
-
Data Status
-
-
-
-
-
-
- hourglass_empty work in
- progress
-
-
-
-
- archive
- archivable
-
-
-
-
- cloud_upload
- retrievable
-
-
-
-
- error
- system error
-
-
-
-
- build
- user error
-
-
-
-
-
-
-
-
-
-
-
-
-
- star
-
-
{{ column.name | replaceUnderscore | titlecase }}
-
-
-
-
-
- {{ value.value || value.v || value }} {{
- value.unit || value.u | prettyUnit
- }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/app/datasets/dataset-table/dataset-table.component.scss b/src/app/datasets/dataset-table/dataset-table.component.scss
index 3b81e2ad9..fd0a35218 100644
--- a/src/app/datasets/dataset-table/dataset-table.component.scss
+++ b/src/app/datasets/dataset-table/dataset-table.component.scss
@@ -1,91 +1,7 @@
.dataset-table {
mat-table {
- overflow-x: scroll;
-
- mat-header-row {
- min-width: 1200px;
- }
-
- mat-header-cell {
- &.mat-column-standard_select {
- justify-content: center;
- }
-
- mat-icon {
- display: flex;
- padding-top: 0.25rem;
- }
- }
-
- mat-row {
- min-width: 1200px;
- }
-
- mat-row:hover {
- background-color: rgba(0, 0, 0, 0.1);
- cursor: pointer;
- }
- mat-cell {
- &.mat-column-standard_select {
- justify-content: center;
- }
-
- mat-icon {
- display: inline-table;
- }
- }
-
- .mat-column-standard_select {
- flex: 0 0 40px;
- }
-
- .mat-column-standard_datasetName {
- flex: 1 0 200px;
- justify-content: left;
- }
-
- .mat-column-standard_runNumber {
- flex: 0.5 0 60px;
- }
-
- .mat-column-standard_sourceFolder {
- flex: 0 1 100px;
- }
-
- .mat-column-standard_size {
- flex: 0.5 0 70px;
- }
-
- .mat-column-standard_creationTime {
- flex: 0 1 90px;
- }
-
- .mat-column-standard_type {
- flex: 0.5 0 70px;
- }
-
- .mat-column-standard_image {
- flex: 0 0 60px;
- }
-
- .mat-column-standard_metadata {
- flex: 1 1 200px;
- }
-
- .mat-column-standard_proposalId {
- flex: 0 1 90px;
- }
-
- .mat-column-standard_ownerGroup {
- flex: 0 1 80px;
- }
-
- .mat-column-standard_dataStatus {
- flex: 1 1 120px;
- }
-
- .mat-column-standard_derivedDatasetsNum {
- flex: 0 1 80px;
+ mat-icon {
+ display: inline-table;
}
}
}
diff --git a/src/app/datasets/dataset-table/dataset-table.component.spec.ts b/src/app/datasets/dataset-table/dataset-table.component.spec.ts
index 894665cc2..42d484279 100644
--- a/src/app/datasets/dataset-table/dataset-table.component.spec.ts
+++ b/src/app/datasets/dataset-table/dataset-table.component.spec.ts
@@ -9,6 +9,7 @@ import {
MockDatasetApi,
mockDataset,
createMock,
+ MockActivatedRoute,
} from "shared/MockStubs";
import { NO_ERRORS_SCHEMA } from "@angular/core";
import {
@@ -28,6 +29,7 @@ import {
} from "state-management/actions/datasets.actions";
import { provideMockStore } from "@ngrx/store/testing";
import { selectDatasets } from "state-management/selectors/datasets.selectors";
+import { selectInstruments } from "state-management/selectors/instruments.selectors";
import { MatTableModule } from "@angular/material/table";
import {
MatCheckboxChange,
@@ -42,6 +44,11 @@ import {
DatasetClass,
DatasetsService,
} from "@scicatproject/scicat-sdk-ts-angular";
+import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model";
+import { ActivatedRoute } from "@angular/router";
+import { JsonHeadPipe } from "shared/pipes/json-head.pipe";
+import { DatePipe } from "@angular/common";
+import { FileSizePipe } from "shared/pipes/filesize.pipe";
const getConfig = () => ({});
@@ -67,8 +74,14 @@ describe("DatasetTableComponent", () => {
],
providers: [
provideMockStore({
- selectors: [{ selector: selectDatasets, value: [] }],
+ selectors: [
+ { selector: selectDatasets, value: [] },
+ { selector: selectInstruments, value: [] },
+ ],
}),
+ JsonHeadPipe,
+ DatePipe,
+ FileSizePipe,
],
declarations: [DatasetTableComponent],
});
@@ -80,6 +93,7 @@ describe("DatasetTableComponent", () => {
useValue: { getConfig },
},
{ provide: DatasetsService, useClass: MockDatasetApi },
+ { provide: ActivatedRoute, useClass: MockActivatedRoute },
],
},
});
@@ -89,7 +103,6 @@ describe("DatasetTableComponent", () => {
beforeEach(() => {
fixture = TestBed.createComponent(DatasetTableComponent);
component = fixture.componentInstance;
- component.tableColumns = [];
fixture.detectChanges();
});
@@ -105,24 +118,15 @@ describe("DatasetTableComponent", () => {
expect(component).toBeTruthy();
});
- describe("#doSettingsClick()", () => {
- it("should emit a MouseEvent on click", () => {
- const emitSpy = spyOn(component.settingsClick, "emit");
-
- const event = {} as MouseEvent;
- component.doSettingsClick(event);
-
- expect(emitSpy).toHaveBeenCalledTimes(1);
- expect(emitSpy).toHaveBeenCalledWith(event);
- });
- });
-
describe("#doRowClick()", () => {
it("should emit the dataset clicked", () => {
const emitSpy = spyOn(component.rowClick, "emit");
const dataset = mockDataset;
- component.doRowClick(dataset);
+ component.onRowEvent({
+ event: RowEventType.RowClick,
+ sender: { row: dataset },
+ });
expect(emitSpy).toHaveBeenCalledTimes(1);
expect(emitSpy).toHaveBeenCalledWith(dataset);
@@ -317,40 +321,6 @@ describe("DatasetTableComponent", () => {
});
});
- describe("#isSelected()", () => {
- it("should return false if dataset is not selected", () => {
- const dataset = createMock({});
- const selected = component.isSelected(dataset);
-
- expect(selected).toEqual(false);
- });
- });
-
- describe("#isAllSelected()", () => {
- it("should return false if length of datasets and length of selectedSets are not equal", () => {
- component.datasets = [mockDataset];
-
- const allSelected = component.isAllSelected();
-
- expect(allSelected).toEqual(false);
- });
-
- it("should return true if length of datasets and length of selectedSets are equal", () => {
- const allSelected = component.isAllSelected();
-
- expect(allSelected).toEqual(true);
- });
- });
-
- describe("#isInBatch()", () => {
- it("should return false if dataset is not in batch", () => {
- const dataset = createMock({});
- const inBatch = component.isInBatch(dataset);
-
- expect(inBatch).toEqual(false);
- });
- });
-
describe("#onSelect()", () => {
it("should dispatch a selectDatasetAction if checked is true", () => {
dispatchSpy = spyOn(store, "dispatch");
@@ -423,11 +393,283 @@ describe("DatasetTableComponent", () => {
});
});
- describe("#countDerivedDatasets()", () => {
- xit("should return the number of derived datasets for a dataset", () => {
- // const dataset = mockDataset;
- // const numberOfDerivedDataset = component.countDerivedDatasets(dataset);
- // expect(numberOfDerivedDataset).toEqual(0);
+ describe("#convertSavedColumns() with instrumentName", () => {
+ beforeEach(() => {
+ component.instruments = [
+ {
+ pid: "instrument1",
+ uniqueName: "unique1",
+ name: "Test Instrument 1",
+ },
+ {
+ pid: "instrument2",
+ uniqueName: "unique2",
+ name: "Test Instrument 2",
+ },
+ { pid: "instrument3", uniqueName: "unique3", name: "" },
+ ] as any[];
+
+ component.instrumentMap = new Map(
+ component.instruments.map((instrument) => [instrument.pid, instrument]),
+ );
+ });
+
+ it("should render instrument name when instrument is found", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = { instrumentId: "instrument1" };
+ const result = instrumentColumn.customRender(instrumentColumn, mockRow);
+
+ expect(result).toBe("Test Instrument 1");
+ });
+
+ it("should render instrumentId when instrument is not found", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = { instrumentId: "nonexistent" };
+ const result = instrumentColumn.customRender(instrumentColumn, mockRow);
+
+ expect(result).toBe("nonexistent");
+ });
+
+ it("should render '-' when instrumentId is not present", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = {};
+ const result = instrumentColumn.customRender(instrumentColumn, mockRow);
+
+ expect(result).toBe("-");
+ });
+
+ it("should render instrumentId when instrument has empty name", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = { instrumentId: "instrument3" };
+ const result = instrumentColumn.customRender(instrumentColumn, mockRow);
+
+ expect(result).toBe("instrument3");
+ });
+
+ it("should export instrument name when instrument is found", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = { instrumentId: "instrument2" };
+ const result = instrumentColumn.toExport(mockRow, instrumentColumn);
+
+ expect(result).toBe("Test Instrument 2");
+ });
+
+ it("should export instrumentId when instrument is not found", () => {
+ const columns = [
+ {
+ name: "instrumentName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+ const instrumentColumn = convertedColumns[0];
+
+ const mockRow = { instrumentId: "unknown-instrument" };
+ const result = instrumentColumn.toExport(mockRow, instrumentColumn);
+
+ expect(result).toBe("unknown-instrument");
+ });
+
+ it("should not affect other column types", () => {
+ const columns = [
+ {
+ name: "datasetName",
+ order: 0,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ {
+ name: "instrumentName",
+ order: 1,
+ enabled: true,
+ width: 200,
+ type: "standard" as const,
+ },
+ ];
+
+ const convertedColumns = component.convertSavedColumns(columns);
+
+ expect(convertedColumns.length).toBe(2);
+ expect(convertedColumns[0].name).toBe("datasetName");
+ expect(convertedColumns[0].customRender).toBeUndefined();
+ expect(convertedColumns[1].name).toBe("instrumentName");
+ expect(convertedColumns[1].customRender).toBeDefined();
+ });
+ });
+
+ describe("instruments subscription with Map optimization", () => {
+ it("should update both instruments array and instrumentMap when instruments observable changes", () => {
+ const mockInstruments = [
+ { pid: "inst1", uniqueName: "unique1", name: "Instrument 1" },
+ { pid: "inst2", uniqueName: "unique2", name: "Instrument 2" },
+ ];
+
+ component.instruments = mockInstruments;
+ component.instrumentMap = new Map(
+ mockInstruments.map((instrument) => [instrument.pid, instrument]),
+ );
+
+ expect(component.instruments).toEqual(mockInstruments);
+ expect(component.instrumentMap.size).toBe(2);
+ expect(component.instrumentMap.get("inst1")).toEqual(mockInstruments[0]);
+ expect(component.instrumentMap.get("inst2")).toEqual(mockInstruments[1]);
+ });
+
+ it("should handle empty instruments array and clear instrumentMap", () => {
+ component.instruments = [];
+ component.instrumentMap = new Map();
+
+ expect(component.instruments).toEqual([]);
+ expect(component.instrumentMap.size).toBe(0);
+ });
+
+ it("should provide O(1) lookup performance for instrument retrieval", () => {
+ const mockInstruments = [
+ { pid: "fast-lookup", uniqueName: "unique1", name: "Fast Instrument" },
+ ];
+
+ component.instrumentMap = new Map(
+ mockInstruments.map((instrument) => [instrument.pid, instrument]),
+ );
+
+ const foundInstrument = component.instrumentMap.get("fast-lookup");
+ expect(foundInstrument).toEqual(mockInstruments[0]);
+
+ const notFoundInstrument = component.instrumentMap.get("nonexistent");
+ expect(notFoundInstrument).toBeUndefined();
+ });
+ });
+
+ describe("#getInstrumentName() private method", () => {
+ beforeEach(() => {
+ const mockInstruments = [
+ { pid: "inst1", uniqueName: "unique1", name: "Test Instrument 1" },
+ { pid: "inst2", uniqueName: "unique2", name: "Test Instrument 2" },
+ { pid: "inst3", uniqueName: "unique3", name: "" },
+ ];
+
+ component.instrumentMap = new Map(
+ mockInstruments.map((instrument) => [instrument.pid, instrument]),
+ );
+ });
+
+ it("should return instrument name when instrument is found", () => {
+ const mockRow = { instrumentId: "inst1" } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("Test Instrument 1");
+ });
+
+ it("should return instrumentId when instrument is not found", () => {
+ const mockRow = { instrumentId: "nonexistent" } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("nonexistent");
+ });
+
+ it("should return '-' when instrumentId is not present", () => {
+ const mockRow = {} as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("-");
+ });
+
+ it("should return instrumentId when instrument has empty name", () => {
+ const mockRow = { instrumentId: "inst3" } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("inst3");
+ });
+
+ it("should handle undefined instrumentId gracefully", () => {
+ const mockRow = { instrumentId: undefined } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("-");
+ });
+
+ it("should handle null instrumentId gracefully", () => {
+ const mockRow = { instrumentId: null } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("-");
+ });
+
+ it("should handle empty string instrumentId gracefully", () => {
+ const mockRow = { instrumentId: "" } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("-");
+ });
+
+ it("should return instrument name even when instrumentId is empty but instrument exists", () => {
+ // Add an instrument with empty string pid to test edge case
+ component.instrumentMap.set("", {
+ pid: "",
+ name: "Empty PID Instrument",
+ } as any);
+
+ const mockRow = { instrumentId: "" } as any;
+ const result = component["getInstrumentName"](mockRow);
+ expect(result).toBe("Empty PID Instrument");
});
});
});
diff --git a/src/app/datasets/dataset-table/dataset-table.component.ts b/src/app/datasets/dataset-table/dataset-table.component.ts
index 5ea06844f..35d1d5431 100644
--- a/src/app/datasets/dataset-table/dataset-table.component.ts
+++ b/src/app/datasets/dataset-table/dataset-table.component.ts
@@ -5,13 +5,11 @@ import {
Output,
EventEmitter,
Input,
- OnChanges,
- SimpleChange,
ViewEncapsulation,
} from "@angular/core";
import { TableColumn } from "state-management/models";
import { MatCheckboxChange } from "@angular/material/checkbox";
-import { Subscription } from "rxjs";
+import { BehaviorSubject, Subscription, lastValueFrom, take } from "rxjs";
import { Store } from "@ngrx/store";
import {
clearSelectionAction,
@@ -20,6 +18,7 @@ import {
selectAllDatasetsAction,
sortByColumnAction,
} from "state-management/actions/datasets.actions";
+import { fetchInstrumentsAction } from "state-management/actions/instruments.actions";
import {
selectDatasets,
@@ -28,14 +27,43 @@ import {
selectTotalSets,
selectDatasetsInBatch,
} from "state-management/selectors/datasets.selectors";
-import { get } from "lodash-es";
+import { get as lodashGet } from "lodash-es";
import { AppConfigService } from "app-config.service";
-import { selectCurrentUser } from "state-management/selectors/user.selectors";
+import {
+ selectColumnsWithHasFetchedSettings,
+ selectCurrentUser,
+} from "state-management/selectors/user.selectors";
import {
DatasetClass,
OutputDatasetObsoleteDto,
+ Instrument,
} from "@scicatproject/scicat-sdk-ts-angular";
-import { PageEvent } from "@angular/material/paginator";
+import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model";
+import {
+ ITableSetting,
+ TableSettingEventType,
+} 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,
+ ITableEvent,
+ RowEventType,
+ TableEventType,
+ TableSelectionMode,
+} from "shared/modules/dynamic-material-table/models/table-row.model";
+import { updateUserSettingsAction } from "state-management/actions/user.actions";
+import { Sort } from "@angular/material/sort";
+import { ActivatedRoute } from "@angular/router";
+import { JsonHeadPipe } from "shared/pipes/json-head.pipe";
+import { DatePipe } from "@angular/common";
+import { FileSizePipe } from "shared/pipes/filesize.pipe";
+import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings";
+import { TableConfigService } from "shared/services/table-config.service";
+import { selectInstruments } from "state-management/selectors/instruments.selectors";
+
export interface SortChangeEvent {
active: string;
direction: "asc" | "desc" | "";
@@ -48,19 +76,22 @@ export interface SortChangeEvent {
encapsulation: ViewEncapsulation.None,
standalone: false,
})
-export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges {
- private inBatchPids: string[] = [];
+export class DatasetTableComponent implements OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
+ selectionIds: string[] = [];
appConfig = this.appConfigService.getConfig();
-
- lodashGet = get;
currentPage$ = this.store.select(selectPage);
datasetsPerPage$ = this.store.select(selectDatasetsPerPage);
datasetCount$ = this.store.select(selectTotalSets);
+ currentUser$ = this.store.select(selectCurrentUser);
+ datasets$ = this.store.select(selectDatasets);
+ selectedDatasets$ = this.store.select(selectDatasetsInBatch);
+ selectColumnsWithFetchedSettings$ = this.store.select(
+ selectColumnsWithHasFetchedSettings,
+ );
+ instruments$ = this.store.select(selectInstruments);
- @Input() tableColumns: TableColumn[] | null = null;
- displayedColumns: string[] = [];
@Input() selectedSets: OutputDatasetObsoleteDto[] | null = null;
@Output() pageChange = new EventEmitter<{
pageIndex: number;
@@ -68,27 +99,190 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges {
}>();
datasets: OutputDatasetObsoleteDto[] = [];
+ instruments: Instrument[] = [];
+ instrumentMap: Map = new Map();
- @Output() settingsClick = new EventEmitter();
@Output() rowClick = new EventEmitter();
+ tableDefaultSettingsConfig: ITableSetting = {
+ visibleActionMenu: actionMenu,
+ settingList: [
+ {
+ visibleActionMenu: actionMenu,
+ isDefaultSetting: true,
+ isCurrentSetting: true,
+ columnSetting: [],
+ },
+ ],
+ rowStyle: {
+ "border-bottom": "1px solid #d2d2d2",
+ },
+ };
+
+ tableName = "datasetsTable";
+
+ columns: TableField[];
+
+ pending = true;
+
+ setting: ITableSetting = {};
+
+ paginationMode: TablePaginationMode = "server-side";
+
+ dataSource: BehaviorSubject = new BehaviorSubject<
+ OutputDatasetObsoleteDto[]
+ >([]);
+
+ pagination: TablePagination = {};
+
+ rowSelectionMode: TableSelectionMode = "multi";
+
+ showGlobalTextSearch = false;
+
+ defaultPageSize = 10;
+
+ defaultPageSizeOptions = [5, 10, 25, 100];
+
+ tablesSettings: object;
+
constructor(
public appConfigService: AppConfigService,
private store: Store,
+ private route: ActivatedRoute,
+ private jsonHeadPipe: JsonHeadPipe,
+ private datePipe: DatePipe,
+ private fileSize: FileSizePipe,
+ private tableConfigService: TableConfigService,
) {}
- onPageChange(event: PageEvent) {
- this.pageChange.emit({
- pageIndex: event.pageIndex,
- pageSize: event.pageSize,
+ private getInstrumentName(row: OutputDatasetObsoleteDto): string {
+ const instrument = this.instrumentMap.get(row.instrumentId);
+ if (instrument?.name) {
+ return instrument.name;
+ }
+ if (row.instrumentId != null) {
+ return row.instrumentId === "" ? "-" : row.instrumentId;
+ }
+ return "-";
+ }
+
+ getTableSort(): ITableSetting["tableSort"] {
+ const { queryParams } = this.route.snapshot;
+
+ if (queryParams.sortDirection && queryParams.sortColumn) {
+ return {
+ sortColumn: queryParams.sortColumn,
+ sortDirection: queryParams.sortDirection,
+ };
+ }
+
+ return null;
+ }
+
+ getTablePaginationConfig(dataCount = 0): TablePagination {
+ const { queryParams } = this.route.snapshot;
+
+ const { skip = 0, limit = 25 } = JSON.parse(queryParams.args ?? "{}");
+
+ return {
+ pageSizeOptions: this.defaultPageSizeOptions,
+ pageIndex: skip / limit || 0,
+ pageSize: limit || this.defaultPageSize,
+ length: dataCount,
+ };
+ }
+
+ initTable(
+ settingConfig: ITableSetting,
+ paginationConfig: TablePagination,
+ ): void {
+ let currentColumnSetting = settingConfig.settingList.find(
+ (s) => s.isCurrentSetting,
+ )?.columnSetting;
+
+ if (!currentColumnSetting && settingConfig.settingList.length > 0) {
+ currentColumnSetting = settingConfig.settingList[0].columnSetting;
+ }
+
+ this.columns = currentColumnSetting;
+ this.setting = settingConfig;
+ this.pagination = paginationConfig;
+ }
+
+ saveTableSettings(setting: ITableSetting) {
+ this.pending = true;
+ const columnsSetting = setting.columnSetting.map((column, index) => {
+ const { name, display, width, type } = column;
+
+ return {
+ name,
+ enabled: !!(display === "visible"),
+ order: index,
+ width,
+ type,
+ };
});
+ this.store.dispatch(
+ updateUserSettingsAction({
+ property: {
+ columns: columnsSetting,
+ },
+ }),
+ );
+
+ this.pending = false;
+ }
+
+ onSettingChange(event: {
+ type: TableSettingEventType;
+ setting: ITableSetting;
+ }) {
+ if (
+ event.type === TableSettingEventType.save ||
+ event.type === TableSettingEventType.create
+ ) {
+ this.saveTableSettings(event.setting);
+ }
+ }
+
+ onRowEvent({ event, sender }: IRowEvent) {
+ if (event === RowEventType.RowClick) {
+ const dataset = sender.row;
+ this.rowClick.emit(dataset);
+ } else if (event === RowEventType.RowSelectionChange) {
+ const dataset = sender.row;
+ if (sender.checked) {
+ this.store.dispatch(selectDatasetAction({ dataset }));
+ } else {
+ this.store.dispatch(deselectDatasetAction({ dataset }));
+ }
+ } else if (event === RowEventType.MasterSelectionChange) {
+ if (sender.checked) {
+ this.store.dispatch(selectAllDatasetsAction());
+ } else {
+ this.store.dispatch(clearSelectionAction());
+ }
+ }
}
- doSettingsClick(event: MouseEvent) {
- this.settingsClick.emit(event);
+
+ onTableEvent({ event, sender }: ITableEvent) {
+ if (event === TableEventType.SortChanged) {
+ const { active, direction } = sender as Sort;
+
+ let column = active;
+ if (column === "runNumber") {
+ column = "scientificMetadata.runNumber.value";
+ }
+
+ this.store.dispatch(sortByColumnAction({ column, direction }));
+ }
}
- doRowClick(dataset: OutputDatasetObsoleteDto): void {
- this.rowClick.emit(dataset);
+ onPageChange({ pageIndex, pageSize }: TablePagination) {
+ this.pageChange.emit({
+ pageIndex,
+ pageSize,
+ });
}
// conditional to asses dataset status and assign correct icon ArchViewMode.work_in_progress
@@ -149,22 +343,9 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges {
return false;
}
- isSelected(dataset: DatasetClass): boolean {
- if (!this.selectedSets) {
- return false;
- }
- return this.selectedSets.map((set) => set.pid).indexOf(dataset.pid) !== -1;
- }
-
- isAllSelected(): boolean {
- const numSelected = this.selectedSets ? this.selectedSets.length : 0;
- const numRows = this.datasets ? this.datasets.length : 0;
- return numSelected === numRows;
- }
-
- isInBatch(dataset: DatasetClass): boolean {
- return this.inBatchPids.indexOf(dataset.pid) !== -1;
- }
+ // isInBatch(dataset: DatasetClass): boolean {
+ // return this.inBatchPids.indexOf(dataset.pid) !== -1;
+ // }
onSelect(event: MatCheckboxChange, dataset: OutputDatasetObsoleteDto): void {
if (event.checked) {
@@ -189,71 +370,197 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges {
this.store.dispatch(sortByColumnAction({ column, direction }));
}
- // countDerivedDatasets(dataset: Dataset): number {
- // let derivedDatasetsNum = 0;
- // if (dataset.history) {
- // dataset.history.forEach(item => {
- // if (
- // item.hasOwnProperty("derivedDataset") &&
- // this.datasets.map(set => set.pid).includes(item.derivedDataset.pid)
- // ) {
- // derivedDatasetsNum++;
- // }
- // });
- // }
- // return derivedDatasetsNum;
- // }
+ convertSavedColumns(columns: TableColumn[]): TableField[] {
+ return columns
+ .filter((column) => column.name !== "select")
+ .map((column) => {
+ const convertedColumn: TableField = {
+ name: column.name,
+ header: column.header,
+ index: column.order,
+ display: column.enabled ? "visible" : "hidden",
+ width: column.width,
+ type: column.type as any,
+ };
+
+ if (column.name === "runNumber" && column.type !== "custom") {
+ // NOTE: This is for the saved columns in the database or the old config.
+ convertedColumn.customRender = (c, row) =>
+ lodashGet(row, "scientificMetadata.runNumber.value");
+ convertedColumn.toExport = (row) =>
+ lodashGet(row, "scientificMetadata.runNumber.value");
+ }
+ // NOTE: This is how we render the custom columns if new config is used.
+ if (column.type === "custom") {
+ convertedColumn.customRender = (c, row) =>
+ lodashGet(row, column.path || column.name);
+ convertedColumn.toExport = (row) =>
+ lodashGet(row, column.path || column.name);
+ }
+
+ if (column.name === "size") {
+ convertedColumn.customRender = (column, row) =>
+ this.fileSize.transform(row[column.name]);
+ convertedColumn.toExport = (row) =>
+ this.fileSize.transform(row[column.name]);
+ }
+
+ if (column.name === "creationTime") {
+ convertedColumn.customRender = (column, row) =>
+ this.datePipe.transform(row[column.name]);
+ convertedColumn.toExport = (row) =>
+ this.datePipe.transform(row[column.name]);
+ }
+
+ if (
+ column.name === "metadata" ||
+ column.name === "scientificMetadata"
+ ) {
+ convertedColumn.customRender = (column, row) => {
+ // NOTE: Maybe here we should use the "scientificMetadata" as field name and not "metadata". This should be changed in the backend config.
+ return this.jsonHeadPipe.transform(row["scientificMetadata"]);
+ };
+ convertedColumn.toExport = (row) => {
+ return this.jsonHeadPipe.transform(row["scientificMetadata"]);
+ };
+ }
+
+ if (column.name === "dataStatus") {
+ convertedColumn.renderContentIcon = (column, row) => {
+ if (this.wipCondition(row)) {
+ return "hourglass_empty";
+ } else if (this.archivableCondition(row)) {
+ return "archive";
+ } else if (this.retrievableCondition(row)) {
+ return "archive";
+ } else if (this.systemErrorCondition(row)) {
+ return "error_outline";
+ } else if (this.userErrorCondition(row)) {
+ return "error_outline";
+ }
+
+ return "";
+ };
+
+ convertedColumn.customRender = (column, row) => {
+ if (this.wipCondition(row)) {
+ return "Work in progress";
+ } else if (this.archivableCondition(row)) {
+ return "Archivable";
+ } else if (this.retrievableCondition(row)) {
+ return "Retrievable";
+ } else if (this.systemErrorCondition(row)) {
+ return "System error";
+ } else if (this.userErrorCondition(row)) {
+ return "User error";
+ }
+
+ return "";
+ };
+
+ convertedColumn.toExport = (row) => {
+ if (this.wipCondition(row)) {
+ return "Work in progress";
+ } else if (this.archivableCondition(row)) {
+ return "Archivable";
+ } else if (this.retrievableCondition(row)) {
+ return "Retrievable";
+ } else if (this.systemErrorCondition(row)) {
+ return "System error";
+ } else if (this.userErrorCondition(row)) {
+ return "User error";
+ }
+
+ return "";
+ };
+ }
+
+ if (column.name === "image") {
+ convertedColumn.renderImage = true;
+ }
+
+ if (column.name === "instrumentName") {
+ convertedColumn.customRender = (column, row) =>
+ this.getInstrumentName(row);
+ convertedColumn.toExport = (row, column) =>
+ this.getInstrumentName(row);
+ }
+
+ return convertedColumn;
+ });
+ }
ngOnInit() {
+ this.store.dispatch(fetchInstrumentsAction({ limit: 1000, skip: 0 }));
+
this.subscriptions.push(
- this.store.select(selectDatasetsInBatch).subscribe((datasets) => {
- this.inBatchPids = datasets.map((dataset) => {
+ this.selectedDatasets$.subscribe((datasets) => {
+ // NOTE: In the selectionIds we are storing either _id or pid. Dynamic material table works only with these two.
+ this.selectionIds = datasets.map((dataset) => {
return dataset.pid;
});
}),
);
- if (this.tableColumns) {
- this.displayedColumns = this.tableColumns
- .filter((column) => column.enabled)
- .map((column) => {
- return column.type + "_" + column.name;
- });
- }
+ this.subscriptions.push(
+ this.instruments$.subscribe((instruments) => {
+ this.instruments = instruments;
+ this.instrumentMap = new Map(
+ instruments.map((instrument) => [instrument.pid, instrument]),
+ );
+ }),
+ );
this.subscriptions.push(
- this.store.select(selectDatasets).subscribe((datasets) => {
- this.store.select(selectCurrentUser).subscribe((currentUser) => {
- const publishedDatasets = datasets.filter(
- (dataset) => dataset.isPublished,
- );
- this.datasets = currentUser ? datasets : publishedDatasets;
- });
+ this.datasets$.subscribe((datasets) => {
+ this.currentUser$.subscribe((currentUser) => {
+ this.datasetCount$.subscribe(async (count) => {
+ const defaultTableColumns = await lastValueFrom(
+ this.selectColumnsWithFetchedSettings$.pipe(take(1)),
+ );
- // this.derivationMapPids = this.datasetDerivationsMaps.map(
- // datasetderivationMap => datasetderivationMap.datasetPid
- // );
- // this.datasetDerivationsMaps = datasets
- // .filter(({ pid }) => !this.derivationMapPids.includes(pid))
- // .map(dataset => ({
- // datasetPid: dataset.pid,
- // derivedDatasetsNum: this.countDerivedDatasets(dataset)
- // }));
+ if (
+ defaultTableColumns.hasFetchedSettings &&
+ defaultTableColumns.columns.length
+ ) {
+ const tableColumns = defaultTableColumns.columns;
+
+ if (!currentUser) {
+ this.rowSelectionMode = "none";
+ }
+
+ if (tableColumns) {
+ this.dataSource.next(datasets);
+ this.pending = false;
+
+ const savedTableConfigColumns =
+ this.convertSavedColumns(tableColumns);
+
+ const tableSort = this.getTableSort();
+ const paginationConfig = this.getTablePaginationConfig(count);
+
+ this.tableDefaultSettingsConfig.settingList[0].columnSetting =
+ savedTableConfigColumns;
+
+ const tableSettingsConfig =
+ this.tableConfigService.getTableSettingsConfig(
+ this.tableName,
+ this.tableDefaultSettingsConfig,
+ savedTableConfigColumns,
+ tableSort,
+ );
+
+ if (tableSettingsConfig?.settingList.length) {
+ this.initTable(tableSettingsConfig, paginationConfig);
+ }
+ }
+ }
+ });
+ });
}),
);
}
- ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
- for (const propName in changes) {
- if (propName === "tableColumns") {
- this.tableColumns = changes[propName].currentValue;
- this.displayedColumns = changes[propName].currentValue
- .filter((column: TableColumn) => column.enabled)
- .map((column: TableColumn) => column.type + "_" + column.name);
- }
- }
- }
-
ngOnDestroy() {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.html b/src/app/datasets/datasets-filter/datasets-filter.component.html
index 7886cfd33..38853e286 100644
--- a/src/app/datasets/datasets-filter/datasets-filter.component.html
+++ b/src/app/datasets/datasets-filter/datasets-filter.component.html
@@ -7,58 +7,176 @@
-
+ ">
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
search
Apply
-
+
undo
Reset Filters
-
+
\ No newline at end of file
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.scss b/src/app/datasets/datasets-filter/datasets-filter.component.scss
index 92dbd784e..127630a8c 100644
--- a/src/app/datasets/datasets-filter/datasets-filter.component.scss
+++ b/src/app/datasets/datasets-filter/datasets-filter.component.scss
@@ -69,4 +69,73 @@ mat-card {
.section-container:first-child {
margin-top: unset;
}
+
+ .outgassing-filters {
+ padding: 0.5rem;
+ border-radius: 4px;
+ background-color: rgba(0, 0, 0, 0.02);
+ margin-bottom: 1rem;
+
+ h4 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ color: rgba(0, 0, 0, 0.7);
+ font-size: 1rem;
+ font-weight: 500;
+ }
+
+ // Accordion styling
+ mat-expansion-panel {
+ margin-bottom: 0.5rem;
+ border-radius: 4px;
+ overflow: hidden;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .custom-panel-header {
+ height: auto !important;
+ padding: 8px 16px;
+
+ .panel-header-content {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+
+ ::ng-deep .mat-content {
+ flex-direction: column;
+ align-items: flex-start;
+ overflow: visible;
+ }
+
+ ::ng-deep .mat-expansion-panel-header-title {
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.8);
+ margin-bottom: 4px;
+ white-space: normal;
+ }
+
+ ::ng-deep .mat-expansion-panel-header-description {
+ font-size: 0.85rem;
+ color: rgba(0, 0, 0, 0.6);
+ margin: 0;
+ white-space: normal;
+ margin-top: 2px;
+ }
+ } .outgassing-form {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ // Make the form fields stack vertically
+ .form-field-full-width {
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+ }
+ }
}
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.ts b/src/app/datasets/datasets-filter/datasets-filter.component.ts
index fde7399fa..37cd5e136 100644
--- a/src/app/datasets/datasets-filter/datasets-filter.component.ts
+++ b/src/app/datasets/datasets-filter/datasets-filter.component.ts
@@ -5,18 +5,22 @@ import {
Type,
ViewContainerRef,
} from "@angular/core";
+import { FormControl, FormGroup, Validators } from "@angular/forms";
import { MatDialog } from "@angular/material/dialog";
+import { MatSnackBar } from "@angular/material/snack-bar";
import { Store } from "@ngrx/store";
import { cloneDeep, isEqual } from "lodash-es";
import {
selectHasAppliedFilters,
selectScientificConditions,
} from "state-management/selectors/datasets.selectors";
+import { ScientificCondition } from "state-management/models";
import {
clearFacetsAction,
fetchDatasetsAction,
fetchFacetCountsAction,
+ addScientificConditionAction,
} from "state-management/actions/datasets.actions";
import {
deselectAllCustomColumnsAction,
@@ -74,19 +78,67 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy {
scientificConditions$ = this.store.select(selectScientificConditions);
appConfig = this.appConfigService.getConfig();
-
+ unitsEnabled = this.appConfig.scienceSearchUnitsEnabled;
clearSearchBar = false;
+ // Track the expanded state of each outgassing filter panel
+ expandedPanels: { [key: string]: boolean } = {
+ typeOfCleaning: false,
+ outgassing1h: false,
+ outgassing10h: false,
+ outgassing100h: false,
+ outgassingGreater100h: false,
+ };
+
hasAppliedFilters$ = this.store.select(selectHasAppliedFilters);
labelMaps: { [key: string]: string } = {};
+ // Add form groups for the outgassing filters
+ typeOfCleaningForm = new FormGroup({
+ lhs: new FormControl("Type of Cleaning", [Validators.required]),
+ relation: new FormControl("EQUAL_TO_STRING", [Validators.required]),
+ rhs: new FormControl("", [Validators.required, Validators.minLength(1)]),
+ unit: new FormControl(""),
+ });
+
+ outgassingForm1h = new FormGroup({
+ lhs: new FormControl("Outgassing values after 1h", [Validators.required]),
+ relation: new FormControl("GREATER_THAN", [Validators.required]),
+ rhs: new FormControl("", [Validators.required, Validators.minLength(1)]),
+ unit: new FormControl(""),
+ });
+
+ outgassingForm10h = new FormGroup({
+ lhs: new FormControl("Outgassing values after 10h", [Validators.required]),
+ relation: new FormControl("GREATER_THAN", [Validators.required]),
+ rhs: new FormControl("", [Validators.required, Validators.minLength(1)]),
+ unit: new FormControl(""),
+ });
+
+ outgassingForm100h = new FormGroup({
+ lhs: new FormControl("Outgassing values after 100h", [Validators.required]),
+ relation: new FormControl("GREATER_THAN", [Validators.required]),
+ rhs: new FormControl("", [Validators.required, Validators.minLength(1)]),
+ unit: new FormControl(""),
+ });
+
+ outgassingFormGreater100h = new FormGroup({
+ lhs: new FormControl("Outgassing values after >100h", [
+ Validators.required,
+ ]),
+ relation: new FormControl("GREATER_THAN", [Validators.required]),
+ rhs: new FormControl("", [Validators.required, Validators.minLength(1)]),
+ unit: new FormControl(""),
+ });
+
constructor(
public appConfigService: AppConfigService,
public dialog: MatDialog,
private store: Store,
private asyncPipe: AsyncPipe,
private viewContainerRef: ViewContainerRef,
+ private snackBar: MatSnackBar,
) {}
ngOnInit() {
@@ -175,10 +227,80 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy {
}
applyFilters() {
+ // Apply all outgassing filters that have values
+ this.applyOutgassingFilters();
+
+ // Fetch datasets with applied filters
this.store.dispatch(fetchDatasetsAction());
this.store.dispatch(fetchFacetCountsAction());
}
+ // Apply all outgassing filters at once
+ private applyOutgassingFilters() {
+ const forms = [
+ { type: "typeOfCleaning", form: this.typeOfCleaningForm },
+ { type: "1h", form: this.outgassingForm1h },
+ { type: "10h", form: this.outgassingForm10h },
+ { type: "100h", form: this.outgassingForm100h },
+ { type: "greater100h", form: this.outgassingFormGreater100h },
+ ];
+ // Process each form that has a value
+ forms.forEach(({ type, form }) => {
+ // Only process if the form has a value
+ if (form.get("rhs")?.value) {
+ this.processOutgassingFilter(
+ type as "1h" | "10h" | "100h" | "greater100h",
+ );
+ }
+ });
+ }
+
+ // Process a single outgassing filter
+ private processOutgassingFilter(
+ filterType: "typeOfCleaning" | "1h" | "10h" | "100h" | "greater100h",
+ ): void {
+ let form: FormGroup;
+
+ // Select the appropriate form based on the filter type
+ switch (filterType) {
+ case "typeOfCleaning":
+ form = this.typeOfCleaningForm;
+ break;
+ case "1h":
+ form = this.outgassingForm1h;
+ break;
+ case "10h":
+ form = this.outgassingForm10h;
+ break;
+ case "100h":
+ form = this.outgassingForm100h;
+ break;
+ case "greater100h":
+ form = this.outgassingFormGreater100h;
+ break;
+ default:
+ return;
+ }
+
+ // Check if form is valid
+ if (form.invalid || !form.get("rhs")?.value) {
+ return;
+ }
+
+ const { lhs, relation, unit } = form.value;
+ const rawRhs = form.get("rhs")?.value;
+
+ // Parse the value based on the relation
+ const rhs =
+ relation === "EQUAL_TO_STRING" ? String(rawRhs) : Number(rawRhs);
+
+ // Create the condition
+ const condition = { lhs, relation, rhs, unit };
+ console.log({ condition });
+ // Dispatch the action to add the scientific condition
+ this.store.dispatch(addScientificConditionAction({ condition }));
+ }
+
renderComponent(filterObj: FilterConfig): any {
const key = Object.keys(filterObj)[0];
const isEnabled = filterObj[key];
@@ -189,6 +311,98 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy {
return COMPONENT_MAP[key];
}
+
+ // Method to apply outgassing filters
+ applyOutgassingFilter(
+ filterType: "typeOfCleaning" | "1h" | "10h" | "100h" | "greater100h",
+ ): void {
+ let form: FormGroup;
+
+ // Select the appropriate form based on the filter type
+ switch (filterType) {
+ case "typeOfCleaning":
+ form = this.typeOfCleaningForm;
+ break;
+ case "1h":
+ form = this.outgassingForm1h;
+ break;
+ case "10h":
+ form = this.outgassingForm10h;
+ break;
+ case "100h":
+ form = this.outgassingForm100h;
+ break;
+ case "greater100h":
+ form = this.outgassingFormGreater100h;
+ break;
+ default:
+ return;
+ }
+
+ // Check if form is valid
+ if (form.invalid) {
+ return;
+ }
+
+ const { lhs, relation, unit } = form.value;
+ const rawRhs = form.get("rhs")?.value;
+
+ // Parse the value based on the relation
+ const rhs =
+ relation === "EQUAL_TO_STRING" ? String(rawRhs) : Number(rawRhs);
+
+ // Create the condition
+ const condition = { lhs, relation, rhs, unit };
+
+ // Dispatch the action to add the scientific condition
+ this.store.dispatch(addScientificConditionAction({ condition }));
+
+ // Show success message
+ this.snackBar.open(
+ `Added filter: ${lhs} ${this.formatRelation(relation)} ${rhs} ${unit || ""}`,
+ "Close",
+ {
+ duration: 2000,
+ },
+ );
+
+ // Reset the value field after applying
+ form.get("rhs")?.reset("");
+ }
+
+ // Helper method to format relation for display
+ private formatRelation(relation: string): string {
+ switch (relation) {
+ case "GREATER_THAN":
+ return ">";
+ case "LESS_THAN":
+ return "<";
+ case "EQUAL_TO_NUMERIC":
+ case "EQUAL_TO_STRING":
+ return "=";
+ default:
+ return relation;
+ }
+ }
+
+ // Helper method to get filter display text
+ getFilterDisplayText(formGroup: FormGroup): string {
+ const relation = formGroup.get("relation")?.value;
+ const rhs = formGroup.get("rhs")?.value;
+ const unit = formGroup.get("unit")?.value;
+
+ if (!rhs) {
+ return "No value set";
+ }
+
+ return `${this.formatRelation(relation)} ${rhs} ${unit || ""}`.trim();
+ }
+
+ // Toggle panel expansion
+ togglePanel(panel: string): void {
+ this.expandedPanels[panel] = !this.expandedPanels[panel];
+ }
+
ngOnDestroy() {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts
index 9bbefaf23..3e132a2e4 100644
--- a/src/app/datasets/datasets.module.ts
+++ b/src/app/datasets/datasets.module.ts
@@ -20,6 +20,7 @@ import { MatCardModule } from "@angular/material/card";
import { MatCheckboxModule } from "@angular/material/checkbox";
import { MatOptionModule } from "@angular/material/core";
import { MatDialogModule } from "@angular/material/dialog";
+import { MatExpansionModule } from "@angular/material/expansion";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatGridListModule } from "@angular/material/grid-list";
import { MatIconModule } from "@angular/material/icon";
@@ -49,7 +50,6 @@ import { DatasetDetailComponent } from "./dataset-detail/dataset-detail/dataset-
import { DatasetTableComponent } from "./dataset-table/dataset-table.component";
import { DatasetsFilterComponent } from "./datasets-filter/datasets-filter.component";
import { AddDatasetDialogComponent } from "./add-dataset-dialog/add-dataset-dialog.component";
-import { DatasetTableSettingsComponent } from "./dataset-table-settings/dataset-table-settings.component";
import { DatasetTableActionsComponent } from "./dataset-table-actions/dataset-table-actions.component";
import { DatasetLifecycleComponent } from "./dataset-lifecycle/dataset-lifecycle.component";
import { SampleEditComponent } from "./sample-edit/sample-edit.component";
@@ -85,6 +85,8 @@ import { userReducer } from "state-management/reducers/user.reducer";
import { MatSnackBarModule } from "@angular/material/snack-bar";
import { DatasetDetailDynamicComponent } from "./dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component";
import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-wrapper.component";
+import { JsonHeadPipe } from "shared/pipes/json-head.pipe";
+import { ThumbnailPipe } from "shared/pipes/thumbnail.pipe";
@NgModule({
imports: [
CommonModule,
@@ -100,6 +102,7 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
+ MatExpansionModule,
MatFormFieldModule,
MatGridListModule,
MatIconModule,
@@ -162,7 +165,6 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w
ReduceComponent,
DatasetDetailsDashboardComponent,
AddDatasetDialogComponent,
- DatasetTableSettingsComponent,
DatasetTableActionsComponent,
DatasetLifecycleComponent,
SampleEditComponent,
@@ -178,6 +180,8 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w
providers: [
ArchivingService,
AsyncPipe,
+ JsonHeadPipe,
+ ThumbnailPipe,
ADAuthService,
SharedScicatFrontendModule,
FileSizePipe,
diff --git a/src/app/files/files-dashboard/files-dashboard.component.html b/src/app/files/files-dashboard/files-dashboard.component.html
index b61a861b2..aec823181 100644
--- a/src/app/files/files-dashboard/files-dashboard.component.html
+++ b/src/app/files/files-dashboard/files-dashboard.component.html
@@ -1,8 +1,20 @@
-
-
+
diff --git a/src/app/files/files-dashboard/files-dashboard.component.spec.ts b/src/app/files/files-dashboard/files-dashboard.component.spec.ts
index 72207c6a9..e8b70bc7e 100644
--- a/src/app/files/files-dashboard/files-dashboard.component.spec.ts
+++ b/src/app/files/files-dashboard/files-dashboard.component.spec.ts
@@ -1,28 +1,24 @@
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router";
-import { AppConfigService } from "app-config.service";
-import { MockActivatedRoute, MockRouter } from "shared/MockStubs";
-import { ExportExcelService } from "shared/services/export-excel.service";
-import { ScicatDataService } from "shared/services/scicat-data-service";
+import { MockActivatedRoute, MockRouter, MockStore } from "shared/MockStubs";
import { FilesDashboardComponent } from "./files-dashboard.component";
+import { DatePipe } from "@angular/common";
+import { Store } from "@ngrx/store";
describe("FilesDashboardComponent", () => {
let component: FilesDashboardComponent;
let fixture: ComponentFixture;
- const getConfig = () => ({});
-
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
declarations: [FilesDashboardComponent],
providers: [
{ provide: ActivatedRoute, useClass: MockActivatedRoute },
- { provide: AppConfigService, useValue: { getConfig } },
- { provide: ExportExcelService, useValue: {} },
{ provide: Router, useClass: MockRouter },
- { provide: ScicatDataService, useValue: {} },
+ { provide: Store, useClass: MockStore },
+ DatePipe,
],
}).compileComponents();
}));
diff --git a/src/app/files/files-dashboard/files-dashboard.component.ts b/src/app/files/files-dashboard/files-dashboard.component.ts
index 7eddc0fe3..5473fc45e 100644
--- a/src/app/files/files-dashboard/files-dashboard.component.ts
+++ b/src/app/files/files-dashboard/files-dashboard.component.ts
@@ -1,9 +1,30 @@
-import { Component, OnDestroy } from "@angular/core";
-import { SciCatDataSource } from "../../shared/services/scicat.datasource";
-import { ScicatDataService } from "../../shared/services/scicat-data-service";
-import { ExportExcelService } from "../../shared/services/export-excel.service";
-import { Column } from "shared/modules/shared-table/shared-table.module";
-import { AppConfigService } from "app-config.service";
+import { Component, OnDestroy, OnInit } from "@angular/core";
+import { BehaviorSubject, Subscription } from "rxjs";
+import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model";
+import {
+ ITableSetting,
+ TableSettingEventType,
+} 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,
+ ITableEvent,
+ TableEventType,
+ TableSelectionMode,
+} from "shared/modules/dynamic-material-table/models/table-row.model";
+import { Store } from "@ngrx/store";
+import { ActivatedRoute, Router } from "@angular/router";
+import { updateUserSettingsAction } from "state-management/actions/user.actions";
+import { Sort } from "@angular/material/sort";
+import { selectFilesWithCountAndTableSettings } from "state-management/selectors/files.selectors";
+import { fetchAllOrigDatablocksAction } from "state-management/actions/files.actions";
+import { get } from "lodash-es";
+import { DatePipe } from "@angular/common";
+import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings";
+import { TableConfigService } from "shared/services/table-config.service";
@Component({
selector: "app-files-dashboard",
@@ -11,92 +32,278 @@ import { AppConfigService } from "app-config.service";
styleUrls: ["./files-dashboard.component.scss"],
standalone: false,
})
-export class FilesDashboardComponent implements OnDestroy {
- columns: Column[] = [
- {
- id: "dataFileList.path",
- icon: "text_snippet",
- label: "Filename",
- canSort: true,
- matchMode: "contains",
- hideOrder: 1,
- },
- {
- id: "dataFileList.size",
- icon: "save",
- label: "Size",
- canSort: true,
- matchMode: "greaterThan",
- hideOrder: 2,
- },
- {
- id: "dataFileList.time",
- icon: "access_time",
- label: "Created at",
- format: "date medium",
- canSort: true,
- matchMode: "between",
- sortDefault: "desc",
- hideOrder: 3,
- },
- {
- id: "dataFileList.uid",
- icon: "person",
- label: "UID",
- canSort: true,
- matchMode: "contains",
- hideOrder: 4,
- },
- {
- id: "dataFileList.gid",
- icon: "group",
- label: "GID",
- canSort: true,
- matchMode: "contains",
- hideOrder: 5,
- },
- {
- id: "ownerGroup",
- icon: "group",
- label: "Owner Group",
- canSort: true,
- matchMode: "contains",
- hideOrder: 6,
- },
- {
- id: "datasetId",
- icon: "list",
- label: "Dataset PID",
- type: "dataseturl",
- canSort: true,
- matchMode: "contains",
- hideOrder: 7,
- },
- ];
+export class FilesDashboardComponent implements OnInit, OnDestroy {
+ filesWithCountAndTableSettings$ = this.store.select(
+ selectFilesWithCountAndTableSettings,
+ );
- tableDefinition = {
- collection: "Origdatablocks",
- columns: this.columns,
- };
+ subscriptions: Subscription[] = [];
+
+ tableName = "filesTable";
+
+ columns: TableField[];
+
+ pending = true;
+
+ setting: ITableSetting = {};
+
+ paginationMode: TablePaginationMode = "server-side";
+
+ dataSource: BehaviorSubject