diff --git a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts index c272748157..991fa38ed3 100644 --- a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts +++ b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.spec.ts @@ -105,6 +105,18 @@ describe("DatasetTableActionsComponent", () => { setArchiveViewModeAction({ modeToggle }), ); }); + + it("should dispatch selected non-default archive mode", () => { + dispatchSpy = spyOn(store, "dispatch"); + const modeToggle = ArchViewMode.deleted; + + component.onModeChange(modeToggle); + + expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(dispatchSpy).toHaveBeenCalledWith( + setArchiveViewModeAction({ modeToggle }), + ); + }); }); describe("#isEmptySelection()", () => { diff --git a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.ts b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.ts index 39b48a8248..bc28185dd9 100644 --- a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.ts +++ b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.ts @@ -38,6 +38,7 @@ export class DatasetTableActionsComponent implements OnInit, OnDestroy { ArchViewMode.work_in_progress, ArchViewMode.system_error, ArchViewMode.user_error, + ArchViewMode.deleted, ]; searchPublicDataEnabled = this.appConfig.searchPublicDataEnabled; diff --git a/src/app/shared/modules/breadcrumb/breadcrumb.component.spec.ts b/src/app/shared/modules/breadcrumb/breadcrumb.component.spec.ts index 1904cc4bb8..0a8f10f672 100644 --- a/src/app/shared/modules/breadcrumb/breadcrumb.component.spec.ts +++ b/src/app/shared/modules/breadcrumb/breadcrumb.component.spec.ts @@ -3,14 +3,23 @@ import { provideHttpClient, withInterceptorsFromDi, } from "@angular/common/http"; +import { Location } from "@angular/common"; +import { of } from "rxjs"; import { MockStore } from "shared/MockStubs"; import { BreadcrumbComponent } from "./breadcrumb.component"; import { Store } from "@ngrx/store"; -import { provideRouter } from "@angular/router"; +import { provideRouter, Router } from "@angular/router"; +import { ArchViewMode } from "state-management/models"; +import { + setArchiveViewModeAction, + setFiltersAction, +} from "state-management/actions/datasets.actions"; describe("BreadcrumbComponent", () => { let component: BreadcrumbComponent; let fixture: ComponentFixture; + let store: MockStore; + let router: Router; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -27,6 +36,8 @@ describe("BreadcrumbComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(BreadcrumbComponent); component = fixture.componentInstance; + store = TestBed.inject(Store) as unknown as MockStore; + router = TestBed.inject(Router); fixture.detectChanges(); }); @@ -37,4 +48,57 @@ describe("BreadcrumbComponent", () => { it("should create", () => { expect(component).toBeTruthy(); }); + + it("should dispatch setFiltersAction and setArchiveViewModeAction for datasets breadcrumb", () => { + const dispatchSpy = spyOn(store, "dispatch"); + const selectSpy = spyOn(store, "select").and.returnValues( + of({ text: "abc", skip: 7 }) as unknown as ReturnType< + MockStore["select"] + >, + of(ArchViewMode.deleted) as unknown as ReturnType, + ); + const backSpy = spyOn(TestBed.inject(Location), "back"); + const crumb = { + label: "datasets", + path: "datasets", + params: {}, + url: "/datasets", + fallback: "/datasets", + }; + + component.crumbClick(0, crumb); + + expect(selectSpy).toHaveBeenCalledTimes(2); + expect(dispatchSpy).toHaveBeenCalledTimes(2); + expect((dispatchSpy.calls.argsFor(0) as object)[0]).toEqual( + setFiltersAction({ + datasetFilters: { + text: "abc", + skip: 7, + }, + }), + ); + expect((dispatchSpy.calls.argsFor(1) as object)[0]).toEqual( + setArchiveViewModeAction({ modeToggle: ArchViewMode.deleted }), + ); + expect(backSpy).toHaveBeenCalled(); + }); + + it("should navigate by url for non-datasets breadcrumb", async () => { + const navigateSpy = spyOn(router, "navigateByUrl").and.returnValue( + Promise.resolve(true), + ); + const crumb = { + label: "about", + path: "about", + params: {}, + url: "/about", + fallback: "/about", + }; + + component.crumbClick(0, crumb); + await fixture.whenStable(); + + expect(navigateSpy).toHaveBeenCalledWith("/about"); + }); }); diff --git a/src/app/shared/modules/breadcrumb/breadcrumb.component.ts b/src/app/shared/modules/breadcrumb/breadcrumb.component.ts index 4a67102596..044e485dd0 100644 --- a/src/app/shared/modules/breadcrumb/breadcrumb.component.ts +++ b/src/app/shared/modules/breadcrumb/breadcrumb.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { Router, ActivatedRoute, NavigationEnd, Params } from "@angular/router"; import { Store } from "@ngrx/store"; +import { combineLatest } from "rxjs"; import { selectArchiveViewMode, @@ -8,8 +9,11 @@ import { } from "state-management/selectors/datasets.selectors"; import { take, filter } from "rxjs/operators"; import { TitleCasePipe } from "shared/pipes/title-case.pipe"; -import { ArchViewMode } from "state-management/models"; import { Location } from "@angular/common"; +import { + setFiltersAction, + setArchiveViewModeAction, +} from "state-management/actions/datasets.actions"; interface Breadcrumb { label: string; @@ -49,7 +53,7 @@ export class BreadcrumbComponent implements OnInit { // Update breadcrumb when navigating to child routes this.router.events .pipe(filter((event) => event instanceof NavigationEnd)) - .subscribe((event) => { + .subscribe(() => { this.setBreadcrumbs(); }); } @@ -115,81 +119,20 @@ export class BreadcrumbComponent implements OnInit { } // this catches errors and redirects to the fallback, this could/should be set in the routing module? if (crumb.fallback === "/datasets") { - this.store - .select(selectFilters) - .pipe(take(1)) - .subscribe((filters) => { - this.store - .select(selectArchiveViewMode) - .pipe(take(1)) - .subscribe((currentMode) => { - filters["mode"] = setMode(currentMode); - this.location.back(); - }); - }); + combineLatest([ + this.store.select(selectFilters).pipe(take(1)), + this.store.select(selectArchiveViewMode).pipe(take(1)), + ]).subscribe(([filters, modeToggle]) => { + this.store.dispatch( + setFiltersAction({ datasetFilters: { ...filters } }), + ); + this.store.dispatch(setArchiveViewModeAction({ modeToggle })); + this.location.back(); + }); } else { this.router .navigateByUrl(url + crumb.url) - .catch((error) => this.router.navigateByUrl(url + crumb.fallback)); + .catch(() => this.router.navigateByUrl(url + crumb.fallback)); } } } - -const setMode = (modeToggle: ArchViewMode) => { - switch (modeToggle) { - case ArchViewMode.all: - return {}; - case ArchViewMode.archivable: - return { - "datasetlifecycle.archivable": true, - "datasetlifecycle.retrievable": false, - }; - case ArchViewMode.retrievable: - return { - "datasetlifecycle.retrievable": true, - "datasetlifecycle.archivable": false, - }; - case ArchViewMode.work_in_progress: - return { - $or: [ - { - "datasetlifecycle.retrievable": false, - "datasetlifecycle.archivable": false, - "datasetlifecycle.archiveStatusMessage": { - $ne: "scheduleArchiveJobFailed", - }, - "datasetlifecycle.retrieveStatusMessage": { - $ne: "scheduleRetrieveJobFailed", - }, - }, - ], - }; - case ArchViewMode.system_error: - return { - $or: [ - { - "datasetlifecycle.retrievable": true, - "datasetlifecycle.archivable": true, - }, - { - "datasetlifecycle.archiveStatusMessage": "scheduleArchiveJobFailed", - }, - { - "datasetlifecycle.retrieveStatusMessage": - "scheduleRetrieveJobFailed", - }, - ], - }; - case ArchViewMode.user_error: - return { - $or: [ - { - "datasetlifecycle.archiveStatusMessage": "missingFilesError", - }, - ], - }; - default: { - return {}; - } - } -}; diff --git a/src/app/shared/services/datasets-list.service.ts b/src/app/shared/services/datasets-list.service.ts index bb6bf7ed60..5545b986b3 100644 --- a/src/app/shared/services/datasets-list.service.ts +++ b/src/app/shared/services/datasets-list.service.ts @@ -112,6 +112,10 @@ export class DatasetsListService implements OnDestroy { return false; } + deletedCondition(dataset: DatasetClass): boolean { + return dataset.datasetlifecycle.archiveStatusMessage === "deleted"; + } + convertSavedDatasetColumns(columns: TableColumn[]): TableField[] { return columns .filter((column) => column.name !== "select") @@ -188,6 +192,8 @@ export class DatasetsListService implements OnDestroy { return "Archivable"; } else if (this.retrievableCondition(row)) { return "Retrievable"; + } else if (this.deletedCondition(row)) { + return "Deleted"; } else if (this.systemErrorCondition(row)) { return "System error"; } else if (this.userErrorCondition(row)) { @@ -204,6 +210,8 @@ export class DatasetsListService implements OnDestroy { return "Archivable"; } else if (this.retrievableCondition(row)) { return "Retrievable"; + } else if (this.deletedCondition(row)) { + return "Deleted"; } else if (this.systemErrorCondition(row)) { return "System error"; } else if (this.userErrorCondition(row)) { diff --git a/src/app/state-management/actions/datasets.actions.ts b/src/app/state-management/actions/datasets.actions.ts index 8a00c121a3..32e204375e 100644 --- a/src/app/state-management/actions/datasets.actions.ts +++ b/src/app/state-management/actions/datasets.actions.ts @@ -299,10 +299,7 @@ export const setTextFilterAction = createAction( export const setFiltersAction = createAction( "[Dataset] Set Filters", props<{ - datasetFilters: Record< - string, - string | DateRange | string[] | INumericRange - >; + datasetFilters: Partial; }>(), ); export const addDatasetFilterAction = createAction( diff --git a/src/app/state-management/models/index.ts b/src/app/state-management/models/index.ts index 151ef5ffac..6a0e4a67f6 100644 --- a/src/app/state-management/models/index.ts +++ b/src/app/state-management/models/index.ts @@ -124,6 +124,7 @@ export enum ArchViewMode { work_in_progress = "work in progress", system_error = "system error", user_error = "user error", + deleted = "deleted", } export enum JobViewMode { myJobs = "my jobs", diff --git a/src/app/state-management/reducers/datasets.reducer.spec.ts b/src/app/state-management/reducers/datasets.reducer.spec.ts index 3cb61cbc7a..24119f7115 100644 --- a/src/app/state-management/reducers/datasets.reducer.spec.ts +++ b/src/app/state-management/reducers/datasets.reducer.spec.ts @@ -350,6 +350,40 @@ describe("DatasetsReducer", () => { expect(state.filters.modeToggle).toEqual(modeToggle); expect(state.filters.skip).toEqual(0); }); + + it("should set deleted mode filter", () => { + const modeToggle = ArchViewMode.deleted; + + const action = fromActions.setArchiveViewModeAction({ modeToggle }); + const state = fromDatasets.datasetsReducer(initialDatasetState, action); + + expect(state.filters.mode).toEqual({ + "datasetlifecycle.archiveStatusMessage": "deleted", + }); + expect(state.filters.modeToggle).toEqual(modeToggle); + expect(state.filters.skip).toEqual(0); + }); + + it("should preserve existing skip when mode changes", () => { + const modeToggle = ArchViewMode.archivable; + const stateIn = { + ...initialDatasetState, + filters: { + ...initialDatasetState.filters, + skip: 42, + }, + }; + + const action = fromActions.setArchiveViewModeAction({ modeToggle }); + const state = fromDatasets.datasetsReducer(stateIn, action); + + expect(state.filters.skip).toEqual(42); + expect(state.filters.modeToggle).toEqual(modeToggle); + expect(state.filters.mode).toEqual({ + "datasetlifecycle.archivable": true, + "datasetlifecycle.retrievable": false, + }); + }); }); describe("on setPublicViewMode", () => { @@ -377,6 +411,36 @@ describe("DatasetsReducer", () => { }); }); + describe("on setFiltersAction", () => { + it("should restore filters without updating searchTerms or hasPrefilledFilters", () => { + const stateIn = { + ...initialDatasetState, + searchTerms: "keep-me", + hasPrefilledFilters: false, + }; + const datasetFilters = { + text: "restored", + skip: 12, + modeToggle: ArchViewMode.deleted, + mode: { + "datasetlifecycle.archiveStatusMessage": "deleted", + }, + }; + + const action = fromActions.setFiltersAction({ datasetFilters }); + const state = fromDatasets.datasetsReducer(stateIn, action); + + expect(state.filters.text).toEqual("restored"); + expect(state.filters.skip).toEqual(12); + expect(state.filters.modeToggle).toEqual(ArchViewMode.deleted); + expect(state.filters.mode).toEqual({ + "datasetlifecycle.archiveStatusMessage": "deleted", + }); + expect(state.searchTerms).toEqual("keep-me"); + expect(state.hasPrefilledFilters).toEqual(false); + }); + }); + describe("on clearFacetsAction", () => { it("should clear filters while saving the filters limit and set searchTerms to an empty string", () => { const limit = 10; diff --git a/src/app/state-management/reducers/datasets.reducer.ts b/src/app/state-management/reducers/datasets.reducer.ts index 160a2279a9..6391084561 100644 --- a/src/app/state-management/reducers/datasets.reducer.ts +++ b/src/app/state-management/reducers/datasets.reducer.ts @@ -343,11 +343,16 @@ const reducer = createReducer( ], }; break; + case ArchViewMode.deleted: + mode = { + "datasetlifecycle.archiveStatusMessage": "deleted", + }; + break; default: { break; } } - const filters = { ...state.filters, skip: 0, mode, modeToggle }; + const filters = { skip: 0, ...state.filters, mode, modeToggle }; return { ...state, filters }; }, ),