From 8ec859808fb34cefdc897e9e758cf3c5a5d8a747 Mon Sep 17 00:00:00 2001 From: martin-trajanovski Date: Mon, 25 Aug 2025 10:59:54 +0200 Subject: [PATCH 01/32] feat: make configurable action buttons shared component --- .../datafiles-actions.component.scss | 3 - .../datafiles/datafiles.component.html | 98 +------------------ .../datafiles/datafiles.component.spec.ts | 2 +- .../datasets/datafiles/datafiles.component.ts | 2 +- src/app/datasets/datasets.module.ts | 4 - src/app/shared/MockStubs.ts | 2 +- .../configurable-action.component.html} | 0 .../configurable-action.component.scss} | 0 .../configurable-action.component.spec.ts} | 36 ++----- .../configurable-action.component.ts} | 20 ++-- .../configurable-action.interfaces.ts} | 0 .../configurable-actions.component.html} | 6 +- .../configurable-actions.component.scss | 3 + .../configurable-actions.component.spec.ts} | 18 ++-- .../configurable-actions.component.ts} | 24 ++--- .../configurable-actions.documentation.md} | 0 .../configurable-actions.module.ts | 14 +++ src/app/shared/shared.module.ts | 3 + 18 files changed, 61 insertions(+), 174 deletions(-) delete mode 100644 src/app/datasets/datafiles-actions/datafiles-actions.component.scss rename src/app/{datasets/datafiles-actions/datafiles-action.component.html => shared/modules/configurable-actions/configurable-action.component.html} (100%) rename src/app/{datasets/datafiles-actions/datafiles-action.component.scss => shared/modules/configurable-actions/configurable-action.component.scss} (100%) rename src/app/{datasets/datafiles-actions/datafiles-action.component.spec.ts => shared/modules/configurable-actions/configurable-action.component.spec.ts} (95%) rename src/app/{datasets/datafiles-actions/datafiles-action.component.ts => shared/modules/configurable-actions/configurable-action.component.ts} (92%) rename src/app/{datasets/datafiles-actions/datafiles-action.interfaces.ts => shared/modules/configurable-actions/configurable-action.interfaces.ts} (100%) rename src/app/{datasets/datafiles-actions/datafiles-actions.component.html => shared/modules/configurable-actions/configurable-actions.component.html} (73%) create mode 100644 src/app/shared/modules/configurable-actions/configurable-actions.component.scss rename src/app/{datasets/datafiles-actions/datafiles-actions.component.spec.ts => shared/modules/configurable-actions/configurable-actions.component.spec.ts} (90%) rename src/app/{datasets/datafiles-actions/datafiles-actions.component.ts => shared/modules/configurable-actions/configurable-actions.component.ts} (57%) rename src/app/{datasets/datafiles-actions/datafiles-actions.documentation.md => shared/modules/configurable-actions/configurable-actions.documentation.md} (100%) create mode 100644 src/app/shared/modules/configurable-actions/configurable-actions.module.ts diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.scss b/src/app/datasets/datafiles-actions/datafiles-actions.component.scss deleted file mode 100644 index cdc435d73b..0000000000 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.dataset-datafiles-actions { - float: right; -} \ No newline at end of file diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index 001ca7d386..f315bc1738 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -32,105 +32,11 @@

No files associated to this dataset

