From 9e1e0cda71d74bcf1c85469f5b0f091c9e1fa268 Mon Sep 17 00:00:00 2001 From: minottic Date: Wed, 22 Apr 2026 12:55:09 +0000 Subject: [PATCH 1/6] feat: replace hardcoded jobs with configurable actions This applies both to cart and dataset actions --- src/app/datasets/archiving.service.spec.ts | 202 ------------------ src/app/datasets/archiving.service.ts | 134 ------------ .../batch-view/batch-view.component.html | 20 +- .../batch-view/batch-view.component.ts | 106 ++++----- .../dashboard/dashboard.component.html | 5 +- .../datasets/dashboard/dashboard.component.ts | 4 - .../dataset-table-actions.component.html | 28 +-- .../dataset-table-actions.component.ts | 144 ++++++------- .../dataset-table.component.html | 1 + .../dataset-table.component.spec.ts | 6 +- .../dataset-table/dataset-table.component.ts | 19 +- src/app/datasets/datasets.module.ts | 2 - .../configurable-action.component.spec.ts | 22 ++ .../configurable-action.component.ts | 5 + .../doc/configurable-actions-technical.md | 3 + 15 files changed, 178 insertions(+), 523 deletions(-) delete mode 100644 src/app/datasets/archiving.service.spec.ts delete mode 100644 src/app/datasets/archiving.service.ts diff --git a/src/app/datasets/archiving.service.spec.ts b/src/app/datasets/archiving.service.spec.ts deleted file mode 100644 index 183d5a09d2..0000000000 --- a/src/app/datasets/archiving.service.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { TestBed, waitForAsync } from "@angular/core/testing"; -import { MockStore, provideMockStore } from "@ngrx/store/testing"; -import { RetrieveDestinations } from "app-config.service"; -import { submitJobAction } from "state-management/actions/jobs.actions"; -import { - selectCurrentUser, - selectProfile, - selectTapeCopies, -} from "state-management/selectors/user.selectors"; -import { JobsState } from "state-management/state/jobs.store"; -import { ArchivingService } from "./archiving.service"; -import { createMock, mockDataset } from "shared/MockStubs"; -import { - ReturnedUserDto, - CreateJobDtoV3, -} from "@scicatproject/scicat-sdk-ts-angular"; - -describe("ArchivingService", () => { - let service: ArchivingService; - let store: MockStore; - let dispatchSpy: jasmine.Spy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - providers: [ - ArchivingService, - provideMockStore({ - selectors: [ - { - selector: selectCurrentUser, - value: createMock({ - email: "test@email.com", - username: "testName", - authStrategy: "", - id: "", - }), - }, - { selector: selectTapeCopies, value: "test" }, - { selector: selectProfile, value: { email: "test@email.com" } }, - ], - }), - ], - }); - - service = TestBed.inject(ArchivingService); - store = TestBed.inject(MockStore); - })); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - describe("#createJob()", () => { - it("should create a new object of type Job", () => { - const user = createMock({ - username: "testName", - email: "test@email.com", - authStrategy: "", - id: "", - }); - const datasets = [mockDataset]; - const datasetList = datasets.map((dataset) => ({ - pid: dataset.pid, - files: [], - })); - const archive = true; - const destinationPath = { destinationPath: "/test/path/" }; - - const job = service["createJob"]( - user, - datasets, - archive, - destinationPath, - ); - - expect(job).toBeDefined(); - expect(job["emailJobInitiator"]).toEqual("test@email.com"); - expect(job["jobParams"]["username"]).toEqual("testName"); - expect(job["datasetList"]).toEqual(datasetList); - expect(job["type"]).toEqual("archive"); - }); - }); - - describe("#archiveOrRetrieve()", () => { - xit("should throw an error if no datasets are selected", () => { - const datasets = []; - const archive = true; - - service["archiveOrRetrieve"](datasets, archive).subscribe((res) => { - expect(res).toThrowError("No datasets selected"); - }); - }); - - xit("should call #createJob() and then dispatch a submitJobAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const user = createMock({ - username: "testName", - email: "test@email.com", - authStrategy: "", - id: "", - }); - const datasets = [mockDataset]; - const datasetList = datasets.map((dataset) => ({ - pid: dataset.pid, - files: [], - })); - const archive = true; - - const job = createMock({ - jobParams: { username: user.username }, - emailJobInitiator: user.email, - datasetList, - type: "archive", - executionTime: "", - jobResultObject: {}, - jobStatusMessage: "", - }); - const createJobSpy = spyOn( - service, - "createJob", - ).and.returnValue(job); - - service["archiveOrRetrieve"](datasets, archive).subscribe(() => { - expect(createJobSpy).toHaveBeenCalledWith( - user, - datasets, - archive, - undefined, - ); - expect(dispatchSpy).toHaveBeenCalledOnceWith(submitJobAction({ job })); - }); - }); - }); - - describe("#archive()", () => { - it("should call #archiveOrRetrieve() with archive set to `true`", () => { - const archiveOrRetrieveSpy = spyOn( - service, - "archiveOrRetrieve", - ); - const datasets = [mockDataset]; - - service.archive(datasets); - - expect(archiveOrRetrieveSpy).toHaveBeenCalledOnceWith(datasets, true); - }); - }); - - describe("#retrieve()", () => { - it("should call #archiveOrRetrieve() with archive set to `false`", () => { - const archiveOrRetrieveSpy = spyOn( - service, - "archiveOrRetrieve", - ); - const datasets = [mockDataset]; - const destinationPath = { location: "/test/path/" }; - - service.retrieve(datasets, destinationPath); - - expect(archiveOrRetrieveSpy).toHaveBeenCalledOnceWith( - datasets, - false, - destinationPath, - ); - }); - }); - - describe("#generateOptionLocation()", () => { - it("should return the generated path", () => { - const result = { option: "option", location: "relative" }; - const destinations = [ - { option: "option", location: "/root/" }, - { option: "option2" }, - ]; - expect(service.generateOptionLocation(result, destinations)).toEqual({ - option: "option", - location: "/root/relative", - }); - }); - }); - - describe("#retriveDialogOptions()", () => { - it("should return the dialog options when retrieving", () => { - const destinations: RetrieveDestinations[] = [ - { option: "option1" }, - { option: "option2" }, - ]; - expect(service.retriveDialogOptions(destinations)).toEqual({ - width: "auto", - data: { - title: "Retrieve to", - question: "", - choice: { - options: destinations, - }, - option: destinations[0].option, - }, - }); - }); - }); -}); diff --git a/src/app/datasets/archiving.service.ts b/src/app/datasets/archiving.service.ts deleted file mode 100644 index 99a5489ce8..0000000000 --- a/src/app/datasets/archiving.service.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Injectable } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { combineLatest, Observable } from "rxjs"; -import { first, map } from "rxjs/operators"; -import { submitJobAction } from "state-management/actions/jobs.actions"; -import { - selectCurrentUser, - selectTapeCopies, - selectProfile, -} from "state-management/selectors/user.selectors"; -import { RetrieveDestinations } from "app-config.service"; -import { - OutputDatasetObsoleteDto, - ReturnedUserDto, -} from "@scicatproject/scicat-sdk-ts-angular"; - -@Injectable() -export class ArchivingService { - private currentUser$ = this.store.select(selectCurrentUser); - private tapeCopies$ = this.store.select(selectTapeCopies); - - constructor(private store: Store) {} - - private createJob( - user: ReturnedUserDto, - datasets: OutputDatasetObsoleteDto[], - archive: boolean, - destinationPath?: Record, - // Do not specify tape copies here - ) { - const extra = archive ? {} : destinationPath; - const jobParams = { - username: user.username, - ...extra, - }; - - this.store.select(selectProfile).subscribe((profile) => { - user.email = profile.email; - }); - - const data = { - jobParams, - emailJobInitiator: user.email, - // Revise this, files == []...? See earlier version of this method in dataset-table component for context - datasetList: datasets.map((dataset) => ({ - pid: dataset.pid, - files: [], - })), - type: archive ? "archive" : "retrieve", - }; - - return data; - } - - private archiveOrRetrieve( - datasets: OutputDatasetObsoleteDto[], - archive: boolean, - destPath?: Record, - ): Observable { - return combineLatest([this.currentUser$, this.tapeCopies$]).pipe( - first(), - map(([user, tapeCopies]) => { - if (user) { - const email = user.email; - if (email == null || email.length === 0) { - throw new Error( - "No email for this user could be found, the job will not be submitted", - ); - } - - if (datasets.length === 0) { - throw new Error("No datasets selected"); - } - - const job = this.createJob(user, datasets, archive, destPath); - - this.store.dispatch(submitJobAction({ job: job as any })); - } - }), - ); - } - - public archive(datasets: OutputDatasetObsoleteDto[]): Observable { - return this.archiveOrRetrieve(datasets, true); - } - - public retrieve( - datasets: OutputDatasetObsoleteDto[], - destinationPath: Record, - ): Observable { - return this.archiveOrRetrieve(datasets, false, destinationPath); - } - - public generateOptionLocation( - result: RetrieveDestinations, - retrieveDestinations: RetrieveDestinations[] = [], - ): object | { option: string } | { location: string; option: string } { - if (retrieveDestinations.length > 0) { - const prefix = retrieveDestinations.filter( - (element) => element.option == result.option, - ); - let location = - prefix.length > 0 - ? (prefix[0].location || "") + (result.location || "") - : ""; - let option = result.option; - if (!result.option) { - location = retrieveDestinations[0].location || ""; - option = retrieveDestinations[0].option; - } - return { - option: option, - ...(location != "" ? { location: location } : {}), - }; - } - return {}; - } - - public retriveDialogOptions( - retrieveDestinations: RetrieveDestinations[] = [], - ): object { - return { - width: "auto", - data: { - title: "Retrieve to", - question: "", - choice: { - options: retrieveDestinations, - }, - option: retrieveDestinations?.[0]?.option, - }, - }; - } -} diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index 426ffb2f13..70758c7c4b 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -84,21 +84,11 @@ Share - - - - + + this.archivingSrv.archive(datasets)), - ) - .subscribe( - () => this.clearBatch(), - (err) => - this.store.dispatch( - showMessageAction({ - message: { - type: MessageType.Error, - content: err.message, - duration: 5000, - }, - }), - ), - ); - } - - onRetrieve() { - const dialogOptions = this.archivingSrv.retriveDialogOptions( - this.appConfig.retrieveDestinations, - ); - const dialogRef = this.dialog.open(DialogComponent, dialogOptions); - const destPath = { destinationPath: "/archive/retrieve" }; - dialogRef.afterClosed().subscribe((result) => { - if (result && this.datasetList) { - const locationOption = this.archivingSrv.generateOptionLocation( - result, - this.appConfig.retrieveDestinations, - ); - const extra = { ...destPath, ...locationOption }; - this.archivingSrv.retrieve(this.datasetList, extra).subscribe( - () => this.clearBatch(), - (err) => - this.store.dispatch( - showMessageAction({ - message: { - type: MessageType.Error, - content: err.message, - duration: 5000, - }, - }), - ), - ); - } - - this.router.navigate([], { - queryParams: { retrieve: null }, - queryParamsHandling: "merge", - }); - }); - } - onSaveChanges() { this.store.dispatch( resyncPublishedDataAction({ @@ -344,12 +294,17 @@ export class BatchViewComponent implements OnInit, OnDestroy { }) .unsubscribe(); this.store.dispatch(fetchInstrumentsAction({ limit: 1000, skip: 0 })); + this.store.dispatch(prefillBatchAction()); this.subscriptions.push( this.batch$.subscribe((result) => { if (result) { this.datasetList = result; this.hasBatch = result.length > 0; + this.actionItems = { + datasets: result as ActionItemDataset[], + user: this.userProfile as UserProfile, + }; } }), ); @@ -359,13 +314,34 @@ export class BatchViewComponent implements OnInit, OnDestroy { if (queryParams["share"] === "true") { this.onShare(); } - if (queryParams["retrieve"] === "true") { - this.onRetrieve(); - } }), ); } + onActionFinished(event: { + success: boolean; + result?: unknown; + error?: Error; + }) { + if (event.success) return this.clearBatch(); + const errorMessage = + typeof event.error === "string" + ? event.error + : event.error?.message || "Action failed"; + + if (errorMessage !== "Cancelled by user") { + this.store.dispatch( + showMessageAction({ + message: { + type: MessageType.Error, + content: errorMessage, + duration: 5000, + }, + }), + ); + } + } + ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/datasets/dashboard/dashboard.component.html b/src/app/datasets/dashboard/dashboard.component.html index b42ee5e3d0..59246f88cb 100644 --- a/src/app/datasets/dashboard/dashboard.component.html +++ b/src/app/datasets/dashboard/dashboard.component.html @@ -5,9 +5,7 @@
- +
@@ -37,7 +35,6 @@
diff --git a/src/app/datasets/dashboard/dashboard.component.ts b/src/app/datasets/dashboard/dashboard.component.ts index 3097b717ce..1293eea6a2 100644 --- a/src/app/datasets/dashboard/dashboard.component.ts +++ b/src/app/datasets/dashboard/dashboard.component.ts @@ -11,7 +11,6 @@ import { fetchFacetCountsAction, prefillBatchAction, prefillFiltersAction, - addDatasetAction, fetchDatasetCompleteAction, fetchMetadataKeysAction, changePageAction, @@ -23,14 +22,12 @@ import { import { selectHasPrefilledFilters, selectCurrentDataset, - selectSelectedDatasets, selectPagination, selectIsBatchNonEmpty, } from "state-management/selectors/datasets.selectors"; 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, lastValueFrom } from "rxjs"; import { selectProfile, @@ -59,7 +56,6 @@ export class DashboardComponent implements OnInit, OnDestroy { .select(selectHasPrefilledFilters) .pipe(filter((has) => has)); loggedIn$ = this.store.select(selectIsLoggedIn); - selectedSets$ = this.store.select(selectSelectedDatasets); selectColumns$ = this.store.select(selectColumns); selectHasFetchedSettings$ = this.store.select(selectHasFetchedSettings); 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 950ec89689..61cf2b0990 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 @@ -15,27 +15,15 @@
- - - +