- - + > { - let component: DatafilesActionComponent; - let fixture: ComponentFixture; +describe("1000: ConfigurableActionComponent", () => { + let component: ConfigurableActionComponent; + let fixture: ComponentFixture; let htmlForm: HTMLFormElement; let htmlInput: HTMLInputElement; @@ -175,15 +175,6 @@ describe("1000: DatafilesActionComponent", () => { id: "4ac45f3e-4d79-11ef-856c-6339dab93bee", }); - // const browserWindowMock = { - // document: { - // write() {}, - // body: { - // setAttribute() {}, - // }, - // }, - // } as unknown as Window; - beforeAll(() => { htmlForm = document.createElement("form"); (htmlForm as HTMLFormElement).submit = () => {}; @@ -205,9 +196,9 @@ describe("1000: DatafilesActionComponent", () => { RouterModule.forRoot([]), StoreModule.forRoot({}), ], - declarations: [DatafilesActionComponent], + declarations: [ConfigurableActionComponent], }); - TestBed.overrideComponent(DatafilesActionComponent, { + TestBed.overrideComponent(ConfigurableActionComponent, { set: { providers: [ { provide: UsersService, useClass: MockUserApi }, @@ -227,7 +218,7 @@ describe("1000: DatafilesActionComponent", () => { })); beforeEach(() => { - fixture = TestBed.createComponent(DatafilesActionComponent); + fixture = TestBed.createComponent(ConfigurableActionComponent); component = fixture.componentInstance; component.files = structuredClone(actionFiles); component.actionConfig = actionsConfig[0]; @@ -532,10 +523,8 @@ describe("1000: DatafilesActionComponent", () => { switch (selectedFiles) { case selectedFilesType.file1: component.files[0].selected = true; - //component.files[1].selected = false; break; case selectedFilesType.file2: - //component.files[0].selected = false; component.files[1].selected = true; break; case selectedFilesType.all: @@ -555,8 +544,6 @@ describe("1000: DatafilesActionComponent", () => { }); function createFakeElement(elementType: string): HTMLElement { - //const element = new MockHtmlElement(elementType); - //return element as unknown as HTMLElement; let element: HTMLElement = null; switch (elementType) { @@ -580,7 +567,6 @@ describe("1000: DatafilesActionComponent", () => { ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -600,7 +586,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFilesType.none, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -616,7 +601,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFilesType.none, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -639,7 +623,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFile, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -664,7 +647,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFilesType.none, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -684,7 +666,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFilesType.none, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); @@ -701,7 +682,6 @@ describe("1000: DatafilesActionComponent", () => { selectedFile, ); spyOn(document, "createElement").and.callFake(createFakeElement); - //spyOn(window, "open").and.returnValue(browserWindowMock); component.perform_action(); diff --git a/src/app/datasets/datafiles-actions/datafiles-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts similarity index 92% rename from src/app/datasets/datafiles-actions/datafiles-action.component.ts rename to src/app/shared/modules/configurable-actions/configurable-action.component.ts index e710dc544b..30244f0562 100644 --- a/src/app/datasets/datafiles-actions/datafiles-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -7,22 +7,22 @@ import { } from "@angular/core"; import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; -import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces"; +import { ActionConfig, ActionDataset } from "./configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AuthService } from "shared/services/auth/auth.service"; import { v4 } from "uuid"; import { MatSnackBar } from "@angular/material/snack-bar"; @Component({ - selector: "datafiles-action", - templateUrl: "./datafiles-action.component.html", - styleUrls: ["./datafiles-action.component.scss"], + selector: "configurable-action", + templateUrl: "./configurable-action.component.html", + styleUrls: ["./configurable-action.component.scss"], standalone: false, }) -export class DatafilesActionComponent implements OnInit, OnChanges { +export class ConfigurableActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionConfig: ActionConfig; @Input({ required: true }) actionDataset: ActionDataset; - @Input({ required: true }) files: DataFiles_File[]; + @Input({ required: true }) files?: DataFiles_File[]; @Input({ required: true }) maxFileSize: number; jwt = ""; @@ -77,21 +77,19 @@ export class DatafilesActionComponent implements OnInit, OnChanges { this.use_icon = this.actionConfig.icon !== undefined; this.prepare_disabled_condition(); this.update_status(); - //this.compute_disabled(); } ngOnChanges(changes: SimpleChanges) { if (changes["files"]) { this.update_status(); - //this.compute_disabled(); } } update_status() { this.selectedTotalFileSize = this.files - .filter((item) => item.selected || this.actionConfig.files === "all") + ?.filter((item) => item.selected || this.actionConfig.files === "all") .reduce((sum, item) => sum + item.size, 0); - this.numberOfFileSelected = this.files.filter( + this.numberOfFileSelected = this.files?.filter( (item) => item.selected, ).length; } @@ -110,7 +108,7 @@ export class DatafilesActionComponent implements OnInit, OnChanges { }); } - add_input(name, value) { + add_input(name: string, value: string) { const input = document.createElement("input"); input.type = "hidden"; input.name = name; diff --git a/src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts similarity index 100% rename from src/app/datasets/datafiles-actions/datafiles-action.interfaces.ts rename to src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.html b/src/app/shared/modules/configurable-actions/configurable-actions.component.html similarity index 73% rename from src/app/datasets/datafiles-actions/datafiles-actions.component.html rename to src/app/shared/modules/configurable-actions/configurable-actions.component.html index b35f645ea2..9df216f7d9 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.html +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.html @@ -1,12 +1,12 @@ -
- + - +
diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.scss b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss new file mode 100644 index 0000000000..f5b28b0969 --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss @@ -0,0 +1,3 @@ +.configurable-actions { + float: right; +} \ No newline at end of file diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts similarity index 90% rename from src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts rename to src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts index 5c80ebec87..8b55e3ff71 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.spec.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { DatafilesActionsComponent } from "./datafiles-actions.component"; +import { ConfigurableActionsComponent } from "./configurable-actions.component"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { MatButtonModule } from "@angular/material/button"; import { MatIconModule } from "@angular/material/icon"; @@ -14,9 +14,9 @@ import { MockMatDialogRef, MockUserApi } from "shared/MockStubs"; import { AppConfigService } from "app-config.service"; import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; -describe("DatafilesActionsComponent", () => { - let component: DatafilesActionsComponent; - let fixture: ComponentFixture; +describe("ConfigurableActionsComponent", () => { + let component: ConfigurableActionsComponent; + let fixture: ComponentFixture; const mockAppConfigService = { getConfig: () => { return { @@ -86,9 +86,9 @@ describe("DatafilesActionsComponent", () => { RouterModule.forRoot([]), StoreModule.forRoot({}), ], - declarations: [DatafilesActionsComponent], + declarations: [ConfigurableActionsComponent], }); - TestBed.overrideComponent(DatafilesActionsComponent, { + TestBed.overrideComponent(ConfigurableActionsComponent, { set: { providers: [ { provide: UsersService, useClass: MockUserApi }, @@ -102,7 +102,7 @@ describe("DatafilesActionsComponent", () => { })); beforeEach(() => { - fixture = TestBed.createComponent(DatafilesActionsComponent); + fixture = TestBed.createComponent(ConfigurableActionsComponent); component = fixture.componentInstance; component.files = [ { @@ -189,7 +189,7 @@ describe("DatafilesActionsComponent", () => { it("there should be 4 actions as defined in default configuration", async () => { expect(component.sortedActionsConfig.length).toEqual(actionsConfig.length); const htmlElement: HTMLElement = fixture.nativeElement; - const htmlActions = htmlElement.querySelectorAll("datafiles-action"); + const htmlActions = htmlElement.querySelectorAll("configurable-action"); expect(htmlActions.length).toEqual(actionsConfig.length); }); @@ -198,7 +198,7 @@ describe("DatafilesActionsComponent", () => { fixture.detectChanges(); expect(component.sortedActionsConfig.length).toEqual(0); const htmlElement: HTMLElement = fixture.nativeElement; - const htmlActions = htmlElement.querySelectorAll("datafiles-action"); + const htmlActions = htmlElement.querySelectorAll("configurable-action"); expect(htmlActions.length).toEqual(0); }); }); diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts similarity index 57% rename from src/app/datasets/datafiles-actions/datafiles-actions.component.ts rename to src/app/shared/modules/configurable-actions/configurable-actions.component.ts index a6698cc915..d593908091 100644 --- a/src/app/datasets/datafiles-actions/datafiles-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -1,37 +1,27 @@ import { Component, Input } from "@angular/core"; -import { ActionConfig, ActionDataset } from "./datafiles-action.interfaces"; +import { ActionConfig, ActionDataset } from "./configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AppConfigService } from "app-config.service"; -//import { DatafilesActionComponent } from "./datafiles-action.component"; @Component({ - selector: "datafiles-actions", - //standalone: true, - //imports: [DatafilesActionComponent], - templateUrl: "./datafiles-actions.component.html", - styleUrls: ["./datafiles-actions.component.scss"], + selector: "configurable-actions", + templateUrl: "./configurable-actions.component.html", + styleUrls: ["./configurable-actions.component.scss"], standalone: false, }) -export class DatafilesActionsComponent { +export class ConfigurableActionsComponent { private _sortedActionsConfig: ActionConfig[]; @Input({ required: true }) actionsConfig: ActionConfig[]; @Input({ required: true }) actionDataset: ActionDataset; - @Input({ required: true }) files: DataFiles_File[]; + @Input({ required: true }) files?: DataFiles_File[]; constructor(public appConfigService: AppConfigService) {} - // ngOnInit() { - // this.sortedActionsConfig = this.actionsConfig; - // this.sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => - // a.order && b.order ? a.order - b.order : 0, - // ); - // } - get visible(): boolean { return ( this.appConfigService.getConfig().datafilesActionsEnabled && - this.files.length > 0 + this.files?.length > 0 ); } diff --git a/src/app/datasets/datafiles-actions/datafiles-actions.documentation.md b/src/app/shared/modules/configurable-actions/configurable-actions.documentation.md similarity index 100% rename from src/app/datasets/datafiles-actions/datafiles-actions.documentation.md rename to src/app/shared/modules/configurable-actions/configurable-actions.documentation.md diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.module.ts b/src/app/shared/modules/configurable-actions/configurable-actions.module.ts new file mode 100644 index 0000000000..6241ffe260 --- /dev/null +++ b/src/app/shared/modules/configurable-actions/configurable-actions.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; + +import { MatButtonModule } from "@angular/material/button"; +import { ConfigurableActionsComponent } from "./configurable-actions.component"; +import { ConfigurableActionComponent } from "./configurable-action.component"; +import { MatIconModule } from "@angular/material/icon"; + +@NgModule({ + imports: [CommonModule, MatIconModule, MatButtonModule], + declarations: [ConfigurableActionsComponent, ConfigurableActionComponent], + exports: [ConfigurableActionsComponent, ConfigurableActionComponent], +}) +export class ConfigurableActionsModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 1f6e95b886..bdfa71d65e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -22,6 +22,7 @@ import { DynamicMatTableModule } from "./modules/dynamic-material-table/table/dy import { TranslateModule } from "@ngx-translate/core"; import { FullTextSearchBarModule } from "./modules/full-text-search-bar/full-text-search-bar.module"; import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module"; +import { ConfigurableActionsModule } from "./modules/configurable-actions/configurable-actions.module"; @NgModule({ imports: [ BreadcrumbModule, @@ -43,6 +44,7 @@ import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module ScientificMetadataTreeModule, DynamicMatTableModule.forRoot({}), TranslateModule, + ConfigurableActionsModule, ], providers: [ ConfigService, @@ -69,6 +71,7 @@ import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module FiltersModule, DynamicMatTableModule, TranslateModule, + ConfigurableActionsModule, ], }) export class SharedScicatFrontendModule {} From b562e62c92cf7afbb3914c766816857fd77b266e Mon Sep 17 00:00:00 2001 From: martin-trajanovski Date: Tue, 26 Aug 2025 11:41:35 +0200 Subject: [PATCH 02/32] more general naming in the component inputs and add new action type xhr --- src/app/app-config.service.ts | 2 + .../datafiles/datafiles.component.html | 2 +- .../datasets/datafiles/datafiles.component.ts | 7 +- .../dataset-detail.component.html | 31 +++-- .../configurable-action.component.ts | 130 +++++++++++++++--- .../configurable-action.interfaces.ts | 6 +- .../configurable-actions.component.html | 2 +- .../configurable-actions.component.spec.ts | 10 +- .../configurable-actions.component.ts | 11 +- 9 files changed, 151 insertions(+), 50 deletions(-) diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index a31c5e63af..81ab692dec 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -73,6 +73,8 @@ export interface AppConfigInterface { datasetDetailsShowMissingProposalId: boolean; datafilesActionsEnabled: boolean; datafilesActions: any[]; + datasetSelectionActionsEnabled: boolean; + datasetSelectionActions: any[]; editDatasetEnabled: boolean; editDatasetSampleEnabled: boolean; editMetadataEnabled: boolean; diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index f315bc1738..0279293da6 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -34,7 +34,7 @@

No files associated to this dataset

diff --git a/src/app/datasets/datafiles/datafiles.component.ts b/src/app/datasets/datafiles/datafiles.component.ts index e9eb011724..0d287f2f1c 100644 --- a/src/app/datasets/datafiles/datafiles.component.ts +++ b/src/app/datasets/datafiles/datafiles.component.ts @@ -35,7 +35,7 @@ import { submitJobAction } from "state-management/actions/jobs.actions"; import { AppConfigService } from "app-config.service"; import { NgForm } from "@angular/forms"; import { DataFiles_File } from "./datafiles.interfaces"; -import { ActionDataset } from "shared/modules/configurable-actions/configurable-action.interfaces"; +import { ActionItem } from "shared/modules/configurable-actions/configurable-action.interfaces"; import { AuthService } from "shared/services/auth/auth.service"; @Component({ @@ -69,7 +69,7 @@ export class DatafilesComponent files: Array = []; datasetPid = ""; - actionDataset: ActionDataset; + actionDatasets: ActionItem[]; count = 0; pageSize = 25; @@ -96,7 +96,6 @@ export class DatafilesComponent icon: "save", sort: false, inList: true, - // pipe: FilePathTruncate, }, { name: "size", @@ -250,7 +249,7 @@ export class DatafilesComponent if (dataset) { this.sourceFolder = dataset.sourceFolder; this.datasetPid = dataset.pid; - this.actionDataset = dataset; + this.actionDatasets = [dataset]; } }), ); diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html index ac605dd2fc..53f90345c3 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html @@ -1,4 +1,8 @@ +
description - {{ "General Information" | translate }} + {{ + "General Information" | translate + }} @@ -167,7 +173,9 @@ person - {{ "Creator Information" | translate }} + {{ + "Creator Information" | translate + }} @@ -206,8 +214,10 @@ - folder - {{ "File Information" | translate }} + folder + {{ + "File Information" | translate + }} @@ -231,7 +241,9 @@ library_books - {{ "Related Documents" | translate }} + {{ + "Related Documents" | translate + }} @@ -345,10 +357,11 @@ - - science - {{ "Scientific Metadata" | translate }} + + science + {{ + "Scientific Metadata" | translate + }} { this.jwt = jwt.jwt; @@ -57,6 +59,18 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { .replaceAll("#Selected", String(this.numberOfFileSelected > 0)); } + private evaluate_hidden_condition(condition: string) { + return condition + .replaceAll( + "#isPublished", + String(this.actionItems[0].isPublished === true), + ) + .replaceAll( + "#!isPublished", + String(this.actionItems[0].isPublished === false), + ); + } + private prepare_disabled_condition() { if (this.actionConfig.enabled) { this.disabled_condition = @@ -72,8 +86,18 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { } } + private prepare_hidden_condition() { + if (this.actionConfig.hidden) { + return ( + "!(" + this.evaluate_hidden_condition(this.actionConfig.hidden) + ")" + ); + } else { + return "false"; + } + } + ngOnInit() { - this.use_mat_icon = this.actionConfig.mat_icon !== undefined; + this.use_mat_icon = !!this.actionConfig.mat_icon; this.use_icon = this.actionConfig.icon !== undefined; this.prepare_disabled_condition(); this.update_status(); @@ -108,6 +132,19 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { }); } + get visible() { + if (!this.actionConfig.hidden) { + return true; + } else { + const expr = this.prepare_hidden_condition(); + const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); + + return fn({ + isPublished: this.actionItems[0]?.isPublished === true, + }); + } + } + add_input(name: string, value: string) { const input = document.createElement("input"); input.type = "hidden"; @@ -121,6 +158,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { switch (action_type) { case "json-download": return this.type_json_download(); + case "xhr": + return this.type_xhr(); case "form": default: return this.type_form(); @@ -144,24 +183,21 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { this.form.appendChild(this.add_input("jwt", this.jwt)); - this.form.appendChild(this.add_input("dataset", this.actionDataset.pid)); - - this.form.appendChild( - this.add_input("directory", this.actionDataset.sourceFolder), - ); + this.actionItems.forEach((actionItem, index) => { + this.form.appendChild(this.add_input(`item[${index}]`, actionItem.pid)); + this.form.appendChild( + this.add_input(`directory[${index}]`, actionItem.sourceFolder), + ); + }); - let index = 0; - for (const item of this.files) { + this.files?.forEach((item, index) => { if ( this.actionConfig.files === "all" || (this.actionConfig.files === "selected" && item.selected) ) { - this.form.appendChild( - this.add_input("files[" + index + "]", item.path), - ); - index = index + 1; + this.form.appendChild(this.add_input(`files[${index}]`, item.path)); } - } + }); document.body.appendChild(this.form); this.form.submit(); @@ -175,13 +211,16 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { payload = this.actionConfig.payload .replace(/{{ auth_token }}/, `Bearer ${this.authService.getToken().id}`) .replace(/{{ jwt }}/, this.jwt) - .replace(/{{ datasetPid }}/, this.actionDataset.pid) - .replace(/{{ sourceFolder }}/, this.actionDataset.sourceFolder) + .replace(/{{ pid }}/, this.actionItems.map((i) => i.pid).join(",")) + .replace( + /{{ sourceFolder }}/, + this.actionItems.map((i) => i.sourceFolder).join(","), + ) .replace( /{{ filesPath }}/, JSON.stringify( this.files - .filter( + ?.filter( (item) => this.actionConfig.files === "all" || (this.actionConfig.files === "selected" && item.selected), @@ -193,8 +232,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { const data = { auth_token: `Bearer ${this.authService.getToken().id}`, jwt: this.jwt, - dataset: this.actionDataset.pid, - directory: this.actionDataset.sourceFolder, + items: this.actionItems.map((i) => i.pid), + directories: this.actionItems.map((i) => i.sourceFolder), files: this.files .filter( (item) => @@ -246,4 +285,51 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return true; } + + type_xhr() { + for (const element of this.actionItems) { + const payload = this.actionConfig.payload || ""; + const url = this.actionConfig.url.replace( + /{{id}}/, + encodeURIComponent(element.pid), + ); + + fetch(url, { + method: this.actionConfig.method || "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.authService.getToken().id}`, + }, + body: payload, + }) + .then((response) => { + if (!response.ok) { + return Promise.reject( + new Error(`HTTP Error code: ${response.status}`), + ); + } + + this.store.dispatch( + updatePropertyAction({ + pid: element.pid, + property: JSON.parse(this.actionConfig.payload), + }), + ); + + return response; + }) + .catch((error) => { + console.log("Error: ", error); + this.snackBar.open( + "There has been an error performing the action", + "Close", + { + duration: 2000, + }, + ); + }); + } + + return true; + } } diff --git a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts index 1218e829a1..681aa03e3e 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -15,9 +15,11 @@ export interface ActionConfig { disabled?: string; payload?: string; filename?: string; + hidden?: string; } -export interface ActionDataset { +export interface ActionItem { pid: string; - sourceFolder: string; + sourceFolder?: string; + isPublished?: boolean; } diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.html b/src/app/shared/modules/configurable-actions/configurable-actions.component.html index 9df216f7d9..1e90fe58e7 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.html +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.html @@ -3,7 +3,7 @@ diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts index 8b55e3ff71..a3e19c940e 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts @@ -129,10 +129,12 @@ describe("ConfigurableActionsComponent", () => { }, ]; component.actionsConfig = actionsConfig; - component.actionDataset = { - pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571", - sourceFolder: "/level_1/level_2/level3", - }; + component.actionItems = [ + { + pid: "57eb0ad6-48d4-11ef-814b-df221a8e3571", + sourceFolder: "/level_1/level_2/level3", + }, + ]; fixture.detectChanges(); }); diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index d593908091..273001b8bc 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from "@angular/core"; -import { ActionConfig, ActionDataset } from "./configurable-action.interfaces"; +import { ActionConfig, ActionItem } from "./configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AppConfigService } from "app-config.service"; @@ -13,16 +13,13 @@ export class ConfigurableActionsComponent { private _sortedActionsConfig: ActionConfig[]; @Input({ required: true }) actionsConfig: ActionConfig[]; - @Input({ required: true }) actionDataset: ActionDataset; - @Input({ required: true }) files?: DataFiles_File[]; + @Input({ required: true }) actionItems: ActionItem[]; + @Input() files?: DataFiles_File[]; constructor(public appConfigService: AppConfigService) {} get visible(): boolean { - return ( - this.appConfigService.getConfig().datafilesActionsEnabled && - this.files?.length > 0 - ); + return this.appConfigService.getConfig().datafilesActionsEnabled; } get maxFileSize(): number { From 6f163502c18bc91ee714b0750202649c0b1f16e1 Mon Sep 17 00:00:00 2001 From: martin-trajanovski Date: Tue, 26 Aug 2025 15:18:14 +0200 Subject: [PATCH 03/32] add the configurable buttons in the dataset selection view --- CI/e2e/frontend.config.e2e.json | 70 ++++++++++++++++++- src/app/app-config.service.ts | 2 + .../batch-view/batch-view.component.html | 44 ++---------- .../batch-view/batch-view.component.ts | 32 ++++++++- .../datafiles/datafiles.component.html | 1 + .../datafiles/datafiles.component.spec.ts | 4 +- .../dataset-detail.component.html | 26 +------ .../dataset-detail.component.scss | 13 ++-- src/app/shared/MockStubs.ts | 6 +- .../configurable-action.component.ts | 12 ++++ .../configurable-actions.component.scss | 3 +- .../configurable-actions.component.ts | 5 +- 12 files changed, 136 insertions(+), 82 deletions(-) diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 7bdad6c8de..41c327604a 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -24,7 +24,6 @@ "ingestManual": null, "jobsEnabled": true, "jsonMetadataEnabled": true, - "jupyterHubUrl": "", "landingPage": "doi.ess.eu/detail/", "lbBaseURL": "http://localhost:3000", "logbookEnabled": true, @@ -106,6 +105,75 @@ "authorization": ["#datasetAccess", "#datasetPublic"] } ], + "datasetDetailsActionsEnabled": true, + "datasetDetailsActions": [ + { + "id": "01", + "order": 1, + "type": "xhr", + "method": "PATCH", + "description": "Publish dataset", + "label": "Publish", + "hidden": "#isPublished", + "mat_icon": "", + "url": "/api/v3/datasets/{{id}}", + "payload": "{\"isPublished\": true}" + }, + { + "id": "02", + "order": 2, + "type": "xhr", + "method": "PATCH", + "description": "Unpublish published dataset", + "label": "Unpublish", + "hidden": "#!isPublished", + "mat_icon": "", + "url": "/api/v3/datasets/{{id}}", + "payload": "{\"isPublished\": false}" + }, + { + "id": "03", + "order": 3, + "type": "form", + "method": "GET", + "description": "Jupyter hub", + "label": "Jupyter hub", + "mat_icon": "", + "url": "https://jupyterhub.esss.lu.se/", + "target": "_blank" + } + ], + "datasetSelectionActionsEnabled": true, + "datasetSelectionActions": [ + { + "id": "01", + "order": 1, + "type": "link", + "description": "Publish datasets", + "label": "Publish", + "mat_icon": "school", + "url": "/datasets/batch/publish" + }, + { + "id": "02", + "order": 2, + "type": "link", + "description": "Share datasets", + "label": "Share", + "mat_icon": "share", + "url": "/datasets/batch?share=true" + }, + { + "id": "03", + "order": 3, + "type": "link", + "hidden": "!archiveWorkflowEnabled", + "description": "Retrieve datasets", + "label": "Retrieve", + "mat_icon": "cloud_download", + "url": "/datasets/batch?retrieve=true" + } + ], "labelMaps": { "filters": { "LocationFilter": "Location", diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 81ab692dec..0f5b4deb1e 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -73,6 +73,8 @@ export interface AppConfigInterface { datasetDetailsShowMissingProposalId: boolean; datafilesActionsEnabled: boolean; datafilesActions: any[]; + datasetDetailsActionsEnabled: boolean; + datasetDetailsActions: any[]; datasetSelectionActionsEnabled: boolean; datasetSelectionActions: any[]; editDatasetEnabled: boolean; diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index cce14bc9ff..0634c9d9a1 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -1,47 +1,15 @@
-
+
+ - - - - - - - -
{ + if (queryParams["share"] === "true") { + this.onShare(); + } + if (queryParams["retrieve"] === "true") { + this.onRetrieve(); + } + }), + ); } ngOnDestroy() { diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index 0279293da6..d355e9d720 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -36,6 +36,7 @@

No files associated to this dataset

[actionsConfig]="appConfig.datafilesActions" [actionItems]="actionDatasets" [files]="files" + [visible]="appConfig.datafilesActionsEnabled" >
diff --git a/src/app/datasets/datafiles/datafiles.component.spec.ts b/src/app/datasets/datafiles/datafiles.component.spec.ts index 3b30b7a4e3..c1c843e6d1 100644 --- a/src/app/datasets/datafiles/datafiles.component.spec.ts +++ b/src/app/datasets/datafiles/datafiles.component.spec.ts @@ -18,7 +18,7 @@ import { MatIconModule } from "@angular/material/icon"; import { MatButtonModule } from "@angular/material/button"; import { AppConfigService } from "app-config.service"; import { MatDialogModule, MatDialogRef } from "@angular/material/dialog"; -import { DatafilesActionsComponent } from "shared/modules/configurable-actions/configurable-actions.component"; +import { ConfigurableActionsComponent } from "shared/modules/configurable-actions/configurable-actions.component"; import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; import { AuthService } from "shared/services/auth/auth.service"; import { FileSizePipe } from "shared/pipes/filesize.pipe"; @@ -100,7 +100,7 @@ describe("DatafilesComponent", () => { { provide: AppConfigService, useValue: { getConfig } }, { provide: AuthService, useValue: MockAuthService }, { - provide: DatafilesActionsComponent, + provide: ConfigurableActionsComponent, useClass: MockDatafilesActionsComponent, }, { provide: FileSizePipe }, diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html index b9433d5eaf..b434a7c0e4 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html @@ -1,31 +1,9 @@ -
- -
- - Public - -
-
diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.scss b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.scss index 50dd208df2..07cf2cb468 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.scss +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.scss @@ -1,7 +1,11 @@ mat-card { margin: 1em; - .scientific-header, .related-header, .creator-header, .file-header, .general-header { + .scientific-header, + .related-header, + .creator-header, + .file-header, + .general-header { display: flex; align-items: center; padding: 1em; @@ -64,13 +68,6 @@ mat-card { } } } -.jupyter-button { - margin: 1em 0 0 1em; -} -.public-toggle { - margin: 1em 1em 0 0; - float: right; -} .attachment-card { display: flex; align-items: center; diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index 182e548b72..b0062b5f7b 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -6,11 +6,10 @@ import { AppConfig } from "app-config.module"; import { SciCatDataSource } from "./services/scicat.datasource"; import { ActionConfig, - ActionDataset, + ActionItem, } from "shared/modules/configurable-actions/configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { - Attachment, Instrument, OutputJobV3Dto, OutputDatasetObsoleteDto, @@ -262,8 +261,9 @@ export class MockScicatDataSource extends SciCatDataSource { export class MockDatafilesActionsComponent { actionsConfig: ActionConfig[]; - dataset: ActionDataset; + actionItems: ActionItem[]; files: DataFiles_File[]; + visible: boolean; } export class MockHtmlElement { diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index 2f16b74942..70c898edd2 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -14,6 +14,8 @@ import { v4 } from "uuid"; import { MatSnackBar } from "@angular/material/snack-bar"; import { Store } from "@ngrx/store"; import { updatePropertyAction } from "state-management/actions/datasets.actions"; +import { Router } from "@angular/router"; +import { AppConfigService } from "app-config.service"; @Component({ selector: "configurable-action", @@ -39,8 +41,10 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { constructor( private usersService: UsersService, private authService: AuthService, + private configService: AppConfigService, private snackBar: MatSnackBar, private store: Store, + private router: Router, ) { this.usersService.usersControllerGetUserJWTV3().subscribe((jwt) => { this.jwt = jwt.jwt; @@ -141,6 +145,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return fn({ isPublished: this.actionItems[0]?.isPublished === true, + archiveWorkflowEnabled: + this.configService.getConfig().archiveWorkflowEnabled, }); } } @@ -160,6 +166,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return this.type_json_download(); case "xhr": return this.type_xhr(); + case "link": + return this.type_link(); case "form": default: return this.type_form(); @@ -332,4 +340,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return true; } + + type_link() { + this.router.navigateByUrl(this.actionConfig.url); + } } diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.scss b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss index f5b28b0969..5848d29eee 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.scss +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss @@ -1,3 +1,4 @@ .configurable-actions { float: right; -} \ No newline at end of file + margin: 1em; +} diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index 273001b8bc..57ce178126 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -15,13 +15,10 @@ export class ConfigurableActionsComponent { @Input({ required: true }) actionsConfig: ActionConfig[]; @Input({ required: true }) actionItems: ActionItem[]; @Input() files?: DataFiles_File[]; + @Input() visible = true; constructor(public appConfigService: AppConfigService) {} - get visible(): boolean { - return this.appConfigService.getConfig().datafilesActionsEnabled; - } - get maxFileSize(): number { return this.appConfigService.getConfig().maxDirectDownloadSize || 0; } From ffc3bcb260c1fb3e0a4ba3c0b5d50d5345d7e95e Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Thu, 11 Sep 2025 17:34:34 +0200 Subject: [PATCH 04/32] WIP of the day --- .../configurable-action.component.ts | 100 +++++++++++++++++- .../configurable-action.interfaces.ts | 21 +++- 2 files changed, 113 insertions(+), 8 deletions(-) diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index 70c898edd2..212c5b560b 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -7,7 +7,7 @@ import { } from "@angular/core"; import { UsersService } from "@scicatproject/scicat-sdk-ts-angular"; -import { ActionConfig, ActionItem } from "./configurable-action.interfaces"; +import { ActionConfig, ActionItems } from "./configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AuthService } from "shared/services/auth/auth.service"; import { v4 } from "uuid"; @@ -25,8 +25,7 @@ import { AppConfigService } from "app-config.service"; }) export class ConfigurableActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionConfig: ActionConfig; - @Input({ required: true }) actionItems: ActionItem[]; - @Input() files?: DataFiles_File[]; + @Input({ required: true }) actionItems: ActionItems; @Input({ required: true }) maxFileSize: number; jwt = ""; @@ -344,4 +343,99 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { type_link() { this.router.navigateByUrl(this.actionConfig.url); } + +// type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[]; + +// function selectFromJsonWithFilter(jsonObject: JSONValue, selector: string): string[] { +// const results: string[] = []; + +// // Parse the selector into main selection and optional filter +// const [mainSelector, filterSelector] = selector.split('|').map(part => part.trim()); +// const mainKeys = mainSelector +// .replace(/^\./, "") // Remove leading dot +// .split(".") // Split into keys +// .map(key => key.trim()); + +// let filterKeys: string[] | null = null; +// if (filterSelector) { +// if (!filterSelector.startsWith("select ")) { +// throw new Error("Invalid syntax: Filter part must start with 'select'."); +// } +// filterKeys = filterSelector +// .slice(7) // Remove "select " prefix +// .replace(/^\./, "") // Remove leading dot +// .split(".") // Split into keys +// .map(key => key.trim()); +// } + +// const traverse = (obj: JSONValue, keys: string[], filterKeys?: string[], filterObj?: JSONValue) => { +// if (keys.length === 0) { +// // If no more main keys to process, and if optional filter keys exist, evaluate the filter +// if (filterKeys && filterObj !== undefined) { +// const filterPasses = evaluateFilter(filterObj, filterKeys); +// if (!filterPasses) return; +// } + +// // Add the current value if it's a string +// if (typeof obj === "string") { +// results.push(obj); +// } +// return; +// } + +// const key = keys[0]; + +// if (Array.isArray(obj)) { +// // If the current object is an array, process each item +// if (key.startsWith("[") && key.endsWith("]")) { +// const index = parseInt(key.slice(1, -1)); +// if (!isNaN(index) && obj[index] !== undefined) { +// traverse(obj[index], keys.slice(1), filterKeys, obj[index]); +// } +// } else { +// obj.forEach((item) => traverse(item, keys, filterKeys, item)); +// } +// } else if (typeof obj === "object" && obj !== null) { +// traverse(obj[key], keys.slice(1), filterKeys, obj); +// } +// }; + +// const evaluateFilter = (obj: JSONValue, filterKeys: string[]): boolean => { +// let current = obj; +// for (const key of filterKeys) { +// if (Array.isArray(current)) { +// // Return false for arrays within a filter (unsupported case) +// return false; +// } else if (typeof current === "object" && current !== null && key in current) { +// current = current[key]; +// } else { +// return false; // If the key does not exist or is invalid +// } +// } + +// return current === true; // Assume filter checks for a `true` value +// }; + +// // Begin traversing the JSON object +// traverse(jsonObject, mainKeys, filterKeys || undefined, jsonObject); + +// return results; +// } + +// // Example usage +// const jsonExample = { +// datasets: [ +// { files: { selected: true, path: "/path/to/file1" } }, +// { files: { selected: false, path: "/path/to/file2" } }, +// { files: { selected: true, path: "/path/to/file3" } } +// ] +// }; + +// const selector = ".datasets[].files.path | select .datasets[].files.selected"; +// const selectedPaths = selectFromJsonWithFilter(jsonExample, selector); + +// console.log(selectedPaths); // Output: ["/path/to/file1", "/path/to/file3"] + + + } diff --git a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts index 681aa03e3e..d54c09ae22 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -1,3 +1,5 @@ +import { DatasetClass } from "@scicatproject/scicat-sdk-ts-angular"; + export interface ActionConfig { id: string; description?: string; @@ -18,8 +20,17 @@ export interface ActionConfig { hidden?: string; } -export interface ActionItem { - pid: string; - sourceFolder?: string; - isPublished?: boolean; -} +// export interface ActionItem { +// pid: string; +// sourceFolder?: string; +// isPublished?: boolean; +// } + +export interface ActionItems { + datasets: { + pid: string; + sourceFolder?: string; + isPublished?: boolean; + files?: DataFiles_File[]; + }[], +} \ No newline at end of file From b31492a7058bc6159d4910d0f7d3993fe2ce0fd9 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Mon, 15 Sep 2025 16:46:53 +0200 Subject: [PATCH 05/32] work of the day --- .../configurable-action.component.ts | 283 ++++++++++-------- .../configurable-action.interfaces.ts | 4 +- src/assets/config.json | 57 +++- 3 files changed, 217 insertions(+), 127 deletions(-) diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index 212c5b560b..bf2360bafb 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -17,6 +17,137 @@ import { updatePropertyAction } from "state-management/actions/datasets.actions" import { Router } from "@angular/router"; import { AppConfigService } from "app-config.service"; +type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[]; + +function processSelector( + jsonObject: JSONValue, + selector: string +): string | string[] | number | number[] { + const results: string[] = []; + const numericResults: number[] = []; + let sum = 0; + + // Support for wrapping selector in "[ ... ] | operation" + const [coreSelectorPart, operationPart] = selector.includes("|") + ? selector.split("|").map(part => part.trim()) + : [selector.trim(), null]; + + // Parse the core selector and optional filter + const coreSelector = coreSelectorPart.replace(/^\[|\]$/g, ""); // Remove enclosing brackets, if present + const [mainSelector, filterSelector] = coreSelector.split('|').map(part => part.trim()); + const mainKeys = mainSelector + .replace(/^\./, "") // Remove leading dot + .split(".") // Split into keys + .map(key => key.trim()); + + let filterKeys: string[] | null = null; + if (filterSelector) { + if (!filterSelector.startsWith("select ")) { + throw new Error("Invalid syntax: Filter part must start with 'select'."); + } + filterKeys = filterSelector + .slice(7) // Remove "select " prefix + .replace(/^\./, "") // Remove leading dot + .split(".") // Split into keys + .map(key => key.trim()); + } + + const traverse = (obj: JSONValue, keys: string[], filterKeys?: string[], filterObj?: JSONValue) => { + if (keys.length === 0) { + // If no more main keys to process, evaluate the filter (if provided) + if (filterKeys && filterObj !== undefined) { + const filterPasses = evaluateFilter(filterObj, filterKeys); + if (!filterPasses) return; + } + + // Add the current value to the appropriate collection + if (typeof obj === "string") { + results.push(obj); + } else if (typeof obj === "number") { + numericResults.push(obj); + sum += obj; + } + + return; + } + + const key = keys[0]; + + if (Array.isArray(obj)) { + // If the current object is an array, process each item + if (key.startsWith("[") && key.endsWith("]")) { + const index = parseInt(key.slice(1, -1)); + if (!isNaN(index) && obj[index] !== undefined) { + traverse(obj[index], keys.slice(1), filterKeys, obj[index]); + } + } else { + obj.forEach((item) => traverse(item, keys, filterKeys, item)); + } + } else if (typeof obj === "object" && obj !== null) { + traverse(obj[key], keys.slice(1), filterKeys, obj); + } + }; + + const evaluateFilter = (obj: JSONValue, filterKeys: string[]): boolean => { + let current = obj; + for (const key of filterKeys) { + if (Array.isArray(current)) { + // Return false for arrays within a filter (unsupported case) + return false; + } else if (typeof current === "object" && current !== null && key in current) { + current = current[key]; + } else { + return false; + } + } + return current === true; // Assume filter checks for a `true` value + }; + + // Begin traversing the JSON object + traverse(jsonObject, mainKeys, filterKeys || undefined, jsonObject); + + // Handle post-processing commands like `count` or others + let count = 0; + if (operationPart) { + switch (operationPart) { + case "count": + return (results.length > 0 ? results.length : numericResults.length); + break; + case "sum": + return numericResults.reduce((total, value) => total + value, 0); // Defensive to ensure correct computation + break; + default: + throw new Error(`Unsupported operation: ${operationPart}`); + } + } + + return results; +} + +// Example usage +const jsonExample = { + datasets: [ + { files: { selected: true, path: "/path/to/file1", size: 100 } }, + { files: { selected: false, path: "/path/to/file2", size: 200 } }, + { files: { selected: true, path: "/path/to/file3", size: 300 } } + ] +}; + +// // Example 1: Count all selected items +// const selectorCount = "[.datasets[].files.size | select .datasets[].files.selected] | count"; +// const countResult = processSelector(jsonExample, selectorCount); +// console.log(countResult); // Output: 2 + +// // Example 2: Sum all selected sizes +// const selectorSum = "[.datasets[].files.size | select .datasets[].files.selected] | sum"; +// const sumResult = processSelector(jsonExample, selectorSum); +// console.log(sumResult); // Output: 400 + +// // Example 3: Retrieve all paths +// const selectorPaths = "[.datasets[].files.path | select .datasets[].files.selected] | count"; +// const pathsResult = processSelector(jsonExample, selectorPaths); +// console.log(pathsResult); // Output: ["/path/to/file1", "/path/to/file3"] + @Component({ selector: "configurable-action", templateUrl: "./configurable-action.component.html", @@ -26,14 +157,15 @@ import { AppConfigService } from "app-config.service"; export class ConfigurableActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionConfig: ActionConfig; @Input({ required: true }) actionItems: ActionItems; - @Input({ required: true }) maxFileSize: number; + @Input({ required: true }) maxDownloadableSize: number; jwt = ""; use_mat_icon = false; use_icon = false; disabled_condition = "false"; - selectedTotalFileSize = 0; - numberOfFileSelected = 0; + #selectedTotalFileSize = 0; + #numberOfFileSelected = 0; + variables: Record = {}; form: HTMLFormElement = null; @@ -50,18 +182,6 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { }); } - private evaluate_disabled_condition(condition: string) { - return condition - .replaceAll( - "#SizeLimit", - String( - this.maxFileSize > 0 && - this.selectedTotalFileSize <= this.maxFileSize, - ), - ) - .replaceAll("#Selected", String(this.numberOfFileSelected > 0)); - } - private evaluate_hidden_condition(condition: string) { return condition .replaceAll( @@ -74,14 +194,30 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { ); } + private prepare_action_condition(condition: string) { + // Define replacements for specific functions and variables + return condition + // Handle #Length({{ files }}) + .replace( + /#Length\(\{\{\s(\w+)\s\}\}\)/g, + (_, variableName) => `variables.${variableName}.length`) + // Handle #MaxDownloadableSize({{ totalSize }}) + .replace( + /#MaxDownloadableSize\(\{\{\s(\w+)\s\}\}\)/g, + (_, variableName) => `variables.${variableName} <= maxDownloadableSize`) + .replace( + /\{\{\s(\w+)\s\}\}/g, + (_, variableName) => `variables.${variableName}`); + } + private prepare_disabled_condition() { if (this.actionConfig.enabled) { this.disabled_condition = "!(" + - this.evaluate_disabled_condition(this.actionConfig.enabled) + + this.prepare_action_condition(this.actionConfig.enabled) + ")"; } else if (this.actionConfig.disabled) { - this.disabled_condition = this.evaluate_disabled_condition( + this.disabled_condition = this.prepare_action_condition( this.actionConfig.disabled, ); } else { @@ -107,31 +243,28 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { } ngOnChanges(changes: SimpleChanges) { - if (changes["files"]) { + if (changes["actionItems"]) { this.update_status(); } } update_status() { - this.selectedTotalFileSize = this.files - ?.filter((item) => item.selected || this.actionConfig.files === "all") - .reduce((sum, item) => sum + item.size, 0); - this.numberOfFileSelected = this.files?.filter( - (item) => item.selected, - ).length; + Object.entries(this.actionConfig.variables).forEach(([key,selector]) => { + this.variables[key] = processSelector( + this.actionItems as unknown as JSONValue, + selector) + }) } get disabled() { this.update_status(); - this.prepare_disabled_condition(); const expr = this.disabled_condition; const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); return fn({ - maxFileSize: this.maxFileSize, - selectedTotalFileSize: this.selectedTotalFileSize, - numberOfFileSelected: this.numberOfFileSelected, + variables: this.variables, + maxDownloadableSize: this.maxDownloadableSize, }); } @@ -344,98 +477,4 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { this.router.navigateByUrl(this.actionConfig.url); } -// type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[]; - -// function selectFromJsonWithFilter(jsonObject: JSONValue, selector: string): string[] { -// const results: string[] = []; - -// // Parse the selector into main selection and optional filter -// const [mainSelector, filterSelector] = selector.split('|').map(part => part.trim()); -// const mainKeys = mainSelector -// .replace(/^\./, "") // Remove leading dot -// .split(".") // Split into keys -// .map(key => key.trim()); - -// let filterKeys: string[] | null = null; -// if (filterSelector) { -// if (!filterSelector.startsWith("select ")) { -// throw new Error("Invalid syntax: Filter part must start with 'select'."); -// } -// filterKeys = filterSelector -// .slice(7) // Remove "select " prefix -// .replace(/^\./, "") // Remove leading dot -// .split(".") // Split into keys -// .map(key => key.trim()); -// } - -// const traverse = (obj: JSONValue, keys: string[], filterKeys?: string[], filterObj?: JSONValue) => { -// if (keys.length === 0) { -// // If no more main keys to process, and if optional filter keys exist, evaluate the filter -// if (filterKeys && filterObj !== undefined) { -// const filterPasses = evaluateFilter(filterObj, filterKeys); -// if (!filterPasses) return; -// } - -// // Add the current value if it's a string -// if (typeof obj === "string") { -// results.push(obj); -// } -// return; -// } - -// const key = keys[0]; - -// if (Array.isArray(obj)) { -// // If the current object is an array, process each item -// if (key.startsWith("[") && key.endsWith("]")) { -// const index = parseInt(key.slice(1, -1)); -// if (!isNaN(index) && obj[index] !== undefined) { -// traverse(obj[index], keys.slice(1), filterKeys, obj[index]); -// } -// } else { -// obj.forEach((item) => traverse(item, keys, filterKeys, item)); -// } -// } else if (typeof obj === "object" && obj !== null) { -// traverse(obj[key], keys.slice(1), filterKeys, obj); -// } -// }; - -// const evaluateFilter = (obj: JSONValue, filterKeys: string[]): boolean => { -// let current = obj; -// for (const key of filterKeys) { -// if (Array.isArray(current)) { -// // Return false for arrays within a filter (unsupported case) -// return false; -// } else if (typeof current === "object" && current !== null && key in current) { -// current = current[key]; -// } else { -// return false; // If the key does not exist or is invalid -// } -// } - -// return current === true; // Assume filter checks for a `true` value -// }; - -// // Begin traversing the JSON object -// traverse(jsonObject, mainKeys, filterKeys || undefined, jsonObject); - -// return results; -// } - -// // Example usage -// const jsonExample = { -// datasets: [ -// { files: { selected: true, path: "/path/to/file1" } }, -// { files: { selected: false, path: "/path/to/file2" } }, -// { files: { selected: true, path: "/path/to/file3" } } -// ] -// }; - -// const selector = ".datasets[].files.path | select .datasets[].files.selected"; -// const selectedPaths = selectFromJsonWithFilter(jsonExample, selector); - -// console.log(selectedPaths); // Output: ["/path/to/file1", "/path/to/file3"] - - - } diff --git a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts index d54c09ae22..7ab2b1d9ba 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -1,4 +1,4 @@ -import { DatasetClass } from "@scicatproject/scicat-sdk-ts-angular"; +import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; export interface ActionConfig { id: string; @@ -18,6 +18,8 @@ export interface ActionConfig { payload?: string; filename?: string; hidden?: string; + variables?: Record; + inputs?: Record } // export interface ActionItem { diff --git a/src/assets/config.json b/src/assets/config.json index 4395f9b02d..fc7a1e49e4 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -71,7 +71,18 @@ "type": "form", "url": "https://zip.scicatproject.org/download/all", "target": "_blank", - "enabled": "#SizeLimit", + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path", + "totalSize": "[.datasets[0].files.size] | add", + "folder": ".datasets[0].sourceFolder" + }, + "enabled": "#MaxDownloadableSize(@totalSize)", + "inputs" : { + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, "authorization": ["#datasetAccess", "#datasetPublic"] }, { @@ -84,7 +95,19 @@ "type": "form", "url": "https://zip.scicatproject.org/download/selected", "target": "_blank", - "enabled": "#Selected && #SizeLimit", + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path | select(.datasets[0].files.selected)", + "selected": "[.datasets[0].files.path | select(.datasets[0].files.selected)] | count", + "totalSize": "[.datasets[0].files.size | select(.datasets[0].files.selected)] | sum", + "folder": ".datasets[0].sourceFolder" + }, + "inputs" : { + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, + "enabled": "#Length(@files) && #MaxDownloadableSize(@totalSize)", "authorization": ["#datasetAccess", "#datasetPublic"] }, { @@ -101,7 +124,7 @@ }, { "id": "0cd5b592-0b1a-11f0-a42c-23e177127ee7", - "description": "This action let users download jupyter notebook properly populated with dataset pid and selected files using an instance of sciwyrm", + "description": "This action let users download jupyter notebook properly populated with dataset pid and all files using an instance of sciwyrm", "order": 3, "label": "Notebook All (Download JSON)", "files": "all", @@ -110,9 +133,35 @@ "url": "https://www.sciwyrm.info/notebook", "target": "_blank", "authorization": ["#datasetAccess", "#datasetPublic"], - "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ datasetPid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ filesPath }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", + "variables" : { + "datasetPid": ".datasets[0].pid", + "files": ".datasets[0].files.path", + "folder": ".datasets[0].sourceFolder" + }, + "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ datasetPid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ files }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", "filename": "{{ uuid }}.ipynb" }, + { + "id": "224d8cb0-9233-11f0-991a-bfa2461d77ff", + "description": "This action let users download jupyter notebook properly populated with dataset pid and selected files using an instance of sciwyrm", + "order": 4, + "label": "Notebook Selected (Download JSON)", + "files": "all", + "type": "json-download", + "icon": "/assets/icons/jupyter_logo.png", + "url": "https://www.sciwyrm.info/notebook", + "target": "_blank", + "enabled": "@selected > 0", + "authorization": ["#datasetAccess", "#datasetPublic"], + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path | select(.datasets[0].files.selected)", + "selected": "[.datasets[0].files.path | select(.datasets[0].files.selected)] | count", + "folder": ".datasets[0].sourceFolder" + }, + "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ pid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ files }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", + "filename": "{{ uuid }}.ipynb" + }, { "id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", "order": 1, From 575ecac3d3e1c0ebfb36d63d4201d4295e32a937 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Thu, 9 Oct 2025 19:32:21 +0200 Subject: [PATCH 06/32] first draft of configurable actions --- src/app/app-config.service.ts | 2 + .../datafiles/datafiles.component.html | 3 +- .../datasets/datafiles/datafiles.component.ts | 9 +- .../dataset-detail-dynamic.component.html | 5 + .../configurable-action.component.ts | 279 ++++++++++-------- .../configurable-action.interfaces.ts | 17 +- .../configurable-actions.component.html | 1 - .../configurable-actions.component.ts | 4 +- src/assets/config.json | 126 ++++++-- 9 files changed, 278 insertions(+), 168 deletions(-) diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 0f5b4deb1e..972b9dacd1 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -71,6 +71,8 @@ export interface AppConfigInterface { datasetJsonScientificMetadata: boolean; datasetReduceEnabled: boolean; datasetDetailsShowMissingProposalId: boolean; + datasetActionsEnabled: boolean; + datasetActions: any[]; datafilesActionsEnabled: boolean; datafilesActions: any[]; datasetDetailsActionsEnabled: boolean; diff --git a/src/app/datasets/datafiles/datafiles.component.html b/src/app/datasets/datafiles/datafiles.component.html index d355e9d720..397e373025 100644 --- a/src/app/datasets/datafiles/datafiles.component.html +++ b/src/app/datasets/datafiles/datafiles.component.html @@ -34,8 +34,7 @@

No files associated to this dataset

diff --git a/src/app/datasets/datafiles/datafiles.component.ts b/src/app/datasets/datafiles/datafiles.component.ts index 0d287f2f1c..761754b253 100644 --- a/src/app/datasets/datafiles/datafiles.component.ts +++ b/src/app/datasets/datafiles/datafiles.component.ts @@ -35,7 +35,7 @@ import { submitJobAction } from "state-management/actions/jobs.actions"; import { AppConfigService } from "app-config.service"; import { NgForm } from "@angular/forms"; import { DataFiles_File } from "./datafiles.interfaces"; -import { ActionItem } from "shared/modules/configurable-actions/configurable-action.interfaces"; +import { ActionItemDataset, ActionItems } from "shared/modules/configurable-actions/configurable-action.interfaces"; import { AuthService } from "shared/services/auth/auth.service"; @Component({ @@ -69,7 +69,7 @@ export class DatafilesComponent files: Array = []; datasetPid = ""; - actionDatasets: ActionItem[]; + actionItems: ActionItems; count = 0; pageSize = 25; @@ -247,9 +247,7 @@ export class DatafilesComponent this.subscriptions.push( this.dataset$.subscribe((dataset) => { if (dataset) { - this.sourceFolder = dataset.sourceFolder; - this.datasetPid = dataset.pid; - this.actionDatasets = [dataset]; + this.actionItems.datasets = [dataset]; } }), ); @@ -268,6 +266,7 @@ export class DatafilesComponent this.tableData = files.slice(0, this.pageSize); this.files = files; this.tooLargeFile = this.hasTooLargeFiles(this.files); + this.actionItems.datasets[0].files = files; } }), ); 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 0910b29dd4..4efb98147f 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 @@ -13,6 +13,11 @@ Jupyter Hub
+
diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index bf2360bafb..7f0c60118c 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -17,7 +17,13 @@ import { updatePropertyAction } from "state-management/actions/datasets.actions" import { Router } from "@angular/router"; import { AppConfigService } from "app-config.service"; -type JSONValue = string | number | boolean | null | { [key: string]: JSONValue } | JSONValue[]; +type JSONValue = + | string + | number + | boolean + | null + | { [key: string]: JSONValue } + | JSONValue[]; function processSelector( jsonObject: JSONValue, @@ -38,7 +44,7 @@ function processSelector( const mainKeys = mainSelector .replace(/^\./, "") // Remove leading dot .split(".") // Split into keys - .map(key => key.trim()); + .map((key) => key.trim()); let filterKeys: string[] | null = null; if (filterSelector) { @@ -52,7 +58,12 @@ function processSelector( .map(key => key.trim()); } - const traverse = (obj: JSONValue, keys: string[], filterKeys?: string[], filterObj?: JSONValue) => { + const traverse = ( + obj: JSONValue, + keys: string[], + filterKeys?: string[], + filterObj?: JSONValue + ) => { if (keys.length === 0) { // If no more main keys to process, evaluate the filter (if provided) if (filterKeys && filterObj !== undefined) { @@ -157,7 +168,7 @@ const jsonExample = { export class ConfigurableActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionConfig: ActionConfig; @Input({ required: true }) actionItems: ActionItems; - @Input({ required: true }) maxDownloadableSize: number; + @Input() files?: DataFiles_File[]; jwt = ""; use_mat_icon = false; @@ -264,7 +275,7 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return fn({ variables: this.variables, - maxDownloadableSize: this.maxDownloadableSize, + maxDownloadableSize: this.configService.getConfig().maxDirectDownloadSize, }); } @@ -276,9 +287,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); return fn({ - isPublished: this.actionItems[0]?.isPublished === true, - archiveWorkflowEnabled: - this.configService.getConfig().archiveWorkflowEnabled, + variables: this.variables, + maxDownloadableSize: this.configService.getConfig().maxDirectDownloadSize, }); } } @@ -294,8 +304,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { perform_action() { const action_type = this.actionConfig.type || "form"; switch (action_type) { - case "json-download": - return this.type_json_download(); + case "json-to-download": + return this.type_json_to_download(); case "xhr": return this.type_xhr(); case "link": @@ -306,6 +316,21 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { } } + get_value_from_definition(definition: string) { + if (definition == "#token" || definition == "#tokenSimple") { + return this.authService.getToken().id; + } else if (definition == "#tokenBearer") { + return `Bearer ${this.authService.getToken().id}`; + } else if (definition == "#jwt") { + return this.jwt; + } else if (definition == "#uuid") { + return v4(); + } else if (definition.startsWith("@")) { + return this.variables[definition.slice(1)]; + } + return definition; + } + type_form() { if (this.form !== null) { document.body.removeChild(this.form); @@ -317,25 +342,21 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { this.form.action = this.actionConfig.url; this.form.style.display = "none"; - this.form.appendChild( - this.add_input("auth_token", `Bearer ${this.authService.getToken().id}`), - ); + // use the configuration under inputs to create the form + Object.entries(this.actionConfig.inputs).forEach(([input, definition]) => { - this.form.appendChild(this.add_input("jwt", this.jwt)); - - this.actionItems.forEach((actionItem, index) => { - this.form.appendChild(this.add_input(`item[${index}]`, actionItem.pid)); - this.form.appendChild( - this.add_input(`directory[${index}]`, actionItem.sourceFolder), - ); - }); + const value = this.get_value_from_definition(definition); - this.files?.forEach((item, index) => { - if ( - this.actionConfig.files === "all" || - (this.actionConfig.files === "selected" && item.selected) - ) { - this.form.appendChild(this.add_input(`files[${index}]`, item.path)); + if (input.endsWith("[]")) { + const itemInput = input.slice(-2); + const iteratable = Array.isArray(value)?value:[value]; + iteratable.forEach((itemValue, itemIndex) => { + this.form.appendChild( + this.add_input(`${itemInput}[${itemIndex}]`, value) + ); + }) + } else { + this.form.appendChild(this.add_input(input, value)); } }); @@ -345,131 +366,127 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return true; } - type_json_download() { + get_payload() { let payload = ""; - if (this.actionConfig.payload) { + if (this.actionConfig.payload == "#dump" ) { + payload = JSON.stringify(this.variables); + } else if (this.actionConfig.payload != "#empty" && this.actionConfig.payload) { payload = this.actionConfig.payload - .replace(/{{ auth_token }}/, `Bearer ${this.authService.getToken().id}`) - .replace(/{{ jwt }}/, this.jwt) - .replace(/{{ pid }}/, this.actionItems.map((i) => i.pid).join(",")) - .replace( - /{{ sourceFolder }}/, - this.actionItems.map((i) => i.sourceFolder).join(","), - ) - .replace( - /{{ filesPath }}/, - JSON.stringify( - this.files - ?.filter( - (item) => - this.actionConfig.files === "all" || - (this.actionConfig.files === "selected" && item.selected), - ) - .map((item) => item.path), - ), - ); - } else { - const data = { - auth_token: `Bearer ${this.authService.getToken().id}`, - jwt: this.jwt, - items: this.actionItems.map((i) => i.pid), - directories: this.actionItems.map((i) => i.sourceFolder), - files: this.files - .filter( - (item) => - this.actionConfig.files === "all" || - (this.actionConfig.files === "selected" && item.selected), - ) - .map((item) => item.path), - }; - payload = JSON.stringify(data); } - const filename = this.actionConfig.filename.replace(/{{ uuid }}/, v4()); + return payload.replace( + /{{\s*(\w+)\s*}}/g, + (_, variableName) => { + if (variableName.endsWith("[]")) { + const variableNameClean = variableName.slice(-2); + const value = this.get_value_from_definition(variableNameClean); + const iteratable = Array.isArray(value) ? value : [value]; + return JSON.stringify(iteratable); + } else { + return this.get_value_from_definition(variableName); + } + } + ); + } + + type_json_to_download() { + + const filename = this.actionConfig.filename + .replace( + /{{\s*(\w+)\s*}}/g, + (_, variableName) => this.get_value_from_definition(variableName), + ); fetch(this.actionConfig.url, { method: this.actionConfig.method || "POST", headers: { - "Content-Type": "application/json", + ...{ + "Content-Type": "application/json", + }, + ...(this.actionConfig.headers || {}) }, - body: payload, + body: this.get_payload(), }) - .then((response) => { - if (response.ok) { - return response.blob(); - } else { - // http error - return Promise.reject( - new Error(`HTTP Error code: ${response.status}`), - ); - } - }) - .then((blob) => URL.createObjectURL(blob)) - .then((url) => { - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - }) - .catch((error) => { - console.log("Datafile action error : ", error); - this.snackBar.open( - "There has been an error performing the action", - "Close", - { - duration: 2000, - }, + .then((response) => { + if (response.ok) { + return response.blob(); + } else { + // http error + return Promise.reject( + new Error(`HTTP Error code: ${response.status}`), ); - }); + } + }) + .then((blob) => URL.createObjectURL(blob)) + .then((url) => { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + }) + .catch((error) => { + console.log("Datafile action error : ", error); + this.snackBar.open( + "There has been an error performing the action", + "Close", + { + duration: 2000, + }, + ); + }); return true; } type_xhr() { - for (const element of this.actionItems) { - const payload = this.actionConfig.payload || ""; - const url = this.actionConfig.url.replace( - /{{id}}/, - encodeURIComponent(element.pid), + + const url = this.actionConfig.url + .replace( + /{{\s*(\w+)\s*}}/g, + (_, variableName) => encodeURIComponent(this.get_value_from_definition(variableName)), ); - fetch(url, { - method: this.actionConfig.method || "POST", - headers: { + fetch(url, { + method: this.actionConfig.method || "POST", + headers: { + ...{ "Content-Type": "application/json", - Authorization: `Bearer ${this.authService.getToken().id}`, }, - body: payload, - }) - .then((response) => { - if (!response.ok) { - return Promise.reject( - new Error(`HTTP Error code: ${response.status}`), - ); - } - - this.store.dispatch( - updatePropertyAction({ - pid: element.pid, - property: JSON.parse(this.actionConfig.payload), - }), - ); - - return response; - }) - .catch((error) => { - console.log("Error: ", error); - this.snackBar.open( - "There has been an error performing the action", - "Close", - { - duration: 2000, - }, - ); - }); - } + ...(this.actionConfig.headers || {}) + }, + body: this.get_payload(), + }) + .then((response) => { + if (!response.ok) { + return Promise.reject( + new Error(`HTTP Error code: ${response.status}`), + ); + } + // specific only for datasets + // cannot be used + // this.store.dispatch( + // updatePropertyAction({ + // method: this.actionConfig.method, + // pid: element.pid, + // property: JSON.parse(this.actionConfig.payload), + // }), + // ); + + return response; + }) + .catch((error) => { + console.log("Error: ", error); + this.snackBar.open( + "There has been an error performing the action", + "Close", + { + duration: 2000, + }, + ); + }); + return true; } diff --git a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts index 7ab2b1d9ba..bdcee5b98d 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.interfaces.ts @@ -19,7 +19,8 @@ export interface ActionConfig { filename?: string; hidden?: string; variables?: Record; - inputs?: Record + inputs?: Record; + headers?: Record; } // export interface ActionItem { @@ -28,11 +29,13 @@ export interface ActionConfig { // isPublished?: boolean; // } +export interface ActionItemDataset { + pid: string; + sourceFolder?: string; + isPublished?: boolean; + files?: DataFiles_File[]; +} + export interface ActionItems { - datasets: { - pid: string; - sourceFolder?: string; - isPublished?: boolean; - files?: DataFiles_File[]; - }[], + datasets: ActionItemDataset[], } \ No newline at end of file diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.html b/src/app/shared/modules/configurable-actions/configurable-actions.component.html index 1e90fe58e7..6c23d1ad35 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.html +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.html @@ -5,7 +5,6 @@ [actionConfig]="actionConfig" [actionItems]="actionItems" [files]="files" - [maxFileSize]="maxFileSize" >
diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index 57ce178126..b3c3a4c5f1 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from "@angular/core"; -import { ActionConfig, ActionItem } from "./configurable-action.interfaces"; +import { ActionConfig, ActionItems } from "./configurable-action.interfaces"; import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; import { AppConfigService } from "app-config.service"; @@ -13,7 +13,7 @@ export class ConfigurableActionsComponent { private _sortedActionsConfig: ActionConfig[]; @Input({ required: true }) actionsConfig: ActionConfig[]; - @Input({ required: true }) actionItems: ActionItem[]; + @Input({ required: true }) actionItems: ActionItems; @Input() files?: DataFiles_File[]; @Input() visible = true; diff --git a/src/assets/config.json b/src/assets/config.json index fc7a1e49e4..a396a7a059 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -59,12 +59,15 @@ "datasetDetailsShowMissingProposalId": false, "notificationInterceptorEnabled": true, "metadataEditingUnitListDisabled": true, + "datasetActionsEnabled": false, + "datasetActions": [ + ], "datafilesActionsEnabled": true, "datafilesActions": [ { "id": "eed8efec-4354-11ef-a3b5-d75573a5d37f", "description": "This action let users download all files using the zip service", - "order": 5, + "order": 1, "label": "Download All", "files": "all", "mat_icon": "download", @@ -88,7 +91,7 @@ { "id": "3072fafc-4363-11ef-b9f9-ebf568222d26", "description": "This action let users download selected files using the zip service", - "order": 4, + "order": 2, "label": "Download Selected", "files": "selected", "mat_icon": "download", @@ -103,6 +106,8 @@ "folder": ".datasets[0].sourceFolder" }, "inputs" : { + "auth_token" : "#tokenBearer", + "jwt" : "#jwt", "item[]" : "@pid", "directory[]" : "@folder", "files[]": "@files" @@ -113,19 +118,59 @@ { "id": "4f974f0e-4364-11ef-9c63-03d19f813f4e", "description": "This action let users download jupyter notebook properly populated with dataset pid and all files using an instance of sciwyrm", - "order": 2, + "order": 3, "label": "Notebook All (Form)", "files": "all", "icon": "/assets/icons/jupyter_logo.png", "type": "form", "url": "https://www.scicat.info/notebook/all", "target": "_blank", + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path", + "totalSize": "[.datasets[0].files.size] | add", + "folder": ".datasets[0].sourceFolder" + }, + "enabled": "", + "inputs" : { + "auth_token" : "#token", + "jwt" : "#jwt", + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, + "authorization": ["#datasetAccess", "#datasetPublic"] + }, + { + "id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", + "order": 4, + "label": "Notebook Selected", + "files": "selected", + "icon": "/assets/icons/jupyter_logo.png", + "type": "form", + "url": "https://www.scicat.info/notebook/selected", + "target": "_blank", + "variables" : { + "pid": ".datasets[0].pid", + "files": ".datasets[0].files.path | select(.datasets[0].files.selected)", + "selected": "[.datasets[0].files.path | select(.datasets[0].files.selected)] | count", + "totalSize": "[.datasets[0].files.size | select(.datasets[0].files.selected)] | sum", + "folder": ".datasets[0].sourceFolder" + }, + "inputs" : { + "auth_token" : "#token", + "jwt" : "#jwt", + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, + "enabled": "#Length(@files)", "authorization": ["#datasetAccess", "#datasetPublic"] }, { "id": "0cd5b592-0b1a-11f0-a42c-23e177127ee7", "description": "This action let users download jupyter notebook properly populated with dataset pid and all files using an instance of sciwyrm", - "order": 3, + "order": 5, "label": "Notebook All (Download JSON)", "files": "all", "type": "json-download", @@ -134,20 +179,19 @@ "target": "_blank", "authorization": ["#datasetAccess", "#datasetPublic"], "variables" : { - "datasetPid": ".datasets[0].pid", + "pid": ".datasets[0].pid", "files": ".datasets[0].files.path", "folder": ".datasets[0].sourceFolder" }, - "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ datasetPid }}\",\"directory\":\"{{ sourceFolder }}\",\"files\": {{ files }},\"jwt\":\"{{ jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", - "filename": "{{ uuid }}.ipynb" + "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ @pid }}\",\"directory\":\"{{ @folder }}\",\"files\": {{ @files[] }},\"jwt\":\"{{ #jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", + "filename": "{{ #uuid }}.ipynb" }, { - "id": "224d8cb0-9233-11f0-991a-bfa2461d77ff", + "id": "a414773a-a526-11f0-a7f2-ff1026e5dba9", "description": "This action let users download jupyter notebook properly populated with dataset pid and selected files using an instance of sciwyrm", - "order": 4, + "order": 6, "label": "Notebook Selected (Download JSON)", - "files": "all", - "type": "json-download", + "type": "json-to-download", "icon": "/assets/icons/jupyter_logo.png", "url": "https://www.sciwyrm.info/notebook", "target": "_blank", @@ -163,16 +207,58 @@ "filename": "{{ uuid }}.ipynb" }, { - "id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", - "order": 1, - "label": "Notebook Selected", - "files": "selected", - "icon": "/assets/icons/jupyter_logo.png", - "type": "form", - "url": "https://www.scicat.info/notebook/selected", + "id": "9c6a11b6-a526-11f0-8795-6f025b320cc3", + "description": "This action let users make a call an arbitrary URL and store the reply in the store", + "order": 7, + "label": "Publish", + "type": "xhr", + "mat_icon": "action", + "method" : "PATCH", + "url": "http://localhost:3000/dataset/{{ @pid }}/", "target": "_blank", - "enabled": "#Selected", - "authorization": ["#datasetAccess", "#datasetPublic"] + "enabled": "#datasetOwner && @isPublished", + "authorization": "#datasetOwner && !@isPublished", + "variables" : { + "pid": ".datasets[0].pid", + "isPublished" : ".datasets[0].isPublished" + }, + "payload": "{\"isPublished\":\"true\"}", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "94a1d694-a526-11f0-947b-038d53cd837a", + "description": "This action let users make a call an arbitrary URL and store the reply in the store", + "order": 8, + "label": "Unpublish", + "type": "xhr", + "mat_icon": "action", + "method" : "PATCH", + "url": "http://localhost:3000/dataset/{{ @pid }}/", + "target": "_blank", + "enabled": "#datasetOwner && !@isPublished", + "authorization": "#datasetOwner && @isPublished", + "variables" : { + "pid": ".datasets[0].pid", + "isPublished" : ".datasets[0].isPublished" + }, + "payload": "{\"isPublished\":\"false\"}", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } + }, + { + "id": "c3bcbd40-a526-11f0-915a-93eeff0860ab", + "description": "This action let users jump to another URL entirely", + "order": 9, + "label": "ESS", + "type": "link", + "mat_icon": "action", + "url": "https://ess.eu", + "target": "_blank" } ], "labelMaps": { From f7b044d39a975a322e6631e3a54804f983c7dc5f Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Thu, 6 Nov 2025 16:17:26 +0100 Subject: [PATCH 07/32] saving work done so far --- .../batch-view/batch-view.component.html | 2 + .../datasets/datafiles/datafiles.component.ts | 10 +- .../dataset-detail-dynamic.component.ts | 14 ++ .../dataset-detail.component.html | 2 + .../configurable-action.component.ts | 142 ++++++++++++++---- .../configurable-action.interfaces.ts | 1 + .../configurable-actions.component.html | 1 - .../configurable-actions.component.ts | 2 +- 8 files changed, 138 insertions(+), 36 deletions(-) diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index 8267d0a1d0..5a8863b489 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -1,11 +1,13 @@
+
diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 48e5548577..b7ef9668e4 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -88,6 +88,7 @@ import { IngestorModule } from "../ingestor/ingestor.module"; import { MatExpansionModule } from "@angular/material/expansion"; import { MatBadgeModule } from "@angular/material/badge"; import { TitleCasePipe } from "shared/pipes/title-case.pipe"; +import { ConfigurableActionsModule } from "shared/modules/configurable-actions/configurable-actions.module"; @NgModule({ imports: [ @@ -154,7 +155,7 @@ import { TitleCasePipe } from "shared/pipes/title-case.pipe"; IngestorModule, MatExpansionModule, MatBadgeModule, - IngestorModule, + ConfigurableActionsModule, ], declarations: [ BatchViewComponent, diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index 90afc3d3e2..85357a69ed 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from "@angular/core"; import { ActionConfig, ActionItems } from "./configurable-action.interfaces"; -import { DataFiles_File } from "datasets/datafiles/datafiles.interfaces"; + import { AppConfigService } from "app-config.service"; @Component({ @@ -17,15 +17,15 @@ export class ConfigurableActionsComponent { constructor(public appConfigService: AppConfigService) {} - get visible(): boolean { + visible(): boolean { return this.appConfigService.getConfig().datafilesActionsEnabled; } - get maxFileSize(): number { + maxFileSize(): number { return this.appConfigService.getConfig().maxDirectDownloadSize || 0; } - get sortedActionsConfig(): ActionConfig[] { + sortedActionsConfig(): ActionConfig[] { this._sortedActionsConfig = this.actionsConfig; this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => a.order && b.order ? a.order - b.order : 0, diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts index aabde986c9..cc0abae980 100644 --- a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts @@ -25,7 +25,6 @@ import { TableVirtualScrollModule } from "../cores/table-virtual-scroll.module"; import { PrintTableDialogComponent } from "./extensions/print-dialog/print-dialog.component"; import { MatMenuModule } from "@angular/material/menu"; import { MatTooltipModule } from "@angular/material/tooltip"; -import { MatRippleModule } from "@angular/material/core"; import { TooltipComponent } from "../tooltip/tooltip.component"; import { FullscreenOverlayContainer, @@ -58,6 +57,7 @@ const ExtensionsModule = [HeaderFilterModule, RowMenuModule]; MatIconModule, DragDropModule, TableMenuModule, + MatDividerModule, MatPaginatorModule, MatDialogModule, MatButtonModule, @@ -66,6 +66,8 @@ const ExtensionsModule = [HeaderFilterModule, RowMenuModule]; ExtensionsModule, PipesModule, EmptyContentModule, + OverlayModule, + MatTooltipModule, ], exports: [DynamicMatTableComponent], declarations: [ From 97a620a6e7762640215b82fc7055465767b66acd Mon Sep 17 00:00:00 2001 From: junjiequan Date: Wed, 19 Nov 2025 16:52:54 +0100 Subject: [PATCH 21/32] include fallback for sortedActionsConfig() function --- .../configurable-actions/configurable-actions.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index 85357a69ed..80766fd1ce 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -26,6 +26,7 @@ export class ConfigurableActionsComponent { } sortedActionsConfig(): ActionConfig[] { + if (!this.actionsConfig) return []; this._sortedActionsConfig = this.actionsConfig; this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => a.order && b.order ? a.order - b.order : 0, From efcfc5afced76863d7061a0dd87436f9f718745f Mon Sep 17 00:00:00 2001 From: junjiequan Date: Wed, 19 Nov 2025 16:59:05 +0100 Subject: [PATCH 22/32] minor fix --- .../configurable-actions/configurable-actions.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index 80766fd1ce..df6650b853 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -12,7 +12,7 @@ import { AppConfigService } from "app-config.service"; export class ConfigurableActionsComponent { private _sortedActionsConfig: ActionConfig[]; - @Input({ required: true }) actionsConfig: ActionConfig[]; + @Input({ required: true }) actionsConfig: ActionConfig[] = []; @Input({ required: true }) actionItems: ActionItems; constructor(public appConfigService: AppConfigService) {} @@ -25,10 +25,9 @@ export class ConfigurableActionsComponent { return this.appConfigService.getConfig().maxDirectDownloadSize || 0; } - sortedActionsConfig(): ActionConfig[] { - if (!this.actionsConfig) return []; + get sortedActionsConfig(): ActionConfig[] { this._sortedActionsConfig = this.actionsConfig; - this._sortedActionsConfig.sort((a: ActionConfig, b: ActionConfig) => + this.actionsConfig.sort((a: ActionConfig, b: ActionConfig) => a.order && b.order ? a.order - b.order : 0, ); return this._sortedActionsConfig; From 0d100affb1633a0dac274bc5985997f3ba469687 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Wed, 19 Nov 2025 17:04:02 +0100 Subject: [PATCH 23/32] resolve budget exceeding error --- angular.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular.json b/angular.json index faf13bc3a5..181b25ab0e 100644 --- a/angular.json +++ b/angular.json @@ -56,7 +56,7 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "4mb" + "maximumError": "4.5mb" }, { "type": "anyComponentStyle", From f1b7ea3c3f56142d235d41ac07ca8141209d8adf Mon Sep 17 00:00:00 2001 From: junjiequan Date: Thu, 20 Nov 2025 11:00:57 +0100 Subject: [PATCH 24/32] fix unit test --- src/app/datasets/batch-view/batch-view.component.spec.ts | 4 +++- .../configurable-actions.component.spec.ts | 1 + .../configurable-actions/configurable-actions.component.ts | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/datasets/batch-view/batch-view.component.spec.ts b/src/app/datasets/batch-view/batch-view.component.spec.ts index 92c88544ee..77cd2cd67b 100644 --- a/src/app/datasets/batch-view/batch-view.component.spec.ts +++ b/src/app/datasets/batch-view/batch-view.component.spec.ts @@ -4,8 +4,9 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { BatchViewComponent } from "./batch-view.component"; import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { + MockActivatedRoute, MockArchivingService, MockDatasetApi, mockDataset as dataset, @@ -70,6 +71,7 @@ describe("BatchViewComponent", () => { { provide: Router, useValue: router }, { provide: DatasetsService, useClass: MockDatasetApi }, { provide: AppConfigService, useValue: { getConfig } }, + { provide: ActivatedRoute, useClass: MockActivatedRoute }, ], }, }); diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts index bb4dcf381d..941b1719cc 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts @@ -109,6 +109,7 @@ describe("1010: ConfigurableActionsComponent", () => { }); it("0060: there should be as many actions as defined in default configuration", async () => { + component.actionsConfig = mockActionsConfig; expect(component.sortedActionsConfig.length).toEqual( mockActionsConfig.length, ); diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts index df6650b853..12d543b3f7 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.ts @@ -17,11 +17,11 @@ export class ConfigurableActionsComponent { constructor(public appConfigService: AppConfigService) {} - visible(): boolean { + get visible(): boolean { return this.appConfigService.getConfig().datafilesActionsEnabled; } - maxFileSize(): number { + get maxFileSize(): number { return this.appConfigService.getConfig().maxDirectDownloadSize || 0; } From 1077303d6db66544c29c7839c697b56f1515f24d Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Thu, 20 Nov 2025 17:05:45 +0100 Subject: [PATCH 25/32] restored original actions on datasets list and selection --- CI/e2e/frontend.config.e2e.json | 243 ++++++++++++------ cypress/e2e/datasets/datasets-datafiles.cy.js | 98 ++++++- .../batch-view/batch-view.component.html | 94 ++++++- .../dataset-detail.component.html | 30 ++- .../configurable-action.component.ts | 7 + src/assets/config.json | 19 +- 6 files changed, 389 insertions(+), 102 deletions(-) diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 6041393e5b..40358ae692 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -1,6 +1,7 @@ { "accessTokenPrefix": "Bearer ", "addDatasetEnabled": false, + "allowConfigOverrides": true, "archiveWorkflowEnabled": false, "datasetReduceEnabled": true, "datasetJsonScientificMetadata": true, @@ -10,8 +11,10 @@ "editPublishedData": true, "addSampleEnabled": false, "externalAuthEndpoint": "/api/v3/auth/msad", - "facility": "SciCat Vanilla", + "facility": "SciCat Vanilla CI Test", "siteIcon": "site-header-logo.png", + "sitetitle": "", + "siteSciCatLogo": "", "loginFacilityLabel": "SciCat Vanilla", "loginLdapLabel": "Ldap", "loginLocalLabel": "Local", @@ -26,9 +29,18 @@ "jsonMetadataEnabled": true, "landingPage": "doi.ess.eu/detail/", "lbBaseURL": "http://localhost:3000", + "ingestorComponent": { + "ingestorEnabled": false, + "ingestorAutodiscoveryOptions": [ + { + "mailDomain": "university.ch", + "description": "University/facility of Choice", + "facilityBackend": "http://localhost:8888" + } + ] + }, "logbookEnabled": true, "loginFormEnabled": true, - "maxDirectDownloadSize": 1047521824, "thumbnailFetchLimitPerPage": 100, "metadataPreviewEnabled": true, "metadataStructure": "", @@ -37,7 +49,7 @@ "oAuth2Endpoints": [ { "authURL": "api/v3/auth/oidc", - "displayText": "ESS One Identity" + "displayText": "CI Test Identity" } ], "policiesEnabled": true, @@ -49,6 +61,9 @@ "searchSamples": true, "sftpHost": "", "shareEnabled": true, + "sourceFolder": "/data/scicat/vanilla", + "maxDirectDownloadSize": 5000000000, + "maxFileSizeWarning": "Some files are above and cannot be downloaded directly. These file can be downloaded via sftp host: in directory: ", "shoppingCartEnabled": true, "shoppingCartOnHeader": true, "tableSciDataEnabled": true, @@ -59,121 +74,205 @@ "datafilesActions": [ { "id": "eed8efec-4354-11ef-a3b5-d75573a5d37f", - "order": 4, + "description": "This action let users download all files using the zip service", + "order": 1, "label": "Download All", "files": "all", "mat_icon": "download", "type": "form", "url": "http://localhost:4200/download/all", "target": "_blank", - "enabled": "#SizeLimit", + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0FilesPath", + "totalSize": "#Dataset0FilesTotalSize", + "folder": "#Dataset0SourceFolder" + }, + "enabled": "#MaxDownloadableSize(@totalSize)", + "inputs" : { + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, "authorization": ["#datasetAccess", "#datasetPublic"] }, { "id": "3072fafc-4363-11ef-b9f9-ebf568222d26", - "order": 3, + "description": "This action let users download selected files using the zip service", + "order": 2, "label": "Download Selected", "files": "selected", "mat_icon": "download", "type": "form", "url": "http://localhost:4200/download/selected", "target": "_blank", - "enabled": "#Selected && #SizeLimit", + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0SelectedFilesPath", + "selected": "#Dataset0SelectedFilesCount", + "totalSize": "#Dataset0SelectedFilesTotalSize", + "folder": "#Dataset0SourceFolder" + }, + "inputs" : { + "auth_token" : "#tokenBearer", + "jwt" : "#jwt", + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, + "enabled": "#Length(@files) && #MaxDownloadableSize(@totalSize)", "authorization": ["#datasetAccess", "#datasetPublic"] }, { "id": "4f974f0e-4364-11ef-9c63-03d19f813f4e", - "order": 2, - "label": "Notebook All", + "description": "This action let users download jupyter notebook properly populated with dataset pid and all files using an instance of sciwyrm", + "order": 3, + "label": "Notebook All (Form)", "files": "all", "icon": "/assets/icons/jupyter_logo.png", "type": "form", - "url": "http://localhost:4200/notebook/all", + "url": "http://localhost:4200/notebook/all/form", "target": "_blank", + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0FilesPath", + "totalSize": "#Dataset0FilesTotalSize", + "folder": "#Dataset0SourceFolder" + }, + "enabled": "", + "inputs" : { + "auth_token" : "#token", + "jwt" : "#jwt", + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, "authorization": ["#datasetAccess", "#datasetPublic"] }, { "id": "fa3ce6ee-482d-11ef-95e9-ff2c80dd50bd", - "order": 1, - "label": "Notebook Selected", + "order": 4, + "label": "Notebook Selected (Form)", "files": "selected", "icon": "/assets/icons/jupyter_logo.png", "type": "form", - "url": "http://localhost:4200/notebook/selected", + "url": "http://localhost:4200/notebook/selected/form", "target": "_blank", - "enabled": "#Selected", + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0SelectedFilesPath", + "selected": "#Dataset0SelectedFilesCount", + "totalSize": "#Dataset0SelectedFilesTotalSize", + "folder": "#Dataset0SourceFolder" + }, + "inputs" : { + "auth_token" : "#token", + "jwt" : "#jwt", + "item[]" : "@pid", + "directory[]" : "@folder", + "files[]": "@files" + }, + "enabled": "#Length(@files) > 0", "authorization": ["#datasetAccess", "#datasetPublic"] - } - ], - "datasetDetailsActionsEnabled": true, - "datasetDetailsActions": [ - { - "id": "01", - "order": 1, - "type": "xhr", - "method": "PATCH", - "description": "Publish dataset", - "label": "Publish", - "hidden": "#isPublished", - "mat_icon": "", - "url": "/api/v3/datasets/{{id}}", - "payload": "{\"isPublished\": true}" }, { - "id": "02", - "order": 2, - "type": "xhr", - "method": "PATCH", - "description": "Unpublish published dataset", - "label": "Unpublish", - "hidden": "#!isPublished", - "mat_icon": "", - "url": "/api/v3/datasets/{{id}}", - "payload": "{\"isPublished\": false}" + "id": "0cd5b592-0b1a-11f0-a42c-23e177127ee7", + "description": "This action let users download jupyter notebook properly populated with dataset pid and all files using an instance of sciwyrm", + "order": 5, + "label": "Notebook All (Download JSON)", + "files": "all", + "type": "json-download", + "icon": "/assets/icons/jupyter_logo.png", + "url": "http://localhost:4200/notebook/all/json", + "target": "_blank", + "authorization": ["#datasetAccess", "#datasetPublic"], + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0FilesPath", + "folder": "#Dataset0SourceFolder" + }, + "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ @pid }}\",\"directory\":\"{{ @folder }}\",\"files\": {{ @files[] }},\"jwt\":\"{{ #jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", + "filename": "{{ #uuid }}.ipynb" }, { - "id": "03", - "order": 3, - "type": "form", - "method": "GET", - "description": "Jupyter hub", - "label": "Jupyter hub", - "mat_icon": "", - "url": "https://jupyterhub.esss.lu.se/", - "target": "_blank" - } - ], - "datasetSelectionActionsEnabled": true, - "datasetSelectionActions": [ + "id": "a414773a-a526-11f0-a7f2-ff1026e5dba9", + "description": "This action let users download jupyter notebook properly populated with dataset pid and selected files using an instance of sciwyrm", + "order": 6, + "label": "Notebook Selected (Download JSON)", + "type": "json-to-download", + "icon": "/assets/icons/jupyter_logo.png", + "url": "http://localhost:4200/notebook/selected/json", + "target": "_blank", + "enabled": "#Length(@files) > 0", + "authorization": ["#datasetAccess", "#datasetPublic"], + "variables" : { + "pid": "#Dataset0Pid", + "files": "#Dataset0SelectedFilesPath", + "selected": "#Dataset0SelectedFilesCount", + "folder": "#Dataset0SourceFolder" + }, + "payload": "{\"template_id\":\"c975455e-ede3-11ef-94fb-138c9cd51fc0\",\"parameters\":{\"dataset\":\"{{ @pid }}\",\"directory\":\"{{ @folder }}\",\"files\": {{ @files[] }},\"jwt\":\"{{ #jwt }}\",\"scicat_url\":\"https://staging.scicat.ess.url\",\"file_server_url\":\"sftserver2.esss.dk\",\"file_server_port\":\"22\"}}", + "filename": "{{ #uuid }}.ipynb" + }, { - "id": "01", - "order": 1, - "type": "link", - "description": "Publish datasets", + "id": "9c6a11b6-a526-11f0-8795-6f025b320cc3", + "description": "This action let users make a call an arbitrary URL and store the reply in the store", + "order": 7, "label": "Publish", - "mat_icon": "school", - "url": "/datasets/batch/publish" + "type": "xhr", + "mat_icon": "action", + "method" : "PATCH", + "url": "http://localhost:4200/dataset/{{ @pid }}/", + "target": "_blank", + "enabled": "#datasetOwner && @isPublished", + "authorization": "#datasetOwner && !@isPublished", + "variables" : { + "pid": "@Dataset0Pid", + "isPublished" : "#Dataset[0]Field[isPublished]" + }, + "payload": "{\"isPublished\":\"true\"}", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } }, { - "id": "02", - "order": 2, - "type": "link", - "description": "Share datasets", - "label": "Share", - "mat_icon": "share", - "url": "/datasets/batch?share=true" + "id": "94a1d694-a526-11f0-947b-038d53cd837a", + "description": "This action let users make a call an arbitrary URL and store the reply in the store", + "order": 8, + "label": "Unpublish", + "type": "xhr", + "mat_icon": "action", + "method" : "PATCH", + "url": "http://localhost:4200/dataset/{{ @pid }}/", + "target": "_blank", + "enabled": "#datasetOwner && !@isPublished", + "authorization": "#datasetOwner && @isPublished", + "variables" : { + "pid": "#Dataset0Pid", + "isPublished" : "#Dataset[0]Field[isPublished]" + }, + "payload": "{\"isPublished\":\"false\"}", + "headers": { + "Content-Type": "application/json", + "Authorization": "#tokenBearer" + } }, { - "id": "03", - "order": 3, + "id": "c3bcbd40-a526-11f0-915a-93eeff0860ab", + "description": "This action let users jump to another URL entirely", + "order": 9, + "label": "ESS", "type": "link", - "hidden": "!archiveWorkflowEnabled", - "description": "Retrieve datasets", - "label": "Retrieve", - "mat_icon": "cloud_download", - "url": "/datasets/batch?retrieve=true" + "mat_icon": "action", + "url": "http://localhost:4200/external", + "target": "_blank" } ], + "datasetDetailsActionsEnabled": false, + "datasetDetailsActions": [], + "selectionActionsEnabled": true, + "selectionActions": [], "labelMaps": { "filters": { "LocationFilter": "Location", diff --git a/cypress/e2e/datasets/datasets-datafiles.cy.js b/cypress/e2e/datasets/datasets-datafiles.cy.js index c81cbaaaff..fae038386f 100644 --- a/cypress/e2e/datasets/datasets-datafiles.cy.js +++ b/cypress/e2e/datasets/datasets-datafiles.cy.js @@ -21,10 +21,18 @@ describe("Dataset datafiles", () => { const actionUrl = { downloadSelected: "http://localhost:4200/download/selected", downloadAll: "http://localhost:4200/download/all", - notebookSelected: "http://localhost:4200/notebook/selected", - notebookAll: "http://localhost:4200/notebook/all", + notebookFormSelected: "http://localhost:4200/notebook/selected/form", + notebookFormAll: "http://localhost:4200/notebook/all/form", + notebookJsonSelected: "/notebook/selected/json", + notebookJsonAll: "/notebook/all/json", }; - it("Should be able to download/notebook with selected/all", () => { + it("Should be able to download or notebook (form) with selected or all files", () => { + // Intercept the expected network request + // cy.intercept('POST', 'https://zip.scicatproject.org/download/all').as('DownloadFormAll'); + // cy.intercept('POST', 'https://zip.scicatproject.org/download/selected').as('DownloadFormSelected'); + // cy.intercept('POST', 'https://zip.scicatproject.org/notebook/all').as('DownloadNotebookAll'); + // cy.intercept('POST', 'https://zip.scicatproject.org/notebook/selected').as('DownloadNotebookSelected'); + cy.createDataset({ type: "raw", dataFileSize: "small" }); cy.visit("/datasets"); @@ -63,19 +71,87 @@ describe("Dataset datafiles", () => { cy.get("form").eq(1).should("have.attr", "action", actionUrl.downloadAll); // Test notebook selected - cy.get('button:contains("Notebook Selected")').click(); + cy.get('button:contains("Notebook Selected (Form)")').click(); cy.get("@formSubmit").should("have.been.called", 3); cy.get("form") .eq(2) - .should("have.attr", "action", actionUrl.notebookSelected); + .should("have.attr", "action", actionUrl.notebookFormSelected); // Test notebook all - cy.get('button:contains("Notebook All")').click(); + cy.get('button:contains("Notebook All (Form)")').click(); cy.get("@formSubmit").should("have.been.called", 4); - cy.get("form").eq(3).should("have.attr", "action", actionUrl.notebookAll); + cy.get("form").eq(3).should("have.attr", "action", actionUrl.notebookFormAll); + }); + + it("Should be able to download the notebook from sciwyrm with selected or all files", () => { + // // Intercept the expected network request + // cy.intercept('POST', actionUrl.notebookJsonAll, { + // statusCode: 200, + // body: { name: "Notebook Json All" } + // }).as('DownloadNotebookAll'); + // cy.intercept('POST', actionUrl.notebookJsonSelected, { + // statusCode: 200, + // body: { name: "Notebook Json Select" } + // }).as('DownloadNotebookSelected'); + + cy.window().then((win) => { + cy.stub(win.document, 'createElement').callsFake((tag) => { + if (tag === 'a') { + // Return a spy-able anchor element + const a = document.createElement('a'); + cy.spy(a, 'click').as('aClick'); + return a; + } + return document.createElement(tag); + }); + cy.stub(win.URL, 'createObjectURL').callsFake(() => 'blob:fake-url'); + }); + + cy.createDataset({ type: "raw", dataFileSize: "small" }); + + cy.visit("/datasets"); + + cy.get(".dataset-table mat-table mat-header-row").should("exist"); + + cy.finishedLoading(); + + cy.get('[data-cy="text-search"]').clear().type("Cypress"); + cy.get('[data-cy="search-button"]').click(); + + cy.isLoading(); + + cy.get("mat-row").contains("Cypress Dataset").first().click(); + + cy.wait("@fetch"); + + cy.get(".mat-mdc-tab-link").contains("Datafiles").click(); + + cy.get(".mdc-checkbox__native-control").eq(1).check(); + + + //cy.intercept('POST', '/your/download/url').as('downloadRequest'); + + // Test notebook selected + cy.get('button:contains("Notebook Selected (Download JSON)")').click(); + // Wait for the intercepted call and assert the response + // cy.wait('@DownloadNotebookSelected').then((interception) => { + // expect(interception.request.headers['Content-Type']).to.eq('application/json'); + // expect(interception.request.body.template_id).to.eq("c975455e-ede3-11ef-94fb-138c9cd51fc0"); + // }); + // Assert anchor was created and clicked + cy.get('@aClick').should('have.been.called'); + + // Test notebook all + cy.get('button:contains("Notebook All (Download JSON)")').click(); + // cy.wait('@DownloadNotebookAll').then((interception) => { + // expect(interception.request.headers['Content-Type']).to.eq('application/json'); + // expect(interception.request.body.template_id).to.eq("c975455e-ede3-11ef-94fb-138c9cd51fc0"); + // }); + // Assert anchor was created and clicked + cy.get('@aClick').should('have.been.called'); }); - it("Should not be able to download selected/all file that is exceeding size limit", () => { + it("Should not be able to download selected/all files that is exceeding size limit", () => { cy.createDataset({ type: "raw", dataFileSize: "large" }); cy.visit("/datasets"); @@ -103,8 +179,10 @@ describe("Dataset datafiles", () => { cy.get('button:contains("Download Selected")').should("be.disabled"); cy.get('button:contains("Download All")').should("be.disabled"); - cy.get('button:contains("Notebook Selected")').should("not.be.disabled"); - cy.get('button:contains("Notebook Selected")').should("not.be.disabled"); + cy.get('button:contains("Notebook Selected (Form)")').should("not.be.disabled"); + cy.get('button:contains("Notebook All (Form)")').should("not.be.disabled"); + cy.get('button:contains("Notebook Selected (Download JSON)")').should("not.be.disabled"); + cy.get('button:contains("Notebook All (Download JSON)")').should("not.be.disabled"); cy.get(".mdc-checkbox__native-control").eq(1).uncheck(); cy.get(".mdc-checkbox__native-control").eq(2).check(); diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index 5a8863b489..948d9296c2 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -1,17 +1,93 @@
-
- - + + + + + + + + + + +
- +
+ +
+ + Public + +
+
diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index dab0efe5cc..d79d5d94a1 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -388,6 +388,9 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { (_, variableName) => this.get_value_from_definition(variableName), ); + console.log("JSON to download"); + console.log("URL",this.actionConfig.url); + console.log("Method",this.actionConfig.method); fetch(this.actionConfig.url, { method: this.actionConfig.method || "POST", headers: { @@ -399,6 +402,7 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { body: this.get_payload(), }) .then((response) => { + console.log("Response",response); if (response.ok) { return response.blob(); } else { @@ -410,9 +414,12 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { }) .then((blob) => URL.createObjectURL(blob)) .then((url) => { + console.log("VFC 1"); const a = document.createElement("a"); + console.log("a",a); a.href = url; a.download = filename; + console.log a.click(); URL.revokeObjectURL(url); }) diff --git a/src/assets/config.json b/src/assets/config.json index 8b7864b78e..3c37493b3f 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -27,7 +27,6 @@ "ingestManual": null, "jobsEnabled": true, "jsonMetadataEnabled": true, - "jupyterHubUrl": "https://jupyterhub.esss.lu.se/", "landingPage": "doi.ess.eu/detail/", "lbBaseURL": "http://127.0.0.1:3000", "ingestorComponent": { @@ -70,9 +69,6 @@ "datasetDetailsShowMissingProposalId": false, "notificationInterceptorEnabled": true, "metadataEditingUnitListDisabled": true, - "datasetActionsEnabled": false, - "datasetActions": [ - ], "datafilesActionsEnabled": true, "datafilesActions": [ { @@ -272,6 +268,21 @@ "target": "_blank" } ], + "datasetDetailsActionsEnabled": false, + "datasetDetailsActions": [], + "selectionActionsEnabled": true, + "selectionActions": [], + "labelMaps": { + "filters": { + "LocationFilter": "Location", + "PidFilter": "Pid", + "GroupFilter": "Group", + "TypeFilter": "Type", + "KeywordFilter": "Keyword", + "DateRangeFilter": "Start Date - End Date", + "TextFilter": "Text" + } + }, "defaultDatasetsListSettings": { "columns": [ { From 672fbf644e401052f509fc37f8bada19906c9824 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Fri, 21 Nov 2025 14:16:46 +0100 Subject: [PATCH 26/32] Fixed CI configurable actions config, fixed errors from configuration --- CI/e2e/frontend.config.e2e.json | 6 +++--- .../configurable-action.component.ts | 18 +++++++++++++++--- .../configurable-actions.component.scss | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 40358ae692..920d42129d 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -224,7 +224,7 @@ "method" : "PATCH", "url": "http://localhost:4200/dataset/{{ @pid }}/", "target": "_blank", - "enabled": "#datasetOwner && @isPublished", + "enabled": "(#datasetOwner || #userIsAdmin) && !@isPublished", "authorization": "#datasetOwner && !@isPublished", "variables" : { "pid": "@Dataset0Pid", @@ -246,7 +246,7 @@ "method" : "PATCH", "url": "http://localhost:4200/dataset/{{ @pid }}/", "target": "_blank", - "enabled": "#datasetOwner && !@isPublished", + "enabled": "(#datasetOwner || #userIsAdmin) && @isPublished", "authorization": "#datasetOwner && @isPublished", "variables" : { "pid": "#Dataset0Pid", @@ -264,7 +264,7 @@ "order": 9, "label": "ESS", "type": "link", - "mat_icon": "action", + "icon": "/assets/icons/button_ess.png", "url": "http://localhost:4200/external", "target": "_blank" } diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index d79d5d94a1..f1a4df66e8 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -16,7 +16,7 @@ import { Store } from "@ngrx/store"; import { updatePropertyAction } from "state-management/actions/datasets.actions"; import { Router } from "@angular/router"; import { AppConfigService } from "app-config.service"; -import { selectProfile } from "state-management/selectors/user.selectors"; +import { selectIsAdmin, selectProfile } from "state-management/selectors/user.selectors"; import { Subscription } from "rxjs"; import { result } from "lodash-es"; @@ -121,6 +121,7 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { @Input({ required: true }) actionItems: ActionItems; //@Input() files?: DataFiles_File[]; userProfile$ = this.store.select(selectProfile); + isAdmin$ = this.store.select(selectIsAdmin); jwt = ""; use_mat_icon = false; @@ -133,6 +134,7 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { subscriptions: Subscription[] = []; userProfile: any = {}; + isAdmin = false; constructor( private usersService: UsersService, @@ -180,6 +182,8 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { // eslint-disable-next-line no-useless-escape .replace(/\#datasetOwner/g, (_) => `datasetOwner`) // eslint-disable-next-line no-useless-escape + .replace(/\#userIsAdmin/g, (_) => `isAdmin`) + // eslint-disable-next-line no-useless-escape .replace(/\@(\w+)/g, (_, variableName) => `variables.${variableName}`) ); } @@ -215,6 +219,13 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { } }), ); + this.subscriptions.push( + this.isAdmin$.subscribe((isAdmin) => { + if (isAdmin) { + this.isAdmin = isAdmin; + } + }), + ); this.use_mat_icon = !!this.actionConfig.mat_icon; this.use_icon = this.actionConfig.icon !== undefined; this.prepare_disabled_condition(); @@ -244,6 +255,7 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { return this.userProfile.accessGroups?.includes(d.ownerGroup) || false; }) as Array ).some(Boolean), + isAdmin: this.isAdmin, }; } @@ -251,13 +263,13 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { this.update_status(); const expr = this.disabled_condition; - + console.log("Disable Expr",expr); const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); const context = this.context; const res = fn(context); - + console.log("res",res); return res; } diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.scss b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss index 5848d29eee..a0d3feb528 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.scss +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.scss @@ -1,4 +1,4 @@ .configurable-actions { float: right; - margin: 1em; + //margin: 1em; } From f97d85b6db22f7a3df51b961869fe5bb29f5f627 Mon Sep 17 00:00:00 2001 From: Max Novelli Date: Fri, 21 Nov 2025 14:27:56 +0100 Subject: [PATCH 27/32] fixed linting --- .../configurable-action.component.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/app/shared/modules/configurable-actions/configurable-action.component.ts b/src/app/shared/modules/configurable-actions/configurable-action.component.ts index f1a4df66e8..86d91f8df5 100644 --- a/src/app/shared/modules/configurable-actions/configurable-action.component.ts +++ b/src/app/shared/modules/configurable-actions/configurable-action.component.ts @@ -16,7 +16,10 @@ import { Store } from "@ngrx/store"; import { updatePropertyAction } from "state-management/actions/datasets.actions"; import { Router } from "@angular/router"; import { AppConfigService } from "app-config.service"; -import { selectIsAdmin, selectProfile } from "state-management/selectors/user.selectors"; +import { + selectIsAdmin, + selectProfile, +} from "state-management/selectors/user.selectors"; import { Subscription } from "rxjs"; import { result } from "lodash-es"; @@ -263,13 +266,9 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { this.update_status(); const expr = this.disabled_condition; - console.log("Disable Expr",expr); const fn = new Function("ctx", `with (ctx) { return (${expr}); }`); - const context = this.context; - const res = fn(context); - console.log("res",res); return res; } @@ -400,9 +399,6 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { (_, variableName) => this.get_value_from_definition(variableName), ); - console.log("JSON to download"); - console.log("URL",this.actionConfig.url); - console.log("Method",this.actionConfig.method); fetch(this.actionConfig.url, { method: this.actionConfig.method || "POST", headers: { @@ -414,7 +410,6 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { body: this.get_payload(), }) .then((response) => { - console.log("Response",response); if (response.ok) { return response.blob(); } else { @@ -426,12 +421,9 @@ export class ConfigurableActionComponent implements OnInit, OnChanges { }) .then((blob) => URL.createObjectURL(blob)) .then((url) => { - console.log("VFC 1"); const a = document.createElement("a"); - console.log("a",a); a.href = url; a.download = filename; - console.log a.click(); URL.revokeObjectURL(url); }) From 1a989b7130406574b931f644d8ea261c6a4e0c4b Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 24 Nov 2025 11:55:31 +0100 Subject: [PATCH 28/32] fix published-data e2e flaky selector --- cypress/e2e/published-data/published-data.cy.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/published-data/published-data.cy.js b/cypress/e2e/published-data/published-data.cy.js index 871c06b0f9..e0fa4a7d06 100644 --- a/cypress/e2e/published-data/published-data.cy.js +++ b/cypress/e2e/published-data/published-data.cy.js @@ -140,9 +140,8 @@ describe("Datasets general", () => { cy.isLoading(); - cy.get(".dataset-table mat-row input[type='checkbox']") - .last() - .and("not.be.disabled") + cy.get(".dataset-table mat-row input[type='checkbox']:not(:disabled)") + .first() .click(); cy.get("#addToBatchButton").click(); From 892c98d1c48a31f0df149331a0de99121a6eefbb Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 24 Nov 2025 12:22:46 +0100 Subject: [PATCH 29/32] fix published-data flaky selector --- cypress/e2e/published-data/published-data.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/published-data/published-data.cy.js b/cypress/e2e/published-data/published-data.cy.js index e0fa4a7d06..f244ffdd42 100644 --- a/cypress/e2e/published-data/published-data.cy.js +++ b/cypress/e2e/published-data/published-data.cy.js @@ -141,7 +141,7 @@ describe("Datasets general", () => { cy.isLoading(); cy.get(".dataset-table mat-row input[type='checkbox']:not(:disabled)") - .first() + .last() .click(); cy.get("#addToBatchButton").click(); From fc642ea49ea4f461d3fdd79dae03f949e381aa33 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 24 Nov 2025 13:19:36 +0100 Subject: [PATCH 30/32] enable cypress video --- cypress.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress.config.ts b/cypress.config.ts index d2728dcc22..a213335ffe 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -19,4 +19,6 @@ export default defineConfig({ defaultCommandTimeout: 10000, retries: 1, }, + video: true, + videosFolder: "cypress/videos", // default }); From 479cf1ceaf254a0814126e03859f85dbb73e4353 Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 24 Nov 2025 15:10:01 +0100 Subject: [PATCH 31/32] fix published-data e2e test --- cypress.config.ts | 2 -- cypress/e2e/published-data/published-data.cy.js | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index a213335ffe..d2728dcc22 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -19,6 +19,4 @@ export default defineConfig({ defaultCommandTimeout: 10000, retries: 1, }, - video: true, - videosFolder: "cypress/videos", // default }); diff --git a/cypress/e2e/published-data/published-data.cy.js b/cypress/e2e/published-data/published-data.cy.js index f244ffdd42..8275381771 100644 --- a/cypress/e2e/published-data/published-data.cy.js +++ b/cypress/e2e/published-data/published-data.cy.js @@ -95,8 +95,10 @@ describe("Datasets general", () => { }); it("should be able to edit dataset list after creating the published data - 1", () => { - cy.createDataset({ type: "raw" }); - cy.createDataset({ type: "raw" }); + const testDatasetName1 = "Test_Published_Dataset_1"; + const testDatasetName2 = "Test_Published_Dataset_2"; + cy.createDataset({ type: "raw", datasetName: testDatasetName1 }); + cy.createDataset({ type: "raw", datasetName: testDatasetName2 }); cy.visit("/datasets"); @@ -104,7 +106,7 @@ describe("Datasets general", () => { cy.finishedLoading(); - cy.get('[data-cy="text-search"]').clear().type("Cypress"); + cy.get('[data-cy="text-search"]').clear().type(testDatasetName2); cy.get('[data-cy="search-button"]').click(); cy.isLoading(); @@ -135,14 +137,12 @@ describe("Datasets general", () => { cy.finishedLoading(); - cy.get('[data-cy="text-search"]').clear().type("Cypress"); + cy.get('[data-cy="text-search"]').clear().type(testDatasetName1); cy.get('[data-cy="search-button"]').click(); cy.isLoading(); - cy.get(".dataset-table mat-row input[type='checkbox']:not(:disabled)") - .last() - .click(); + cy.get(".dataset-table mat-row input[type='checkbox']").first().click(); cy.get("#addToBatchButton").click(); From 11d201777799db4fb157e1b3a58122b7dc81689e Mon Sep 17 00:00:00 2001 From: junjiequan Date: Mon, 24 Nov 2025 15:51:26 +0100 Subject: [PATCH 32/32] fix unit test --- src/app/datasets/datasets.module.ts | 1 - .../configurable-actions/configurable-actions.component.spec.ts | 1 - src/app/shared/shared.module.ts | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 25828a3ffd..b24483e6fb 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -156,7 +156,6 @@ import { OverlayModule } from "@angular/cdk/overlay"; IngestorModule, MatExpansionModule, MatBadgeModule, - ConfigurableActionsModule, OverlayModule, IngestorModule, ], diff --git a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts index 941b1719cc..bb4dcf381d 100644 --- a/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts +++ b/src/app/shared/modules/configurable-actions/configurable-actions.component.spec.ts @@ -109,7 +109,6 @@ describe("1010: ConfigurableActionsComponent", () => { }); it("0060: there should be as many actions as defined in default configuration", async () => { - component.actionsConfig = mockActionsConfig; expect(component.sortedActionsConfig.length).toEqual( mockActionsConfig.length, ); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 5cd87ee555..787e9585de 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -85,6 +85,7 @@ import { TranslateModule } from "@ngx-translate/core"; JsonFormsAngularMaterialModule, JsonFormsCustomRenderersModule, SharedFilterModule, + ConfigurableActionsModule, ], }) export class SharedScicatFrontendModule {}